Cost Management in Terraform
One of Terraform’s advantages over manual provisioning is the ability to understand, control, and automate cost management. Every resource you define in HCL is a potential cost — and Terraform’s ecosystem gives you tools to estimate, tag, and govern that cost before a single dollar is spent.
The Cost Management Stack
Infracost → Cost estimation from terraform planResource Tagging → Attribute costs to teams/projectsAWS Budgets → Alert when spending exceeds thresholdsTerraform lifecycle → prevent_destroy on expensive resourcesModule abstractions → Enforce cost-efficient defaultsScheduled pipelines → Detect orphaned expensive resourcesInfracost: Cost Estimation Before Apply
Infracost integrates with Terraform to show cost estimates from plan output — before you apply anything.
Install and Run
# macOSbrew install infracostinfracost auth login
# Generate a plan fileterraform plan -out=tfplan
# Get cost breakdowninfracost breakdown --path tfplan
# Output:# Name Monthly Qty Unit Monthly Cost# aws_instance.app# ├─ Instance usage (Linux/UNIX, on-demand, t3.large) 730 hours $60.74# └─ root_block_device# └─ Storage (general purpose SSD, gp3) 20 GB $1.60# aws_db_instance.production# ├─ Database instance (db.r6g.large) 730 hours $175.20# └─ Storage (general purpose SSD, gp2) 100 GB $11.50## OVERALL TOTAL $249.04/moCI/CD Integration
- name: Infracost uses: infracost/actions/setup@v3 with: api-key: ${{ secrets.INFRACOST_API_KEY }}
- name: Generate Infracost diff run: | terraform plan -out=tfplan infracost diff \ --path tfplan \ --format json \ --out-file infracost-diff.json
- name: Post cost estimate to PR run: | infracost comment github \ --path infracost-diff.json \ --repo $GITHUB_REPOSITORY \ --pull-request ${{ github.event.pull_request.number }} \ --github-token ${{ secrets.GITHUB_TOKEN }}The PR comment shows: existing cost, new cost, and the delta — so the team sees “this change adds $45/month” before approving.
Resource Tagging for Cost Attribution
Tags are the foundation of cloud cost management. Without them, you can’t answer “which team is spending the most?” or “what does our auth service cost?”:
variable "cost_center" { description = "Accounting cost center code" type = string}
variable "team" { description = "Owning team name" type = string}
# locals.tflocals { required_tags = { Environment = var.environment Team = var.team CostCenter = var.cost_center Service = var.service_name ManagedBy = "terraform" }}
# Apply to all resourcesresource "aws_instance" "app" { # ... tags = merge(local.required_tags, { Name = "${var.service_name}-app" })}Enforce Tags via AWS Config or OPA
# Deny resources without required tags (using OPA / Conftest)package main
required_tags = {"Team", "CostCenter", "Environment"}
deny[msg] { resource := input.resource_changes[_] resource.type == "aws_instance" missing := required_tags - {key | resource.change.after.tags[key]} count(missing) > 0 msg := sprintf("aws_instance %v is missing required tags: %v", [resource.address, missing])}AWS Budgets with Terraform
Create budget alerts as code so you’re automatically notified of overspending:
resource "aws_budgets_budget" "monthly_cost" { name = "monthly-${var.environment}-budget" budget_type = "COST" limit_amount = "500" limit_unit = "USD" time_unit = "MONTHLY"
notification { comparison_operator = "GREATER_THAN" threshold = 80 # Alert at 80% of budget threshold_type = "PERCENTAGE" notification_type = "ACTUAL" subscriber_email_addresses = ["platform-team@company.com"] }
notification { comparison_operator = "GREATER_THAN" threshold = 100 threshold_type = "PERCENTAGE" notification_type = "FORECASTED" # Alert when forecast exceeds budget subscriber_email_addresses = ["platform-team@company.com", "cto@company.com"] }}Right-Sizing with Terraform Variables
Don’t hardcode expensive instance types — make them configurable and default to cost-effective options:
variable "app_instance_type" { description = "EC2 instance type for application servers" type = string default = "t3.micro" # Cheap default — override in production.tfvars
validation { condition = contains([ "t3.micro", "t3.small", "t3.medium", # Dev/staging "t3.large", "t3.xlarge", # Production "m6i.large", "m6i.xlarge" # High-memory workloads ], var.app_instance_type) error_message = "Must use an approved instance type." }}app_instance_type = "t3.micro"
# environments/production/terraform.tfvarsapp_instance_type = "t3.large"Automatically Clean Up Ephemeral Environments
# Create a TTL tag on dev resourcesresource "aws_instance" "dev_server" { tags = { Environment = "dev" TTL = formatdate("YYYY-MM-DD", timeadd(timestamp(), "168h")) # 7 days from now }}# Schedule: find and destroy expired dev environments#!/bin/bashtoday=$(date +%Y-%m-%d)aws ec2 describe-instances \ --filters "Name=tag:Environment,Values=dev" \ --query "Reservations[].Instances[?Tags[?Key=='TTL'&&Value<='${today}']].InstanceId" \ --output text | xargs -I {} terraform destroy -target=aws_instance.{} -auto-approveCost-Saving Terraform Patterns
# Use Spot instances for non-critical workloadsresource "aws_instance" "batch_worker" { instance_market_options { market_type = "spot" spot_options { max_price = "0.05" # Max hourly price } }}
# S3 intelligent tiering for unknown access patternsresource "aws_s3_bucket_intelligent_tiering_configuration" "data_lake" { bucket = aws_s3_bucket.data_lake.id name = "EntireBucket" tiering { access_tier = "DEEP_ARCHIVE_ACCESS" days = 180 }}
# RDS: start/stop on schedule for non-productionresource "aws_db_instance" "dev" { # ... # Use AWS EventBridge scheduler to stop at night and weekends # Saves ~65% on dev database costs}