A single unreviewed Terraform change can turn into a five-figure cloud bill before anyone notices. An engineer bumps an instance type for a load test, forgets to revert it, and the change merges straight into production. Cloud teams call this “cost drift,” and it is one of the main reasons FinOps exists as a discipline. Instead of catching the bill at the end of the month, you can catch the cost estimate before the pull request merges. This tutorial walks through building a working cost gate with Infracost CLI 2.16.3, then backs it up with OpenCost for Kubernetes spend and AWS Cost Explorer for account-level validation after deployment.
By the end, you will have a GitHub Actions pipeline that blocks a Terraform pull request when the estimated monthly cost increase crosses a threshold you set, plus a live Kubernetes cost dashboard and a scheduled AWS cost check. None of this requires an enterprise FinOps platform. It runs on open-source tooling and a handful of YAML files.
What you’ll build: a three-layer cost gate
The project has three layers, and each one catches a different kind of overspend. Infracost runs pre-deployment, scanning a Terraform plan before anything touches real infrastructure. OpenCost runs inside your Kubernetes cluster after deployment, attributing spend to namespaces and workloads so you know who is actually burning the budget. AWS Cost Explorer runs at the account level, giving you a ground-truth number against which you can check whether your estimates were close.
Running all three is not redundant. Infracost estimates based on list pricing before a resource exists, so it can be off by a meaningful margin once discounts, savings plans, or usage-based billing kick in. OpenCost measures real cluster spend but only sees what runs inside Kubernetes, missing RDS instances, S3, or managed services sitting outside the cluster. Cost Explorer sees everything AWS bills you for, but only after the fact, often with a one-day lag. Stacking the three gives you a pipeline that stops expensive mistakes early and a dashboard that confirms whether your estimates were right.
Prerequisites
Gather these before you start. Version mismatches are the single biggest source of confusing errors in this kind of pipeline, so pin what you can.
- Terraform or OpenTofu, version 1.7 or newer, with an existing project that provisions AWS resources
- Infracost CLI 2.16.3 or newer (verify with
infracost --version) - A GitHub repository with Actions enabled and permission to add repository secrets
- An Infracost API key, free to generate at infracost.io, required even for the open-source CLI’s cloud comment features
- kubectl 1.30+ and Helm 3.14+ pointed at a running Kubernetes cluster (EKS, GKE, or a local kind/minikube cluster for testing)
- AWS CLI 2.37.5 or newer, configured with an IAM identity that has
ce:GetCostAndUsagepermission - jq installed locally and in your CI runner, for parsing JSON cost output
- Basic familiarity with GitHub Actions YAML syntax
You do not need a paid Infracost Cloud plan to follow this tutorial. The free tier covers 1,000 CI/CD runs a month, which is enough for most small-to-mid-size teams. OpenCost is fully open source with no licensing cost of its own, though you still pay for the Prometheus and storage infrastructure that backs it.
Step 1: Install the Infracost CLI
On macOS or Linux with Homebrew available, installation is a single command:
brew install infracost
infracost --version
# Should print something like: Infracost 2.16.3
If Homebrew is not an option, for example on a bare CI runner image, pull the release archive directly and drop the binary on your PATH:
curl -fsSL https://github.com/infracost/infracost/releases/latest/download/infracost-linux-amd64.tar.gz -o infracost.tar.gz
tar xzf infracost.tar.gz -C /tmp
sudo mv /tmp/infracost-linux-amd64 /usr/local/bin/infracost
infracost --version
Run infracost auth login or set the INFRACOST_API_KEY environment variable with the key from your Infracost dashboard. Without a key, the CLI still estimates costs locally but cannot post comments back to pull requests, which is the entire point of the gate.
Step 2: Get a baseline cost estimate locally
Before wiring anything into CI, run Infracost against your existing Terraform project to confirm it parses correctly. From inside the directory holding your .tf files:
infracost breakdown --path .
Infracost reads your Terraform configuration, generates a plan internally (or uses an existing plan JSON file if you point --path at one), and prints a cost table to the terminal. For a typical three-tier app with an RDS instance, an ALB, and a handful of EC2 instances behind an autoscaling group, expect output broken down by resource, with a grand total at the bottom. If a module pulls in a provider Infracost doesn’t price yet, it shows those lines as unsupported rather than failing the whole run, which matters later when you set a strict CI gate.
Step 3: Generate a plan-based diff instead of a static estimate
A static breakdown tells you the total monthly cost of your infrastructure as it stands. What you actually want in a pull request is the delta: how much does this specific change add or remove. Infracost handles that by diffing against your Terraform plan output rather than the whole state:
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
infracost diff --path tfplan.json --format json --out-file infracost-diff.json
This two-step flow (generate a Terraform plan, hand the plan JSON to Infracost) is what makes the cost estimate reflect the actual change under review rather than your entire infrastructure footprint. It also means Infracost never needs cloud credentials of its own: it reads the plan, not your live account.
Step 4: Wire Infracost into GitHub Actions
Infracost’s own documentation points teams toward a guided setup command that configures CI integration for you: infracost ci setup --ci-pipeline, run locally from inside your Terraform project directory. It inspects your repository and scaffolds the workflow file, and the Infracost team recommends removing any older hand-written Infracost GitHub Action workflow before running it, since the two can conflict.
If you prefer to see exactly what’s happening and wire it by hand (useful if your repo already has a complex CI setup), the manual version looks like this:
name: infracost-cost-gate
on:
pull_request:
paths:
- '**.tf'
jobs:
infracost:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate Terraform plan
run: |
terraform init
terraform plan -out=tfplan.binary
terraform show -json tfplan.binary > tfplan.json
- name: Run Infracost diff
run: infracost breakdown --path tfplan.json --format json --out-file infracost.json
- name: Post PR comment
run: infracost comment github --path infracost.json --repo $GITHUB_REPOSITORY --pull-request ${{ github.event.pull_request.number }} --github-token ${{ secrets.GITHUB_TOKEN }} --behavior update
Add INFRACOST_API_KEY as a repository secret under Settings, Secrets and variables, Actions before this workflow runs, or every job fails at the setup step. The --behavior update flag keeps a single running comment on the pull request instead of spamming a new one on every push, which matters once reviewers start treating that comment as the source of truth for cost impact.
Step 5: Turn the comment into an actual gate, not just a notice
A comment on a pull request is useful but easy to ignore. The real value of a cost gate is blocking the merge when a change crosses a budget line. Infracost’s JSON output includes a running cost-delta field for the diff, and you can parse that with jq to decide whether the job should fail:
- name: Enforce cost threshold
run: |
DIFF_COST=$(jq -r '.diffTotalMonthlyCost // "0"' infracost.json)
THRESHOLD=500
echo "Estimated monthly cost change: \$$DIFF_COST"
if (( $(echo "$DIFF_COST > $THRESHOLD" | bc -l) )); then
echo "Cost increase of \$$DIFF_COST exceeds the \$$THRESHOLD/month threshold."
exit 1
fi
Set the threshold to whatever your team agrees on. A reasonable starting point for a mid-size engineering org is a flat dollar figure, somewhere between $200 and $1,000 a month depending on team size, with a second, higher threshold requiring a second approver rather than an outright block. Hardcoding a single number is simple to reason about and easy to explain in a PR review, which matters more than technical elegance when you’re asking an engineer to justify a spend increase to a manager.
Step 6: Deploy OpenCost to see real Kubernetes spend
Infracost estimates cost before deployment. OpenCost measures it after, inside your running cluster. Install it with Helm:
helm repo add opencost-charts https://opencost.github.io/opencost-helm-chart
helm repo update
helm install opencost opencost-charts/opencost \
--namespace opencost \
--create-namespace
OpenCost needs read access to node and pod metrics to attribute cost accurately, so if your cluster doesn’t already run Prometheus or a metrics-server, install one first. On EKS and GKE, OpenCost can also pull list pricing directly from each provider’s billing API once you grant the right IAM role, giving it real unit prices rather than generic defaults.
Step 7: Open the OpenCost UI and read the allocation view
Port-forward the service to reach the built-in dashboard:
kubectl port-forward --namespace opencost service/opencost 9090 9090
Then open http://localhost:9090 in a browser. The allocation view breaks spend down by namespace, controller, and label over a time window you choose, typically the last 24 hours, 7 days, or 30 days. This is the view you want open during a sprint retro when someone asks “why did our cluster bill jump.” OpenCost also ships a built-in MCP server, enabled by default and running on port 8081, which lets AI coding agents or chat tools query cost data directly instead of a human reading the dashboard.
Step 8: Set a Kubernetes namespace budget alert
OpenCost exposes a Prometheus-compatible metrics endpoint, which means you can alert on it the same way you’d alert on any other cluster metric. A simple PrometheusRule watching for a namespace crossing a daily spend threshold looks like this:
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: opencost-budget-alerts
namespace: opencost
spec:
groups:
- name: cost-budgets
rules:
- alert: NamespaceCostBudgetExceeded
expr: sum(node_total_hourly_cost) by (namespace) * 24 > 50
for: 1h
labels:
severity: warning
annotations:
summary: "Namespace {{ $labels.namespace }} is projected to exceed its daily cost budget"
Route that alert to the same Slack channel or on-call tool you already use for infrastructure alerts. Treat a budget alert with the same seriousness as a latency alert, because unlike most infra incidents, a cost overrun doesn’t page anyone on its own.
Step 9: Validate estimates against AWS Cost Explorer
Infracost and OpenCost both estimate. AWS Cost Explorer tells you what you actually paid. Pull a service-level breakdown for the last month with the AWS CLI:
aws ce get-cost-and-usage \
--time-period Start=2026-09-01,End=2026-10-01 \
--granularity MONTHLY \
--metrics "BlendedCost" "UnblendedCost" "UsageQuantity" \
--group-by Type=DIMENSION,Key=SERVICE Type=TAG,Key=Environment
The Cost Explorer console itself is free to use, but programmatic API requests like this one are billed at $0.01 per paginated request, and enabling hourly granularity adds further cost on top of that, so avoid scheduling this query to run every few minutes. Cost Explorer data refreshes at least once every 24 hours, which is why it’s the validation layer rather than the real-time one.
Step 10: Automate the Cost Explorer check on a schedule
Wrap the Cost Explorer call in a scheduled GitHub Actions workflow so it runs weekly without anyone remembering to kick it off manually:
name: weekly-cost-check
on:
schedule:
- cron: '0 9 * * 1'
jobs:
cost-report:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-east-1
- name: Pull weekly cost summary
run: |
START=$(date -d '7 days ago' +%Y-%m-%d)
END=$(date +%Y-%m-%d)
aws ce get-cost-and-usage \
--time-period Start=$START,End=$END \
--granularity DAILY \
--metrics "UnblendedCost" \
--group-by Type=DIMENSION,Key=SERVICE > weekly-cost.json
Pipe weekly-cost.json into a Slack webhook or email step and your team gets a standing Monday-morning cost summary without logging into the AWS console. This closes the loop: Infracost stops expensive changes before merge, OpenCost shows where Kubernetes spend actually went, and this weekly job confirms the account-level total matches expectations.
Step 11: Tune the gate so it doesn’t train engineers to ignore it
The most common failure mode for a cost gate isn’t a bug, it’s a threshold set so low that it fails on routine changes, so engineers learn to merge past a red check without reading it. Review your threshold after the first two to four weeks of real data. If more than roughly one in five pull requests trips the gate, the number is probably too conservative. If nothing ever trips it, it’s too loose to matter. Infracost Cloud, the hosted dashboard layered on top of the open-source CLI, keeps a running history of past diffs per repository, which makes this kind of threshold tuning a lot less like guesswork.
Step 12: Add a break-glass override for legitimate spend increases
Sometimes a cost increase is intentional, like provisioning a larger instance ahead of a known traffic spike. Build an escape hatch rather than forcing people to disable the whole workflow. A label-based override works well in GitHub Actions:
- name: Check for override label
id: override
run: |
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'cost-approved') }}" == "true" ]]; then
echo "skip=true" >> $GITHUB_OUTPUT
fi
- name: Enforce cost threshold
if: steps.override.outputs.skip != 'true'
run: |
# threshold check from Step 5
echo "Running cost gate"
Require a second reviewer, typically a team lead or whoever owns the cloud budget, to apply the cost-approved label before it takes effect. That keeps the override auditable in the pull request history instead of buried in a Slack thread nobody can search six months later.
Aligning tags across Terraform, Kubernetes, and AWS billing
None of the three tools in this pipeline are useful if they can’t agree on who owns what. Infracost reports a cost delta for a Terraform resource, OpenCost attributes cluster spend to a Kubernetes namespace, and AWS Cost Explorer groups billing by account-level cost allocation tags. If those three systems use different naming conventions for team or project ownership, you end up reconciling spreadsheets by hand instead of trusting any single dashboard. The fix is boring but effective: pick one tag key, something like cost-center or team, and apply it everywhere.
In Terraform, that means adding the tag to every resource block or, more realistically, defining it once in a default_tags block on the AWS provider so you don’t have to repeat it:
provider "aws" {
region = "us-east-1"
default_tags {
tags = {
team = "checkout"
cost-center = "eng-platform"
managed-by = "terraform"
}
}
}
In Kubernetes, the equivalent is a namespace or pod label using the exact same key, since OpenCost reads labels (not arbitrary annotations) when building its allocation view. On the AWS side, that tag also has to be explicitly activated as a cost allocation tag in the Billing console before Cost Explorer will group by it. Tags that exist on resources but were never activated in Billing simply don’t show up as a grouping option, a common and confusing gap for teams setting this up for the first time. Once all three layers share the same key, a single cost-center value lets you trace a dollar figure from an Infracost PR comment, through an OpenCost namespace view, all the way to a line item in the monthly AWS invoice.
Securing the pipeline’s credentials
This pipeline touches two sets of secrets: the Infracost API key and the AWS credentials used for the weekly Cost Explorer check. Neither needs broad permissions, and treating them as narrowly as possible limits the damage if a secret ever leaks through a misconfigured log line or a forked pull request workflow.
The Infracost API key only needs to post comments and fetch pricing data. It never touches your cloud account, so a leaked key is an annoyance (someone could burn your free CI run quota) rather than a security incident. The AWS credentials behind the weekly Cost Explorer job are a different story, since they’re real cloud credentials. Scope the IAM policy attached to that identity down to exactly the Cost Explorer read action it needs, and nothing else:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetCostForecast"
],
"Resource": "*"
}
]
}
Cost Explorer’s API doesn’t support resource-level scoping the way S3 or EC2 do, so "Resource": "*" is expected here rather than a sign of an overly broad policy. Where you have more control is the credential type: prefer short-lived credentials issued through GitHub’s OpenID Connect (OIDC) identity provider over long-lived IAM access keys stored as static secrets. An OIDC-based role trust policy means GitHub Actions requests temporary credentials scoped to that one workflow run, and there’s no static AWS_SECRET_ACCESS_KEY sitting in your repository settings for a compromised dependency or a leaked log to expose. If your organization already uses OIDC federation for other AWS-facing workflows, reuse the same IAM role pattern here rather than falling back to static keys out of convenience.
Tool comparison: where each layer fits
| Tool | When it runs | What it sees | Cost to run it |
|---|---|---|---|
| Infracost CLI | Before deployment, in CI | Terraform/OpenTofu plan diff | Free up to 1,000 CI runs/month; Starter plan $250/mo for more |
| OpenCost | Continuously, post-deployment | Live Kubernetes namespace/workload spend | Open source, free; you pay for Prometheus/storage |
| Kubecost | Continuously, post-deployment | Same as OpenCost plus managed UI, alerts, multi-cluster rollups | Free tier plus paid tiers; 30-day trial on paid plans |
| AWS Cost Explorer | Daily batch, after billing settles | Full AWS account spend, all services | Console free; API calls $0.01 per paginated request |
Sample output: what a blocked pull request looks like
Here’s a representative Infracost pull request comment for a change that adds a larger RDS instance and a second NAT gateway:
Infracost estimate: monthly cost will increase by $612
+ aws_db_instance.primary
~ instance_class: db.r6g.large -> db.r6g.2xlarge
+$398/mo
+ aws_nat_gateway.secondary
+$214/mo (plus data processing charges)
Total monthly cost change: +$612.00
Cost gate failed: $612.00 exceeds the $500.00/month threshold.
Add the "cost-approved" label to override.
And a weekly OpenCost allocation summary for comparison, showing where cluster spend actually landed over a 7-day window:
| Namespace | CPU cost | Memory cost | Total (7 days) |
|---|---|---|---|
| checkout-service | $84.20 | $31.10 | $115.30 |
| recommendation-engine | $142.70 | $96.40 | $239.10 |
| batch-jobs | $58.90 | $22.00 | $80.90 |
| staging | $19.40 | $8.10 | $27.50 |
Common pitfalls
- Running Infracost against HCL directly instead of a plan file. Without a generated plan, Infracost has to guess at values that depend on provider defaults or data sources, which produces estimates that drift from reality. Always diff against a plan JSON for anything beyond a quick sanity check.
- Forgetting the repository secret. A missing
INFRACOST_API_KEYdoesn’t always fail loudly. Some setups silently skip the comment step and only the breakdown step errors, which looks like a flaky job rather than a config problem. - Setting one global threshold for every repository. A $500 threshold makes sense for a small service repo and is meaningless for a data platform repo that routinely provisions large clusters. Scope thresholds per repository or per environment path.
- Deploying OpenCost without a working metrics-server. OpenCost’s allocation numbers go flat or missing without real node and pod metrics behind it. Confirm
kubectl top nodesreturns data before troubleshooting OpenCost itself. - Treating Infracost’s estimate as the final bill. List pricing used in the estimate doesn’t account for savings plans, reserved instances, or committed-use discounts, so expect the real invoice to run lower for teams with existing commitments.
- Polling AWS Cost Explorer too frequently. At $0.01 per paginated API request, a tight polling loop adds up fast and the data won’t have changed anyway, since Cost Explorer refreshes roughly once a day.
- Not rotating the Infracost comment instead of appending new ones. Without
--behavior update, every push adds a fresh comment, burying the current estimate under old ones and making the thread unreadable within a few commits.
Troubleshooting
- “Infracost could not detect any resources” error: Confirm you’re running the command from the directory containing your root Terraform module, and that
terraform inithas already been run so provider schemas exist locally. - GitHub Action fails at the setup step with an auth error: The repository secret name must match exactly what the workflow references. Check Settings, Secrets and variables, Actions for a typo like
INFRACOST_KEYinstead ofINFRACOST_API_KEY. - PR comment never appears despite a green job: Check that the workflow has
pull-requests: writepermission set, either in the job’spermissionsblock or at the repository’s Actions settings level. - Cost gate step always passes even on huge changes: Confirm the jq field name matches your Infracost output version. Older CLI versions used slightly different JSON key names for the diff total.
- OpenCost UI loads but every namespace shows $0.00: This almost always means Prometheus isn’t scraping node-exporter or kube-state-metrics. Verify targets are up in the Prometheus UI before assuming OpenCost itself is broken.
- Port-forward to OpenCost UI hangs or times out: Check that the opencost pod is actually Running with
kubectl get pods -n opencost. A pod stuck in CrashLoopBackOff from missing RBAC permissions is a common cause. - AWS CLI returns an AccessDeniedException on get-cost-and-usage: The IAM identity needs the
ce:GetCostAndUsageaction explicitly. It is not covered by broad read-only policies like ReadOnlyAccess in every account configuration. - Scheduled weekly workflow never triggers: GitHub disables scheduled workflows on repositories with no commit activity for 60 days. Push any commit to reactivate the schedule.
- Infracost estimate is dramatically higher than the eventual AWS bill: Check whether your account has Savings Plans or Reserved Instances applied. Infracost prices against on-demand list rates unless configured otherwise.
Advanced tips
Once the basic gate is running reliably, a few extensions make it considerably more useful. First, scope the cost check to only the Terraform directories that changed in a pull request using a path filter, rather than re-estimating the entire repository on every PR. This cuts CI minutes significantly on monorepos with dozens of independent Terraform roots.
Second, feed Infracost’s diff output into a longer-term tracking sheet or data warehouse table rather than only surfacing it in the PR comment. Six months of historical diffs lets you answer questions like “which team’s changes drive the most cost growth” without manually digging through closed pull requests.
Third, pair OpenCost’s namespace-level data with a review of Kubernetes resource requests and limits. A namespace with high OpenCost spend but low actual CPU utilization usually means over-provisioned requests rather than genuinely expensive workloads, and that’s a cheaper fix than renegotiating cloud pricing.
Finally, if your organization already uses Infracost Cloud’s FinOps policy checks, layer those alongside the threshold script from Step 5 rather than replacing it. Policy checks are good at catching patterns, like flagging an unencrypted storage resource, while a simple dollar threshold is easier for a non-engineer budget owner to reason about during a review.
It’s also worth building a small escape valve for noisy, low-value diffs before they ever reach a human reviewer. A pull request that only touches a variable default or renames a resource, with no change to the underlying infrastructure, can still trigger a full Infracost run even though the dollar delta is always zero. Rather than disabling the workflow for those cases, add a cheap pre-check that diffs the plan JSON for actual resource changes and skips straight to a passing status if none exist. That keeps the gate’s signal-to-noise ratio high, which matters more than people expect once a repository accumulates a long history of pull requests and reviewers start pattern-matching on which checks are worth reading closely.
Complete working project structure
Putting everything from this tutorial together, a repository implementing the full cost gate looks like this:
infra-cost-gate/
├── .github/
│ └── workflows/
│ ├── infracost-cost-gate.yml # Step 4-5: PR-triggered cost check
│ └── weekly-cost-check.yml # Step 10: scheduled AWS validation
├── terraform/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── k8s/
│ └── opencost-budget-alerts.yaml # Step 8: PrometheusRule
└── scripts/
└── parse-cost-diff.sh # Step 5: jq threshold logic, extracted for reuse
With that structure in place, a typical change flow runs like this: an engineer opens a pull request modifying terraform/main.tf, the infracost-cost-gate.yml workflow runs automatically, Infracost posts a comment with the dollar delta, and the job either passes or blocks the merge depending on the threshold in parse-cost-diff.sh. After merge and deployment, OpenCost picks up the new resource’s real cluster cost within a few minutes, and the following Monday’s scheduled workflow confirms the account-wide AWS total reflects the change.
Rolling this out across multiple teams and repositories
Everything above describes a single repository. Most organizations running Terraform have dozens of them, owned by different teams with different risk tolerances, and rolling out a hard-blocking cost gate to all of them on day one is a reliable way to generate a wave of complaints and a round of workflow files quietly disabled out of frustration. A staged rollout avoids that.
| Phase | Gate behavior | Typical duration | Goal |
|---|---|---|---|
| 1. Observe | Comment only, never blocks | 2-4 weeks | Collect real diff data, calibrate thresholds per repo |
| 2. Soft gate | Blocks, but override label always available | 4-6 weeks | Get teams used to seeing and responding to the check |
| 3. Hard gate | Blocks; override requires a second approver | Ongoing | Cost review becomes a normal part of the PR process |
| 4. Portfolio view | Aggregate diffs across all repos into one dashboard | Ongoing | Leadership sees total infra spend trend, not just per-PR deltas |
Phase 1 matters more than it looks. Rolling out a hard block before you know what a “normal” cost diff even looks like for a given repository means your first threshold number is a guess, and a bad guess at phase 3 erodes trust in the whole system faster than having no gate at all. Let each repository run in comment-only mode long enough to see a handful of real pull requests pass through, then set that repository’s threshold based on what you actually observed rather than a company-wide default copied from a blog post or, for that matter, from this one.
By phase 4, the goal shifts from blocking individual bad changes to giving whoever owns the cloud budget, often a platform lead or an engineering director, a single place to see aggregate cost trend across every repository running the gate. Infracost Cloud provides this natively if you’re using the hosted tier. Teams on the fully open-source CLI path can get a rough equivalent by having each repository’s workflow post its diff JSON to a shared storage bucket and building a lightweight dashboard on top, a reasonable weekend project once the per-repository gates are stable.
How this fits into a broader FinOps practice
A CI cost gate solves one specific problem: catching expensive changes before they ship. It is not a substitute for a FinOps Foundation-style practice that also covers rightsizing, commitment planning, and cross-team budget ownership. Think of the pipeline in this tutorial as the engineering half of FinOps, the part that plugs into existing developer workflows without requiring a dedicated FinOps analyst to review every pull request by hand. The analyst-facing half, covering savings plan purchases and long-term forecasting, still benefits from the data this pipeline generates, since Infracost diffs and OpenCost allocation reports both export to CSV or JSON for exactly that kind of downstream analysis.
Teams that skip the pre-deployment layer and rely only on monthly Cost Explorer reviews tend to discover overspend weeks after it started, by which point the responsible engineer has often moved on to other projects and lost the context needed to explain or fix it quickly. Moving the check to pull-request time, while the context is still fresh, is the entire value proposition of this setup.
Frequently asked questions
Does Infracost need access to my AWS account?
No. Infracost reads a Terraform plan file and prices resources against public list pricing. It never needs cloud credentials to generate an estimate, which is part of why it’s safe to run in CI without granting the runner broad AWS permissions.
Is Infracost free to use?
The CLI is open source and free. Infracost Cloud, the hosted dashboard with PR history and FinOps policy checks, includes 1,000 free CI/CD runs a month, with a Starter plan at $250 a month for teams that need more.
What’s the difference between OpenCost and Kubecost?
OpenCost is the open-source cost allocation engine. Kubecost is built on the same underlying model but adds a managed UI, alerting, and multi-cluster rollups on top, available through free and paid tiers with a 30-day trial on the paid plans. Many teams start with OpenCost and move to Kubecost only once they need multi-cluster reporting.
Will the cost gate slow down every pull request?
Scope the workflow trigger to paths touching .tf files only, as shown in Step 4. For a typical Terraform root module, the whole Infracost job, including plan generation, usually finishes in under a minute, so it shouldn’t noticeably slow down review cycles.
Can I use this with OpenTofu instead of Terraform?
Yes. Infracost reads the plan JSON format, which both Terraform and OpenTofu produce in a compatible structure, so swapping terraform plan for tofu plan in the workflow steps works without further changes.
How accurate is Infracost’s estimate compared to the real AWS bill?
It depends heavily on whether your account has negotiated discounts, savings plans, or reserved capacity, since Infracost prices against on-demand list rates by default. For accounts running mostly on-demand pricing, estimates tend to track closely. For accounts with heavy commitment discounts, expect the real bill to come in lower than the CI estimate.
Do I need Kubernetes to use this pipeline at all?
No. The Infracost cost gate in Steps 1 through 5 works for any Terraform-managed infrastructure, with or without Kubernetes. OpenCost is specifically for teams running workloads on Kubernetes and can be skipped entirely if your infrastructure doesn’t include a cluster.
What happens if a legitimate deploy needs to bypass the gate in an emergency?
Use the label-based override from Step 12, or temporarily grant a maintainer the ability to merge with a failing required check through repository branch protection settings. Avoid disabling the workflow outright, since that removes visibility for every other pull request merged while it’s off.




