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.
infracost:runs-on:ubuntu-latestpermissions:contents:readpull-requests:writesteps:-name:Setup Infracostuses:infracost/actions/setup@e9d6e6cd65e168e76b0de50ff9957d2fe8bb1832# v3.0.1with:api-key:${{ secrets.INFRACOST_API_KEY }}-name:Checkout base branchuses:actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683# v4.2.2with:ref:'${{github.event.pull_request.base.ref}}'-name:Generate Infracost cost estimate baselinerun:|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 branchuses:actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683# v4.2.2# Generate an Infracost diff and save it to a JSON file.-name:Generate Infracost diffrun:|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 commentrun:|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.
-name:Checkout base branchuses:actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683# v4.2.2with: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.
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.
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.
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:
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!