The Report Is Always Late
Every organization running workloads in the cloud has a cost dashboard. AWS Cost Explorer, a Grafana board wired to Cost and Usage Reports, a monthly spreadsheet someone generates from the billing console. These tools are genuinely useful — after the money is gone.
A report showing that a team deployed 20 oversized EC2 instances last month does not recover that spend. A dashboard flagging that dev environments ran over the weekend does not undo the weekend. Retroactive visibility is better than no visibility, but it is not cost control.
Cost control is a change in behavior *before* resources are provisioned. That requires moving the enforcement point upstream — into the infrastructure code itself.
What Guardrails in IaC Actually Look Like
The goal is to make the right thing easy and the wrong thing impossible, or at least loudly blocked.
Instance Class Constraints in Terraform Variables
The simplest guardrail is a Terraform variable with a validation block. Instead of letting an engineer pass any EC2 instance type, you validate against an approved list:
```hcl variable "instance_type" { description = "EC2 instance type" type = string
validation { c "t3.medium", "t3.large", "m6i.large", "m6i.xlarge", "c6i.large", "c6i.xlarge" ], var.instance_type) error_message = "Instance type must be from the approved list. See the FinOps runbook for exceptions." } } ```
An engineer cannot `terraform apply` a `p3.16xlarge` for a development workload. They can request an exception, which goes through a human review — but the default path keeps them in the guardrails.
Lifecycle Rules for Storage
S3 buckets without lifecycle rules accumulate data indefinitely. In a federal data environment, this is both a cost problem and sometimes a data retention compliance problem. A reusable Terraform module can encode the default lifecycle policy:
```hcl resource "aws_s3_bucket_lifecycle_configuration" "default" { bucket = aws_s3_bucket.this.id
rule { id = "transition-to-ia" status = "Enabled"
transition { days = 30 storage_class = "STANDARD_IA" }
transition { days = 90 storage_class = "GLACIER_IR" }
expiration { days = var.retention_days # default 365, required for compliance } } } ```
When every S3 bucket in your estate is provisioned through this module, every bucket has appropriate tiering by default. No manual lifecycle configuration. No forgotten dev buckets sitting in Standard storage for two years.
Terragrunt for Environment-Level Spend Ceilings
Terragrunt's layered configuration makes it practical to define environment-specific constraints at the environment level rather than requiring every module to re-implement them.
In a multi-environment setup (dev, staging, production), the `env.hcl` for dev can cap the maximum allowed instance sizes and require all resources to use Spot where applicable:
```hcl # environments/dev/env.hcl locals { envir max_instance_tier = "medium" require_spot = true auto_shutdown_enabled = true schedule_tag = "weekdays-only" } ```
Downstream modules read these locals and enforce them. A developer working in dev cannot accidentally request an instance configuration that only makes sense in production.
Budget Alarms as Code
AWS Budgets supports programmatic creation through Terraform. Rather than having someone log into the console and create budget alarms manually — which means they exist for some accounts and not others — define them in code and apply them consistently:
```hcl resource "aws_budgets_budget" "monthly_cost" { name = "${var.environment}-monthly-cost" budget_type = "COST" limit_amount = var.monthly_budget_usd limit_unit = "USD" time_unit = "MONTHLY"
notification { comparis threshold = 80 threshold_type = "PERCENTAGE" notificati subscriber_email_addresses = [var.finops_alert_email] }
notification { comparis threshold = 100 threshold_type = "PERCENTAGE" notificati subscriber_email_addresses = [var.finops_alert_email] } } ```
The 80% actual and 100% forecasted thresholds give the team time to respond before a budget is exceeded. And because it is in Terraform, every environment and every AWS account in your organization gets the same budget monitoring applied consistently.
The Right Workflow: FinOps in Code Review
The other piece that makes this work is treating cost implications as something reviewable in a pull request — not something discovered in a report.
When infrastructure changes go through a PR process, the cost impact can be part of the review. Tools like Infracost integrate with GitHub Actions and post estimated cost changes as a PR comment:
``` Monthly cost change: +$47.23/month ├── aws_instance.app_server: +$38.40 (t3.large → m6i.xlarge) └── aws_db_instance.postgres: +$8.83 (db.t3.medium → db.t3.large) ```
A reviewer can see that an infrastructure change adds $47/month before it is merged. If the cost is justified, it gets approved. If it is not — if someone is upsizing for no stated reason — it gets questioned. This is a much healthier conversation to have before the money is spent than after.
What This Requires Organizationally
Guardrails in code only work when the IaC is actually the path to production. If engineers have console access and can provision resources outside of Terraform, the guardrails are decorative.
The organizational pre-requisite is that infrastructure provisioning goes through code — and that code goes through a pipeline with the guardrails in it. Permission boundaries on AWS roles, enforced through Service Control Policies at the AWS Organizations level, can prevent out-of-band provisioning.
When both pieces are in place — guardrails in the code, and SCPs preventing console-only provisioning — the monthly cost report stops being a document of past failures and starts being a record of consistent, controlled behavior.
That is the difference between FinOps as reporting and FinOps as engineering discipline.