Skip to main content

GitLab CI

Run Infracost in GitLab CI to see cloud cost estimates and FinOps best practices in merge requests.

tip

We recommend using the GitLab App where possible — it's simpler to set up and faster to run.

Quick start

  1. Install the Infracost CLI and run infracost auth login to get a free API key. Retrieve it with infracost configure get api_key.

  2. Add a project CI/CD variable called INFRACOST_API_KEY with your API key. Mark it masked, and untick Protect variable so it works on all merge requests.

  3. Add a Personal access token (or a Group / Project access token on premium / self-managed) with the api scope and Maintainer role, so it can read and write merge request comments. Add it as a masked, unprotected CI/CD variable called GITLAB_TOKEN.

  4. Create a .gitlab-ci.yml file in your repo with the following content:

variables:
TF_ROOT: RELATIVE/PATH/TO/TERRAFORM/CODE
stages:
# Runs the Infracost CLI and posts the merge request comment
- infracost:merge-request-checks
# These stages are needed when using Infracost Cloud
- infracost:default-branch-update
- infracost:merge-request-status-update

# Run Infracost on merge requests to check costs and policies
infracost:merge-request-checks:
stage: infracost:merge-request-checks
image:
name: infracost/infracost:ci-0.10
entrypoint: ['']
script:
# Clone the base branch of the merge request into a temp directory
- git clone $CI_REPOSITORY_URL --branch=$CI_MERGE_REQUEST_TARGET_BRANCH_NAME --single-branch /tmp/base

# Generate the cost baseline from the comparison branch
- |
infracost breakdown --path=/tmp/base/${TF_ROOT} \
--format=json \
--out-file=infracost-base.json

# Generate the diff
- |
infracost diff --path=${TF_ROOT} \
--compare-to=infracost-base.json \
--format=json \
--out-file=infracost.json

# Post a comment on the merge request, updating it on each run
- |
infracost comment gitlab --path=infracost.json \
--repo=$CI_PROJECT_PATH \
--merge-request=$CI_MERGE_REQUEST_IID \
--gitlab-server-url=$CI_SERVER_URL \
--gitlab-token=$GITLAB_TOKEN \
--behavior=update
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

# Run Infracost on default branch and update Infracost Cloud
infracost:default-branch-update:
stage: infracost:default-branch-update
image:
name: infracost/infracost:ci-0.10
entrypoint: ['']
script:
- |
infracost breakdown --path=${TF_ROOT} \
--format=json \
--out-file=infracost.json

infracost upload --path=infracost.json
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Set the MR status to Merged in Infracost Cloud
infracost:merge-request-status-update:
stage: infracost:merge-request-status-update
image:
name: infracost/infracost:ci-0.10
entrypoint: ['']
before_script:
- if [[ ${CI_COMMIT_MESSAGE} =~ ${PATTERN} ]];
then MR_ID=${BASH_REMATCH[1]};
else echo "Unable to extract Merge Request ID"; exit 1;
fi;
script:
- |
curl \
--request POST \
--header "Content-Type: application/json" \
--header "X-API-Key: ${INFRACOST_API_KEY}" \
--data "{ \"query\": \"mutation {updatePullRequestStatus( url: \\\"${CI_PROJECT_URL}/merge_requests/${MR_ID}\\\", status: MERGED )}\" }" \
"https://dashboard.api.infracost.io/graphql";
variables:
PATTERN: 'See merge request.+?!([0-9]+)'
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH && $CI_COMMIT_TITLE =~ /^Merge branch/
  1. Open a test merge request — see these steps for what to expect.

For more elaborate setups (multi-project configs, private modules, Slack notifications, plan-JSON workflows), see the examples in the source repo.

Troubleshooting

HTTP 403 when posting comments

Check that the correct $GITLAB_TOKEN is being used — GitLab's variable precedence rules sometimes select the wrong one. You can also verify the token has the right access:

export GITLAB_TOKEN=mytoken
export CI_SERVER_URL=https://gitlab.com
export CI_PROJECT_PATH=my-org%2Fmy-repo # URL-encoded
export CI_MERGE_REQUEST_IID=123

curl -i -X POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data-urlencode "body=testcomment" \
$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_PATH/merge_requests/$CI_MERGE_REQUEST_IID/notes

A 401 / 403 response means the token is the issue.

Windows: INFRACOST_API_KEY not set

When using a Windows GitLab Runner, the variable syntax differs by shell — $INFRACOST_API_KEY in Bash, $env:INFRACOST_API_KEY in PowerShell. See GitLab's variables docs.

Windows: ParserError on multiline scripts

PowerShell doesn't accept Bash-style line continuations. Convert multi-line CLI commands to one-liners, e.g.:

- |
infracost breakdown --path=/tmp/base/${TF_ROOT} --format=json --out-file=infracost-base.json