PHASE 03 // IMPLEMENT

recfo@implement:~/blogs/09-governance-part-one-iac
recfo@implement:~/blogs/finops/09-governance-part-one-iac $ open
finops

Implementing Governance Through Policy — Part One: IaC

2026-04-19 · Oliver Assad · 8 min read
  • Govern the deploy, not the bill. It is cheaper to prevent a bad resource from being created than to rightsize it after the fact.
  • Three ways to enforce governance in IaC, in order of sophistication:
    1. Terraform validation blocks — simple, built-in, enough for most teams.
    2. Policy-as-code (OPA or Sentinel) — for complex rules that span multiple resources.
    3. Enterprise governance tools (Stacklet, Adaptive6, etc.) — when you want coverage plus remediation without writing code.
  • Pick the lightest tier that actually covers your rules. Don’t over-engineer.

Why governance belongs at deploy-time

In article #08 — Fix the root cause, not the symptoms: Cloud Governs vs Cost Optimizations, I argued that cost optimisation without governance is a treadmill. You rightsize the instance today, someone provisions the next one oversized tomorrow, and the work starts again.

Governance breaks that cycle. Instead of optimising after the fact, you prevent non-compliant resources from being created in the first place. The earliest point where that enforcement is practical is the deploy pipeline — and the cleanest place to wire it in is Infrastructure as Code (IaC).

There is a second reason to deploy via IaC that often gets understated: manual deployments create drift. Someone clicks through a console, ticks a box no one else knows about, and three months later nobody can reproduce the configuration. That drift is the enemy of every FinOps capability: allocation, optimisation, forecasting, and governance all assume that what’s documented matches what’s running. IaC makes that true by construction.

Once you’re deploying via IaC, policy enforcement is a small step away.

Three tiers of enforcement

Governance-through-code has three sensible implementation tiers. The right tier depends on the complexity of your rules, not on how impressive you want to sound in an architecture review.

TierToolGood forEffort
1Terraform validation blocksPer-variable rules (allowed instance types, regions, sizes)Very low
2Policy-as-code (OPA, Sentinel)Cross-resource rules, complex conditions, organisation-wide policyMedium
3Enterprise governance platformFull coverage, remediation, reporting, non-IaC assetsLow effort, higher cost

Walk up the tiers only when the lower tier can’t express the rule you need.

Tier 1 — Terraform validation blocks

The simplest and most underused tool. Terraform has validation blocks built into variable definitions. You constrain what values are allowed; anything outside the list fails at terraform plan time.

Example — restricting which EC2 instance types can be deployed:

variable "instance_type" {
  type = string
  validation {
    condition     = contains(["t3.micro", "t3.small", "m5.large"], var.instance_type)
    error_message = "Instance type must be one of: t3.micro, t3.small, m5.large."
  }
}

For rules with more values, use a list variable so the allowed set is easy to read and extend:

variable "allowed_regions" {
  type    = list(string)
  default = ["us-east-1", "us-west-2", "eu-central-1"]
}

variable "region" {
  type = string
  validation {
    condition     = contains(var.allowed_regions, var.region)
    error_message = "Region must be one of the allowed regions."
  }
}

Better still — wrap the governed resource in a module so the validation logic lives in one place and every downstream consumer inherits it:

# modules/governed_instance/main.tf
variable "instance_type" {
  type = string
  validation {
    condition     = contains(["t3.micro", "t3.small", "m5.large"], var.instance_type)
    error_message = "Instance type must be one of: t3.micro, t3.small, m5.large."
  }
}

resource "aws_instance" "example" {
  instance_type = var.instance_type
  # ... other config ...
}

# In the consumer's Terraform:
module "my_governed_instance" {
  source        = "./modules/governed_instance"
  instance_type = "t3.micro"   # valid
  # instance_type = "m5.24xlarge"  # would fail validation
}

When tier 1 is enough: your rules are per-variable (“no instances bigger than X”, “only these regions”, “only these OSes”). This is usually 70–80% of what a starting governance practice actually needs.

When tier 1 is not enough: rules that span multiple resources (“if this bucket is public, then it must be tagged with data-classification = public”), rules that depend on metadata outside Terraform, or rules that need to be applied org-wide across many teams without trusting each team to include the validation.

Tier 2 — Policy-as-code (OPA and Sentinel)

For anything beyond per-variable rules, step up to policy-as-code. Two dominant options:

  • HashiCorp Sentinel — native to Terraform Cloud / Terraform Enterprise.
  • Open Policy Agent (OPA) — open source, cloud-agnostic, works with Terraform via conftest or similar.

Both let you write policies in a declarative language and evaluate them against the Terraform plan. If the plan violates a policy, the apply is blocked.

Example — a Sentinel rule requiring all EC2 instances to be t3.micro:

import "tfplan"

main = rule {
  all tfplan.resources as _, resources {
    all resources as _, r {
      r.type is "aws_instance" and
      r.attributes.instance_type else "t3.micro" is "t3.micro"
    }
  }
}

The value of policy-as-code is that it runs in the CI/CD pipeline, outside the team’s own Terraform code, so compliance doesn’t depend on individual developers remembering to include the right validation block. Pull request → plan → policy evaluation → merge or block. Governance becomes part of the pipeline, not a convention.

When tier 2 fits: you have a central governance function that needs to enforce rules across teams you don’t directly control, or your rules involve cross-resource conditions that validation blocks can’t express.

Tier 3 — Enterprise FinOps governance tools

At some scale, writing and maintaining your own policy library becomes its own project. At that point, paid tooling starts making sense.

A few names in the space: Stacklet, Adaptive6, and others. These platforms offer:

  • Pre-built policy libraries covering common FinOps and security rules
  • Policy enforcement across IaC and already-deployed resources
  • Remediation (not just detection — they can actually fix or flag things)
  • Reporting, dashboards, and audit trails

When tier 3 is worth the spend: you have a multi-team, multi-account, multi-cloud setup where the cost of maintaining your own policy code is higher than the subscription fee. Or you want to govern resources that weren’t deployed via IaC (lift-and-shift workloads, legacy assets, console-created resources).

I’ll avoid picking a winner between vendors — the right one depends on your stack and your budget. But the general point stands: at a certain size, building it yourself stops being cost-effective.

Matching the tier to the rule

Not every rule needs a heavyweight solution. A small team with a handful of clear constraints can go very far on tier 1 alone. A central platform team governing dozens of delivery teams will outgrow tier 1 within months.

A quick decision path:

 Can the rule be expressed as "this variable must be in this list"?
 ├── Yes → Tier 1 (validation blocks)
 └── No

     ├── Does the rule span multiple resources or depend on logic?
     │   ├── Yes → Tier 2 (OPA / Sentinel)
     │   └── No  → Tier 1 (probably a reusable module)

     └── Is the rule needed across teams you don't directly control,
         or across non-IaC-deployed resources?
         ├── Yes → Tier 3 (enterprise tool)
         └── No  → Tier 2

Don’t reach for tier 3 because it sounds impressive. Reach for it when tier 1 and tier 2 genuinely run out of road.

What if you don’t use IaC?

Fair question. If you don’t deploy via IaC, none of the mechanisms in this article apply at deploy-time.

In my view, that’s a problem worth fixing before you worry about governance — manual deployments are already a drag on allocation, optimisation, and forecasting, not just governance. But it’s a real situation, and there is a way to govern non-IaC deployments.

That’s the topic of Part Two, which covers governance via cloud IAM policies — enforcement from the account/project side rather than the deploy-pipeline side. Different tool, same goal: prevent the bad resource from existing in the first place.

Summary

  • Govern at deploy-time. It’s cheaper than optimising after the fact.
  • Deploy via IaC. That’s the entry condition for most governance-through-code approaches.
  • Start with Terraform validation blocks. Most small-to-mid teams don’t need more.
  • Graduate to OPA/Sentinel when rules outgrow per-variable validation.
  • Reach for enterprise tools only when the scale and complexity justify the spend.
  • Pick the lightest tier that covers your rules. Don’t over-engineer governance.

Thanks for reading. If this helped, share it. Questions or topic suggestions — send them through.