Skip to content

Infracost

The Infracost job in the PR workflow checks how much the PR itself is going to cost. The workflow is just a job directly in the PR workflow. It also uses a Rego policy that is evaluated by the Open Policy Agent that checks if the PR will cost $10 or more, if it does it will fail the policy!

Note

Normally I would put this in it's own workflow and make it reusable, I designed it this way so you could see how flexible GitHub Actions can be! You don't have to use reusable workflows, but in general I recommend doing so as we can all contribute towards a common codebase and everyone then leverages the benefits.

Workflow

.github/workflows/pr.yml
infracost:
  runs-on: ubuntu-latest
  permissions:
    contents: read
    pull-requests: write
  steps:
    - name: Setup Infracost
      uses: infracost/actions/setup@e9d6e6cd65e168e76b0de50ff9957d2fe8bb1832 # v3.0.1
      with:
        api-key: ${{ secrets.INFRACOST_API_KEY }}

    - name: Checkout base branch
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
      with:
        ref: '${{ github.event.pull_request.base.ref }}'

    - name: Generate Infracost cost estimate baseline
      run: |
        infracost breakdown --path=. \
                            --format=json \
                            --out-file=/tmp/infracost-base.json \
                            --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
      working-directory: ${{ github.workspace }}/tofu

    # Checkout the current PR branch so we can create a diff.
    - name: Checkout PR branch
      uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

    # Generate an Infracost diff and save it to a JSON file.
    - name: Generate Infracost diff
      run: |
        infracost diff --path=. \
                        --format=json \
                        --compare-to=/tmp/infracost-base.json \
                        --out-file=/tmp/infracost.json \
                        --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
      working-directory: ${{ github.workspace }}/tofu

    - name: Post Infracost comment
      run: |
          infracost comment github --path=/tmp/infracost.json \
                                    --repo=$GITHUB_REPOSITORY \
                                    --github-token=${{ github.token }} \
                                    --pull-request=${{ github.event.pull_request.number }} \
                                    --behavior=update \
                                    --policy-path ./policies/cost.rego

Configuration

This job requires additional permissions, as it places a comment on the PR we need to give it write permissions on pull-requests and read permissions on contents so it can checkout the base branch AND the PR branch. This is important so it can make a cost comparison!

Additionally, this is the first and only workflow that is making use of GitHub Action Secrets! In order for us to authenticate to the Infracost API, we need an API key. We store this in a secret named INFRACOST_API_KEY which is then passed to the workflow at runtime and is masked in any logs.

Steps

This job has 6 steps:

  1. Setup Infracost
  2. Checkout base branch
  3. Generate Infracost cost estimate baseline
  4. Checkout PR branch
  5. Generate Infracost diff
  6. Post Infracost comment

Setup Infracost

.github/workflows/pr.yml
- name: Setup Infracost
  uses: infracost/actions/setup@e9d6e6cd65e168e76b0de50ff9957d2fe8bb1832 # v3.0.1
  with:
    api-key: ${{ secrets.INFRACOST_API_KEY }}

We start by configuring and installing Infracost using their official GitHub Action. We pass in our API key that authenticates us with the API.

Checkout base branch

.github/workflows/pr.yml
- name: Checkout base branch
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
  with:
    ref: '${{ github.event.pull_request.base.ref }}'

Infracost has to make a comparison with what is existing somewhere, as a result it checkouts the base (target) branch of the repository to know what is existing today.

Generate Infracost cost estimate baseline

.github/workflows/pr.yml
- name: Generate Infracost cost estimate baseline
  run: |
    infracost breakdown --path=. \
                        --format=json \
                        --out-file=/tmp/infracost-base.json \
                        --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
  working-directory: ${{ github.workspace }}/tofu

Once the base branch has been checked out, it then breaks it down in a format to use as a comparison later. As we pass in the working-directory directive, . translates to the tofu/ directory where the configuration is stored in the repo via the --path flag. As we specify the volume size and instance type as part our variables, we pass in the var file to infracost so it generates an accurate breakdown.

Checkout PR branch

.github/workflows/pr.yml
- name: Checkout PR branch
  uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

Simply put, it checkouts the PR branch to learn about what changes are being made.

Generate Infracost diff

.github/workflows/pr.yml
- name: Generate Infracost diff
run: |
  infracost diff --path=. \
                  --format=json \
                  --compare-to=/tmp/infracost-base.json \
                  --out-file=/tmp/infracost.json \
                  --terraform-var-file "${{ github.base_ref == 'main' && 'prod' || 'dev' }}.tfvars"
working-directory: ${{ github.workspace }}/tofu

Now it has context of the two branches, it used the breakdown it generated in the 3rd step and compares it against the PR to generate the cost of any changes made.

Post Infracost comment

.github/workflows/pr.yml
- name: Post Infracost comment
  run: |
      infracost comment github --path=/tmp/infracost.json \
                                --repo=$GITHUB_REPOSITORY \
                                --github-token=${{ github.token }} \
                                --pull-request=${{ github.event.pull_request.number }} \
                                --behavior=update \
                                --policy-path ./policies/cost.rego

Before it posts the comment on GitHub as part of the PR. It uses Open Policy Agent integration to ensure the cost complies with any OPA policies we tell Infracost to compare against. The policy that we are telling infracost to use is:

policies/cost.rego
package infracost # You must specify infracost as the Rego package name

# Each file can have a number of "deny" rules that must return an "out" object
# with keys "msg" & "failed". You can write as many "deny[out]" rule sets as you wish. 
# You can read more about rule definitions in Rego here: https://www.openpolicyagent.org/docs/latest/policy-language/#rules
deny[out] {
  # maxDiff defines the threshold that you require the cost estimate to be below
  maxDiff = 10.0

  # msg defines the output that will be shown in PR comments under the Policy Checks/Failures section
  msg := sprintf(
    "Total monthly cost diff must be less than $%.2f (actual diff is $%.2f)",
    [maxDiff, to_number(input.diffTotalMonthlyCost)],
  )

  # out defines the output for this policy. This output must be formatted with a `msg` and `failed` property.
  out := {
    # the msg you want to display in your PR comment, must be a string
    "msg": msg,
    # a boolean value that determines if this policy has failed.
    # In this case if the Infracost breakdown output diffTotalMonthlyCost is greater that $5000
    "failed": to_number(input.diffTotalMonthlyCost) >= maxDiff
  }
}

The output is then posted as a comment on the PR, if any changes are made, it then updates the existing comment!

You can see an example below:

Infracost PR Comment

Infracost PR Comment 2