Infracost and Gitlab
My current terraform infrastructure run inside Gitlab, and it’s deployed through CI/CD. There is nice feature from Terraform cloud that is the cost estimation. My infrastructure is definitely not big but I through why not having an extra check/information about how much will cost my current playground. Quick look on internet and Infracost come leading my query.
What is Infracost
According to the official website the definition is actually simple:
Cloud cost estimates for Terraform in pull requests
Basically, every time I will do a pull request on my repo, Terracost will estimate the cost change and comment the pull request with the data. There are 3 million prices available for Google/Azure/AWS.
The interesting part is the fact that you can also indicate the current usage of your resource like lambda, API gateway and improve the accuracy of the cost estimation
I’m mainly using Scaleway for the moment but I have few ressources on AWS and Azure that I’m curious about.
Gitlab CI/CD integration
The documentation is available here in Gitlab.
My pipeline will do the following steps for each pull request:
- terraform-validate
- terraform-plan
- infracost
The first step I did is to copy the infracost.yml
file into my repo and rename it infracost.gitlab-ci
and place it under the root folder gitlab-ci
. By default, I put every template CI in this folder, it’s purely a repo organization mater.
.
├───.vscode
├───gitlab-ci
│ └───infracost.gitlab-ci.yml
├───.gitlab-ci.yml
├───.gitignore
├───README.md
└───terraform
├───main.tf
├───variables.tf
├───providers.tf
└───versions.tf
Then I load the template into my main CI pipeline description file .gitlab-ci.yml
include:
- local: '/gitlab-ci/infracost.gitlab-ci.yml'
Then I add my Infracost stage
stages:
- terraform-validate
- terraform-plan
- infracost # New stage added here
- terraform-apply
To finish I add the job
tf-cost:
extends: .infracost # calling the template
environment:
name: prod
variables:
path: "plan.cache" # In my case the path of the plan is coming from the previous plan stage and the arctfact in my case is call plan.cache
stage: infracost
when: on_success
before_script:
- cd ${TF_ROOT}
dependencies:
- tf-plan
allow_failure: true # in case of failure the pipeline will not failed.
only:
refs:
# this will be only trigger for the master branch and merge request
- master
- merge_requests
Gitlab terraform backend config
I’m currently using the Gitlab terraform backend to host my state. In this condition, the Infracost need to be updated with the variables used to authenticate against the backend. You will also need to trigger terraform init
during the before_script
step.
tf-cost:
extends: .infracost
environment:
name: prod
variables:
path: "plan.cache"
# HTTP backend variables
TF_HTTP_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
TF_HTTP_USERNAME: "gitlab-ci-token"
TF_HTTP_PASSWORD: "${CI_JOB_TOKEN}"
TF_HTTP_LOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
TF_HTTP_UNLOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
stage: infracost
when: on_success
before_script:
# browse to th folder of the terraform file and trigger init
- cd ${TF_ROOT}
- terraform init
dependencies:
- tf-plan
allow_failure: true
only:
refs:
- master
- merge_requests
Override the default terraform version
During my setup I was facing an issue with my terraform version, I’m currently running with terraform 1.0.3
version and the latest version inside the Terracost image is 1.0.2
. I had to install the 1.0.3
version directly in the job as follows:
tf-cost:
extends: .infracost
environment:
name: prod
variables:
path: "plan.cache"
TF_HTTP_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
TF_HTTP_USERNAME: "gitlab-ci-token"
TF_HTTP_PASSWORD: "${CI_JOB_TOKEN}"
TF_HTTP_LOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
TF_HTTP_UNLOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
stage: infracost
when: on_success
before_script:
# Add another terraform version
- apk add --no-cache curl unzip
- cd /tmp
- curl -L "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" > t.zip
- unzip -o t.zip
- rm t.zip
- ls -la
- mv -f terraform /usr/bin/terraform_${TF_VERSION}
- ln -s -f /usr/bin/terraform_${TF_VERSION} /usr/bin/terraform
- cd ${TF_ROOT}
- terraform init # don;t forget to init otherwise it will failed
dependencies:
- tf-plan
allow_failure: true
only:
refs:
- master
- merge_requests
Final result
Create a new branch, edit the code and create a pull request on your main branch.
Gitlab CI will create a detached pipeline and run the job matching the only.refs:
information.
only:
refs:
- master
- merge_requests
The pipeline with the different stage:
The pull request comment 🥳:
The full pipeline
# ---------------------------------------------------------------------------- #
# Default configuration #
# ---------------------------------------------------------------------------- #
default:
tags:
- gitlab-org-docker
variables:
TF_ROOT: ${CI_PROJECT_DIR}/terraform
TF_VAR_GITLAB_USER: ${CI_PROJECT_NAMESPACE}
TF_VAR_GITHUB_USER: ${CI_PROJECT_NAMESPACE}
TF_INPUT: 0
INFRACOST_CURRENCY: EUR # currency for infracost
TF_VERSION: "1.0.3"
TF_SEC_VERSION: "v0.58.6"
# ---------------------------------------------------------------------------- #
# Templates #
# ---------------------------------------------------------------------------- #
include:
- local: '/gitlab-ci/infracost.gitlab-ci.yml' # infra cost template
# ---------------------------------------------------------------------------- #
# stage #
# ---------------------------------------------------------------------------- #
stages:
- terraform-validate
- terraform-plan
- infracost
- terraform-apply
# ---------------------------------------------------------------------------- #
# terraform pipeline job #
# ---------------------------------------------------------------------------- #
tf-validate:
image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
environment:
name: prod
variables:
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
stage: terraform-validate
cache:
key: terraform-${CI_ENVIRONMENT_NAME}
paths:
- ${TF_ROOT}/.terraform
before_script:
- cd ${TF_ROOT}
script:
- gitlab-terraform init
- gitlab-terraform validate
only:
refs:
- master
- merge_requests
- terraform/**/*
tf-plan:
image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
environment:
name: prod
variables:
TF_VAR_ENV: $CI_ENVIRONMENT_NAME
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
stage: terraform-plan
before_script:
- cd ${TF_ROOT}
script:
- gitlab-terraform plan
- gitlab-terraform plan-json
when: on_success
cache:
key: terraform-${CI_ENVIRONMENT_NAME}
paths:
- ${TF_ROOT}/.terraform
artifacts:
name: plan
paths:
- ${TF_ROOT}/plan.cache
reports:
terraform: ${TF_ROOT}/plan.json
only:
refs:
- master
- merge_requests
- terraform/**/*
tf-cost:
extends: .infracost
environment:
name: prod
variables:
path: "plan.cache"
TF_HTTP_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
TF_HTTP_USERNAME: "gitlab-ci-token"
TF_HTTP_PASSWORD: "${CI_JOB_TOKEN}"
TF_HTTP_LOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
TF_HTTP_UNLOCK_ADDRESS: "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}/lock"
stage: infracost
when: on_success
before_script:
- apk add --no-cache curl unzip
- cd /tmp
- curl -L "https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip" > t.zip
- unzip -o t.zip
- rm t.zip
- ls -la
- mv -f terraform /usr/bin/terraform_${TF_VERSION}
- ln -s -f /usr/bin/terraform_${TF_VERSION} /usr/bin/terraform
- cd ${TF_ROOT}
- terraform init
cache:
key: terraform-${CI_ENVIRONMENT_NAME}
paths:
- ${TF_ROOT}/.terraform
dependencies:
- tf-plan
allow_failure: true
only:
refs:
- master
- merge_requests
- terraform/**/*
tf-apply:
image: registry.gitlab.com/gitlab-org/terraform-images/stable:latest
environment:
name: prod
variables:
TF_VAR_ENV: ${CI_ENVIRONMENT_NAME}
TF_ADDRESS: ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/terraform/state/${CI_ENVIRONMENT_NAME}
stage: terraform-apply
before_script:
- cd ${TF_ROOT}
script:
- gitlab-terraform apply
when: on_success
cache:
key: terraform-${CI_ENVIRONMENT_NAME}
paths:
- ${TF_ROOT}/.terraform
dependencies:
- tf-plan
only:
refs:
- master
- terraform/**/*
Sources: