Posted on Aug 16, 2026 · Updated Aug 16, 2026 · 11 min read

Terraform Cost Estimation: Catch Spend in the PR

The most expensive Terraform changes are the ones nobody looks at twice: an oversized RDS instance, an extra NAT Gateway per AZ, a GPU node pool left at its default count. By the time any of that shows up on a bill, the PR that introduced it merged weeks ago. Terraform cost estimation moves that check to where the change actually happens — the pull request — so a reviewer sees the dollar impact next to the diff, before terraform apply ever runs. This is a hands-on, copy-pasteable walkthrough: install Infracost, run it against a real terraform plan, wire it into GitHub Actions or GitLab CI so every PR gets a cost comment, and add policy checks that stop the genuinely expensive changes before merge.

TL;DR — Terraform cost estimation in the PR

  • Infracost is the open-source CLI that reads a Terraform plan and prices every resource it touches, with no cloud credentials or state access required
  • Locally: terraform plan -out tfplan.binary → terraform show -json tfplan.binary > plan.json → infracost breakdown --path plan.json
  • infracost diff --path /code --compare-to infracost-base.json shows the dollar delta a change introduces, not just the total
  • The infracost/actions GitHub Action (or the equivalent GitLab CI job) posts and updates a single cost comment on every PR automatically
  • Cost policies — a dollar threshold, a percentage jump, or a banned resource type — turn the comment into an enforced gate that can fail the build
Terminal and pull request showing a Terraform cost estimate before merge

Why estimate cost in the PR, not after deploy

Cloud cost tools that read your bill are, by definition, retrospective — they tell you what a decision cost after it's been running for a billing cycle. By the time a monthly review flags an oversized instance or a duplicated NAT Gateway, fixing it means another PR, another review, another deploy. Catching the same issue in the original pull request costs a comment and a changed line.

This is the "shift-left" argument for FinOps: put the cost signal as close as possible to the decision that creates it. For infrastructure defined in Terraform, that decision point is the pull request, and the mechanism is Infracost — an open-source CLI that parses a Terraform plan, looks up current cloud pricing for every resource in it, and outputs a cost breakdown or a diff against what's currently deployed. No resources are created and no cloud credentials are needed for pricing lookups — it works entirely from the plan.

This tutorial is deliberately command-level: every snippet below is either the literal output of infracost --help on the current CLI (v0.10.x) or drawn directly from Infracost's and GitLab's published CI documentation. For the higher-level case on why to add guardrails and how to roll them out to a team without causing friction, see our companion post, Catch Cloud Costs Before You Deploy: CI/CD Cost Guardrails. This one is the step-by-step build.

Step 1: Install Infracost

Infracost ships prebuilt binaries and package-manager installs for macOS, Linux, and Windows. The install script is the fastest path on Linux/macOS/WSL:

macOS / Linux / WSL
curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh

macOS users with Homebrew can use the tap instead:

macOS (Homebrew)
brew install infracost

Windows users can install with Chocolatey, or download the release binary directly from the Infracost GitHub releases page. Verify the install with:

Verify
infracost --version

Infracost is free to use for the CLI and public pricing data. Registering for an API key (needed for pricing lookups and, later, for the GitHub/GitLab integrations) is a single command that opens a browser to sign up:

Get a free API key
infracost auth login
# or set an existing key directly:
infracost configure set api_key MY_API_KEY

Once installed, running infracost --help lists the available commands. The three that matter for this tutorial are breakdown, diff, and comment.

Step 2: Run a breakdown on a Terraform plan

infracost breakdown is the base command: point it at a Terraform directory or an existing plan, and it returns a full cost breakdown of every priced resource. Infracost supports two input modes. The simplest is pointing it straight at your Terraform directory, which runs terraform plan internally:

From a Terraform directory
infracost breakdown --path /code --terraform-var-file my.tfvars

For CI pipelines, or anywhere you don't want Infracost invoking Terraform itself (HCP Terraform/Terraform Enterprise runs, Bitbucket Pipelines, or any setup where the plan is generated by a separate step), feed it a plan JSON instead — this is the pattern used throughout the rest of this tutorial:

From a terraform plan JSON
terraform plan -out tfplan.binary
terraform show -json tfplan.binary > plan.json
infracost breakdown --path plan.json

Useful flags on breakdown: --format json|table|html controls output shape (table is the human-readable default), --out-file saves the result to disk instead of stdout, and --usage-file lets you supply expected usage (requests/month, data transfer GB, etc.) for resources Infracost can't price from the plan alone — Lambda invocations or S3 request counts, for example.

Step 3: Run a diff to see the cost delta

A total cost breakdown is useful, but the number that actually matters in a PR is the change: what does this specific commit add or remove from the monthly bill? That's what infracost diff is for — it compares the plan for your proposed change against a baseline and reports the delta.

The most direct pattern, from a Terraform plan JSON (works even when the plan already reflects the diff, e.g. a single PR's changes):

Diff from a plan JSON
terraform plan -out tfplan.binary
terraform show -json tfplan.binary > plan.json
infracost diff --path plan.json

To diff two states explicitly — say, the base branch versus the head branch of a PR — generate a baseline breakdown first, then diff the new plan against it with --compare-to:

Diff against a saved baseline
# On the base branch
infracost breakdown --path /code --format json --out-file infracost-base.json

# On the PR branch, after making Terraform changes
infracost diff --path /code --compare-to infracost-base.json

This is exactly the pattern the CI integrations below automate: check out the base branch, run a breakdown, check out the head branch, run a diff against that baseline JSON, and post the result as a comment.

Step 4: Post a cost comment in GitHub Actions

Running Infracost locally is useful for spot-checking a change before you push. The real value of shift-left FinOps comes from automating it so every PR gets a cost comment with no one having to remember to run the CLI. Infracost publishes an official GitHub Action, infracost/actions, that installs the CLI in a workflow step so infracost commands run natively alongside your normal Terraform steps.

A minimal workflow that installs the CLI:

.github/workflows/infracost.yml
name: infracost-comment
on:
  pull_request:
    types: [opened, synchronize, reopened]

permissions:
  contents: read
  pull-requests: write

jobs:
  infracost:
    runs-on: ubuntu-latest
    steps:
      - name: Setup Infracost
        uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}

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

      - name: Generate Infracost cost baseline
        run: |
          infracost breakdown --path=base \
                               --format=json \
                               --out-file=/tmp/infracost-base.json

      - name: Checkout PR branch
        uses: actions/checkout@v4

      - name: Generate Infracost diff
        run: |
          infracost diff --path=. \
                          --format=json \
                          --compare-to=/tmp/infracost-base.json \
                          --out-file=/tmp/infracost.json

      - 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

Store INFRACOST_API_KEY as a repository secret (get one free with infracost auth login from Step 1). The --behavior=update flag means Infracost edits its existing comment on subsequent pushes to the same PR instead of spamming a new one every time — the other supported values are new, hide-and-new, and delete-and-new.

Infracost also ships newer, higher-level actions — infracost/actions/diff@v4 and infracost/actions/scan@v4 — that wrap the checkout/breakdown/diff/comment sequence above into two steps if you'd rather not assemble it manually. The manual version above is worth understanding first since it's the same three CLI commands you just ran locally in Steps 2 and 3, just scripted.

Step 4 (alt): Post a cost comment in GitLab CI

GitLab CI uses the same three commands — breakdown, diff, comment gitlab — inside a merge-request pipeline job. Set INFRACOST_API_KEY and a GITLAB_TOKEN (a project or group access token with api scope) as masked CI/CD variables, then add a job that runs only on merge request pipelines:

.gitlab-ci.yml
stages:
  - infracost

infracost_mr_comment:
  stage: infracost
  image:
    name: infracost/infracost:ci-latest
    entrypoint: [""]
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
  script:
    - infracost breakdown --path=/tmp/base/${TF_ROOT} \
                           --format=json \
                           --out-file=infracost-base.json
    - infracost diff --path=${TF_ROOT} \
                      --compare-to=infracost-base.json \
                      --format=json \
                      --out-file=infracost.json
    - 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

TF_ROOT is a CI/CD variable pointing at the relative path to your Terraform root module. The infracost/infracost:ci-latest image already has the CLI installed, which is why the job doesn't need a separate install step the way the GitHub Actions example does. Infracost's docs note the GitLab App integration as an alternative that's simpler to wire up than a hand-rolled job — worth checking if you'd rather not manage the token and job yourself.

Step 5: Add cost policies and guardrails

A PR comment informs a reviewer; it doesn't stop a merge on its own. Once the team is used to seeing cost diffs, the next step is turning specific ones into enforcement — what Infracost and the wider FinOps community call cost policies or guardrails. There are two practical ways to implement this depending on how far you want to take it:

CI-native gating. Because infracost diff --format json outputs a machine-readable cost delta, a simple follow-up step in the same job can parse that JSON and fail the build if the total monthly delta exceeds a threshold you define — no additional product needed, just a small script (jq, Python, or your language of choice) reading fields like diffTotalMonthlyCost from the diff output and exiting non-zero when it's over budget.

Infracost Cloud policies. Infracost's hosted dashboard product (Infracost Cloud) adds policy-as-code checks on top of the CLI output — rules like "fail if a change adds more than $X/month" or "flag any new resource missing a required tag" — that run centrally rather than being hand-scripted per repo, and surface as pass/fail checks alongside the PR comment. If your team is already using Infracost's GitHub/GitLab integration and API key, this is the path of least resistance for org-wide policy consistency; see the current policy documentation at infracost.io/docs for the exact configuration format, since policy syntax has changed across CLI versions and is worth confirming against the version you're running.

Guardrails worth setting, in rollout order

  • 1. Comment only — no enforcement, just visibility, for the first few weeks
  • 2. Soft threshold — a warning label or required approval above a dollar amount, not a hard block
  • 3. Hard threshold — fail the build above a calibrated dollar or percentage jump, with an override path
  • 4. Tag/resource policies — block resources missing required tags (owner, team, environment) — this one is usually safe as a hard block from day one

