Cloud infrastructure bills are notoriously hard to predict. A Terraform change that looks like a small configuration tweak — upgrading an RDS instance class, enabling Multi-AZ, adding a NAT gateway — can add hundreds of dollars per month. Without tooling, engineers discover the cost impact after the change is deployed and the first bill arrives.
Infracost bridges the gap between infrastructure code and cost. It reads your Terraform (or Terragrunt) and queries cloud provider pricing APIs to generate a cost breakdown before anything is deployed. This guide covers how Infracost works, what it can and can't estimate, how to run it, and how to wire it into a CI/CD pipeline so cost changes appear on every pull request.
How Infracost works
Infracost has three main phases:
1. Parse the Terraform project. Infracost runs terraform init and terraform plan internally, generating a plan JSON file. It reads resource types, instance types, storage sizes, replication settings, and other cost-relevant attributes from the plan output.
2. Look up pricing. Infracost maintains a pricing database synced from AWS, GCP, and Azure price lists. It queries this database to find the on-demand price for each resource configuration. For usage-based resources (data transfer, Lambda invocations, DynamoDB read/write units), it applies usage estimates from a infracost-usage.yml file or uses zero as a baseline.
3. Generate the output. The result is a breakdown of monthly costs per resource, organized by module and resource type, plus a diff view when comparing two Terraform states.
$ infracost breakdown --path .
Name Monthly Qty Unit Monthly Cost
aws_instance.web
Instance usage (Linux/UNIX, on-demand) 730 hours $29.20
root_block_device
Storage (general purpose SSD, gp3) 50 GB $4.00
aws_db_instance.main
Database instance (db.t3.medium, Multi-AZ) 730 hours $118.26
Storage (general purpose SSD, gp2) 100 GB $23.00
aws_nat_gateway.main
NAT gateway 730 hours $32.85
Data processed 0 GB $0.00
OVERALL TOTAL $207.31
What Infracost can estimate
Infracost covers most compute, storage, and database resources where pricing is instance-type or size based:
- EC2 instances — all instance families, Linux/Windows pricing, on-demand rates
- RDS instances — all DB engines and instance classes, Multi-AZ, storage, IOPS
- EKS — cluster cost, node group EC2 instances, Fargate task pricing
- S3 — standard storage pricing (per GB), request costs with usage estimates
- CloudFront — data transfer with usage estimates
- ElastiCache — Redis/Memcached node pricing
- OpenSearch — instance pricing, storage
- NAT Gateway — hourly cost, data processing with usage estimates
- Load balancers — ALB/NLB hourly cost, LCU usage
- Lambda — requires usage estimates for invocation count and duration
- DynamoDB — on-demand vs provisioned capacity, requires usage estimates for on-demand
What Infracost cannot estimate
Some costs are unknowable from Terraform configuration alone because they depend entirely on runtime behavior:
- Data transfer between regions or to the internet (volume varies with traffic)
- CloudWatch log storage and ingestion (volume depends on application log verbosity)
- API Gateway invocations (depends on request volume)
- Spot instance interruption rates
- Reserved instance or Savings Plans discounts (Infracost uses on-demand rates)
Infracost marks these as $0.00 with a note that usage estimates are needed, which is accurate — zero is the floor, not the expected cost.
Usage estimates
For usage-based resources, Infracost reads a infracost-usage.yml file where you specify expected usage:
# infracost-usage.yml
version: 0.1
resource_usage:
aws_lambda_function.api:
monthly_requests: 5000000 # 5M invocations/month
request_duration_ms: 200 # average 200ms per invocation
memory_mb: 512
aws_cloudfront_distribution.cdn:
monthly_data_transfer_to_internet_gb: 500
aws_nat_gateway.main:
monthly_data_processed_gb: 100
aws_dynamodb_table.sessions:
monthly_read_request_units: 1000000
monthly_write_request_units: 200000
infracost breakdown --path . --usage-file infracost-usage.yml
Usage estimates are approximate by definition, but even rough estimates make cost modeling far more useful than showing $0.00 for all usage-based resources. Teams often store a shared infracost-usage.yml alongside production usage metrics, updated quarterly.
Running Infracost
Install
# macOS brew install infracost # Linux curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh # Register for a free API key (rate limiting, no data stored) infracost auth login
Basic cost breakdown
# Show costs for current Terraform directory infracost breakdown --path . # Output JSON for programmatic use infracost breakdown --path . --format json --out-file infracost.json # Show a diff between two Terraform states infracost diff --path . --compare-to infracost-base.json
Cost diff between branches
The diff workflow is the most useful for pull request review: compare the base branch cost against the PR branch cost and surface only the delta.
# On the base branch, generate baseline git checkout main infracost breakdown --path . --format json --out-file infracost-base.json # On the PR branch, generate diff git checkout feature/upgrade-rds infracost diff --path . --compare-to infracost-base.json
Project: my-infrastructure
+ aws_db_instance.main
+ Database instance class changed: db.t3.medium to db.r6g.large
+$168.44 (+142%)
~ aws_instance.web (no change)
~ aws_nat_gateway.main (no change)
Monthly cost change: +$168.44 (from $207.31 to $375.75)
CI/CD integration
Infracost's most valuable feature is the PR comment: on every pull request that changes Terraform, Infracost posts a cost diff directly in the PR. Engineers see the impact before merging.
# .github/workflows/infracost.yml
name: Infracost Cost Estimate
on: [pull_request]
jobs:
infracost:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/checkout@v4
- name: Setup Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate cost estimate for base branch
run: |
git fetch origin ${{ github.base_ref }}
git checkout origin/${{ github.base_ref }}
infracost breakdown --path . \
--format json \
--out-file /tmp/infracost-base.json
- name: Generate cost diff for PR branch
run: |
git checkout ${{ github.sha }}
infracost diff --path . \
--format json \
--compare-to /tmp/infracost-base.json \
--out-file /tmp/infracost-diff.json
- name: Post PR comment
run: |
infracost comment github \
--path /tmp/infracost-diff.json \
--repo $GITHUB_REPOSITORY \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--pull-request ${{ github.event.pull_request.number }} \
--behavior update
The --behavior update flag updates the existing Infracost comment rather than creating a new one on each commit — keeps the PR clean.
Failing CI on large cost increases
You can configure Infracost to fail CI when a PR increases costs beyond a threshold. This works well as a guardrail for staging environments or cost-sensitive teams:
infracost comment github \
--path /tmp/infracost-diff.json \
--github-token ${{ secrets.GITHUB_TOKEN }} \
--pull-request ${{ github.event.pull_request.number }} \
--behavior update \
--policy-path infracost-policy.rego
# infracost-policy.rego — fail CI if monthly cost increase > $100
package infracost
deny[msg] {
maxDiff := 100.0
input.diffTotalMonthlyCost > maxDiff
msg := sprintf(
"Monthly cost increase ($%.2f) exceeds threshold ($%.2f)",
[input.diffTotalMonthlyCost, maxDiff]
)
}
Cost optimization patterns
Infracost makes cost tradeoffs explicit, which is most useful when evaluating architectural decisions. Common patterns where it helps:
Right-sizing instances
Oversized instances are one of the most common sources of waste. Infracost makes the cost difference between instance types visible before deployment:
db.r6g.xlarge(~$480/month) vsdb.r6g.large(~$240/month) — if workload fits, this is pure savings- Multi-AZ doubles RDS cost — necessary for production, wasteful for staging
- gp3 storage is consistently cheaper than gp2 for equivalent IOPS
NAT Gateway costs
NAT Gateway charges both per-hour ($0.045/hour = ~$33/month) and per-GB processed. For high-throughput workloads, data processing costs dominate. Common mitigation: VPC endpoints for S3 and DynamoDB bypass the NAT Gateway entirely, eliminating data processing charges for traffic to those services.
resource "aws_vpc_endpoint" "s3" {
vpc_id = aws_vpc.main.id
service_name = "com.amazonaws.${var.region}.s3"
route_table_ids = [aws_route_table.private.id]
}
Environment-specific sizing
Using production instance sizes in staging and dev environments is a common source of unnecessary spend. Infracost makes the cost difference concrete:
variable "instance_class" {
default = "db.t3.micro" # $13/month in dev
}
# Production tfvars: instance_class = "db.r6g.large" # $240/month
Storage class selection
S3 Intelligent-Tiering automatically moves objects to cheaper storage tiers based on access frequency. For large buckets with unpredictable access patterns, the monitoring fee ($0.0025/1000 objects) is typically far less than the savings from automatic tiering. Infracost can model this with usage estimates for access frequency.
Multi-project and monorepo setups
For repos with multiple Terraform projects, Infracost can aggregate costs across all of them:
# infracost.yml — project configuration
version: 0.1
projects:
- path: ./environments/production
name: Production
- path: ./environments/staging
name: Staging
- path: ./modules/shared-networking
name: Shared Networking
infracost breakdown --config-file infracost.yml --format table
The output shows each project's costs separately and a combined total — useful for understanding the cost split across environments or teams.
Reading Infracost output alongside your architecture
Cost numbers are more actionable when connected to the architecture they describe. When working with Terraform that has both a Checkov security scan and an Infracost estimate, InfraSketch lets you visualize the infrastructure from the same Terraform source: paste your configuration to generate an architecture diagram, then overlay the Infracost JSON output to see per-resource cost annotations directly on the diagram. This makes it easier to communicate cost breakdowns to stakeholders who aren't reading raw JSON.