The Problem
You run terraform apply and get a $500/month surprise on next month’s AWS bill. Nobody reviewed the cost before deploying.
The Solution: Infracost
Infracost shows you the cost of your Terraform changes before you apply them:
$ infracost breakdown --path .
Name Monthly Cost
aws_instance.web
├─ Instance usage (t3.large, on-demand) $60.74
├─ root_block_device (gp3, 50 GB) $4.00
└─ CPU credits $0.00
aws_db_instance.main
├─ Database instance (db.r6g.large) $131.40
└─ Storage (gp3, 100 GB) $11.50
aws_nat_gateway.main
├─ NAT gateway $32.85
└─ Data processed (100 GB) $4.50
OVERALL TOTAL $244.99
Install Infracost
# macOS
brew install infracost
# Linux
curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh
# Docker
docker run -it --rm infracost/infracost breakdown --path /code
Get a free API key:
infracost auth login
# Opens browser → sign up → key saved to ~/.config/infracost/credentials.yml
Basic Usage
Cost Breakdown
# Full cost breakdown
infracost breakdown --path .
# JSON output
infracost breakdown --path . --format json
# Specific Terraform plan
terraform plan -out=plan.tfplan
infracost breakdown --path plan.tfplan
Cost Diff (Most Useful)
See cost impact of your changes:
# Compare current branch to main
infracost diff --path . --compare-to main
Monthly cost will increase by $156.00
+ aws_instance.api
+$60.74 (new resource)
~ aws_db_instance.main
+$95.26 (db.r6g.large → db.r6g.xlarge)
Monthly cost: $244.99 → $400.99
CI/CD Integration
GitLab CI
infracost:
stage: validate
image:
name: infracost/infracost:ci-0.10
entrypoint: [""]
script:
- infracost breakdown --path . --format json --out-file infracost.json
- infracost output --path infracost.json --format gitlab-comment --out-file comment.md
- |
# Post comment to MR
curl -X POST \
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"body\": \"$(cat comment.md | jq -Rs .)\"}" \
"${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/merge_requests/${CI_MERGE_REQUEST_IID}/notes"
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
GitHub Actions
- name: Infracost
uses: infracost/actions/setup@v3
with:
api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate cost estimate
run: |
infracost breakdown --path . --format json --out-file /tmp/infracost.json
infracost comment github --path /tmp/infracost.json \
--repo $GITHUB_REPOSITORY \
--pull-request ${{ github.event.pull_request.number }} \
--github-token ${{ github.token }}
This posts a cost table directly in your pull request.
Cost Policies
Block Expensive Changes
# infracost.yml
version: 0.1
projects:
- path: .
policies:
- name: "Monthly cost limit"
description: "Total monthly cost must be under $1000"
resource_type: "*"
condition:
monthly_cost: "<= 1000"
infracost breakdown --path . --format json | infracost policy check --policy-file infracost.yml
Budget Alerts by Tag
Tag resources with cost centers:
resource "aws_instance" "web" {
instance_type = "t3.large"
tags = {
CostCenter = "engineering"
Project = "api"
}
}
Then track costs by tag in your FinOps dashboard.
Cost Optimization Patterns
Right-Size Instances
# Check: are you over-provisioned?
variable "instance_type" {
type = string
default = "t3.micro" # Start small, scale up based on metrics
validation {
condition = contains(["t3.micro", "t3.small", "t3.medium", "t3.large"], var.instance_type)
error_message = "Use approved instance types only."
}
}
Use Spot/Preemptible for Non-Critical
resource "aws_spot_instance_request" "worker" {
ami = var.ami_id
instance_type = "c5.xlarge"
spot_price = "0.08" # 60-90% savings
wait_for_fulfillment = true
instance_interruption_behavior = "stop"
}
Schedule Non-Production
# Tag dev/staging for auto-shutdown
resource "aws_instance" "dev" {
tags = {
AutoStop = "true"
Schedule = "office-hours" # Used by AWS Instance Scheduler
}
}
Reserved Instances for Steady State
| Pricing | t3.large monthly | Savings |
|---|---|---|
| On-Demand | $60.74 | — |
| 1-Year Reserved (No Upfront) | $38.69 | 36% |
| 1-Year Reserved (All Upfront) | $35.04 | 42% |
| 3-Year Reserved (All Upfront) | $22.34 | 63% |
gp3 Over gp2
# gp3 is 20% cheaper with better baseline performance
root_block_device {
volume_type = "gp3" # Not "gp2"
volume_size = 50
}
Common Resource Costs (us-east-1)
| Resource | Typical Monthly Cost |
|---|---|
| t3.micro | $7.59 |
| t3.large | $60.74 |
| NAT Gateway | $32.85 + data |
| ALB | $16.43 + LCU |
| RDS db.t3.medium (Postgres) | $49.06 |
| RDS db.r6g.large (Postgres) | $131.40 |
| S3 (100 GB) | $2.30 |
| EBS gp3 (100 GB) | $8.00 |
| ElastiCache cache.t3.medium | $46.72 |
| Lambda (1M requests) | $0.20 |
Biggest surprises: NAT Gateways (data transfer costs), ALBs, cross-AZ traffic.
Hands-On Courses
Learn by doing with interactive courses on CopyPasteLearn:
Conclusion
Install Infracost, add it to your CI pipeline, and see cost diffs on every pull request. Right-size instances, use gp3 over gp2, schedule non-production shutdowns, and watch out for NAT Gateway data transfer costs. The goal: no surprise AWS bills after terraform apply.