Whichever mechanism you pick, calibrate thresholds so routine changes pass freely. A gate that blocks every third PR gets routed around; a gate that only fires on the genuinely expensive 5% of changes gets respected.

Reading the PR comment

The posted comment shows, per project, the estimated monthly cost of the plan and — when run as a diff — the delta versus the baseline, broken down by resource. A reviewer scanning it should be able to answer three questions at a glance: what changed, how much it adds or removes per month, and which specific resource is driving the number. The resources that show up most often as the real cost driver are the same ones we flag in our cloud cost red flags post — oversized instances, redundant NAT Gateways, and databases provisioned above actual load.

If a resource in the plan isn't supported for pricing (a very new resource type, or one Infracost doesn't yet model), it's listed as skipped rather than silently omitted — pass --show-skipped to breakdown or diff to see that list explicitly, which matters when you're deciding whether to trust a "$0 change" result.

Common pitfalls

1

Diffing against the wrong baseline

If the base-branch breakdown is stale (generated once and reused across many PRs) the diff will drift from reality. Regenerate the baseline on every run, checked out fresh from the PR's actual base ref, as in the workflow above.

2

Usage-based resources showing $0

Lambda, S3 requests, and similar usage-billed resources can't be priced from a plan alone — Infracost needs an estimate of usage. Supply one with --usage-file rather than assuming a $0 line means no cost.

3

Thresholds set without a calibration period

Jumping straight to a hard-blocking dollar threshold before anyone has seen a comment-only rollout tends to generate override requests and resentment. Run comment-only first, as in the rollout order above.

4

Treating this as a replacement for billing review

Infracost prices the plan, not reality — actual spend also depends on usage patterns, discounts, and commitments that only show up on the real bill. Pre-deploy estimation and monthly bill review are complementary, not either/or.

Methodology & sources

Every CLI command and flag in this tutorial was verified directly against a locally installed Infracost CLI (v0.10.x) via its own --help output, and against Infracost's and GitLab's published integration documentation. It deliberately avoids inventing exact dollar thresholds, specific policy-file syntax, or CLI flags that couldn't be confirmed — flag names and available commands do change across CLI versions, so run infracost breakdown --help, infracost diff --help, and infracost comment github --help yourself before wiring these into a production pipeline, and confirm current behavior against the docs below.

Once a change is estimated and merged, tracking whether the actual bill matched the estimate is the next step — our cloud cost estimation guide covers that broader estimation workflow, and SpendArk's free cloud cost calculator is useful for modeling an architecture's cost before any Terraform exists at all.

Where to run the CI runner behind this pipeline

Infracost's CLI runs comfortably on a small, cheap CI runner — you don't need anything GPU- or memory-heavy just to price a Terraform plan. These providers offer flat, predictable pricing if you're self-hosting runners rather than paying per-minute for hosted CI.

  • Hetzner — inexpensive per-vCPU pricing for a self-hosted GitHub Actions or GitLab runner that just needs to run terraform plan and infracost.
  • DigitalOcean — simple flat-priced droplets if you want a managed, predictable self-hosted runner without operating bare metal.
  • Vultr — affordable VPS instances that work well as lightweight, always-on CI runners for a small team's Terraform pipeline.

Some provider links above are affiliate links — we may earn a commission at no extra cost to you. It never affects our pricing data.

Frequently asked questions

What is terraform cost estimation?

Terraform cost estimation is the practice of pricing a Terraform plan's resources before you apply it, so you know what a change will cost ahead of deployment rather than discovering it on next month's bill. Infracost is the open-source CLI that does this: it parses a terraform plan output and maps each resource to current cloud pricing.

Do I need cloud credentials to run Infracost?

No. Infracost prices resources from the Terraform plan itself using its own pricing data — it doesn't need AWS, Azure, or GCP credentials to generate a cost estimate. You only need the Terraform plan or plan JSON as input.

What's the difference between infracost breakdown and infracost diff?

infracost breakdown shows the total estimated monthly cost of everything in a plan. infracost diff compares that plan against a baseline (a saved breakdown JSON, via --compare-to) and shows only what changed — the dollar delta a specific commit or PR introduces. Diff is what you typically want in a PR comment; breakdown is useful for a one-time full picture.

Does this replace monthly cloud cost reviews?

No. Infracost estimates cost from a plan before deployment; it can't account for actual usage patterns, applied discounts, or commitment pricing that only show up on a real bill. Pre-deploy estimation catches expensive changes early; a monthly billing review catches drift that accumulates after deployment. Run both.

Can Infracost fail my CI build automatically?

Yes, in two ways: you can parse the JSON output of infracost diff --format json in a follow-up script and exit non-zero above a threshold you define, or use Infracost Cloud's hosted policy checks if you want centrally managed rules across repositories. Neither is configured by default — the base integration only posts a comment.

Estimate your cloud costs — for free

Compare AWS, Azure, and GCP pricing side by side with our free calculator, and dig into the guides to learn how to cut cloud waste. No sign-up required.