Policy as Code & Compliance

Writing compliance rules that evaluate a plan before it applies, the two policy ecosystems, and turning drift detection and audit logging into continuous compliance rather than an annual scramble.

advanced 19 min lesson hands-on task included

Policy as Code writes compliance rules in code and evaluates them automatically — the same move Infrastructure as Code made for infrastructure. The rules run against a plan, so a violation is caught before anything is created rather than found in an audit months later.


Topic 1: The Three Kinds of Compliance

  • Security — network segmentation, encryption at rest and in transit, access control.
  • Regulatory — data-protection, healthcare, financial and payment-card regimes, each with its own evidentiary requirements.
  • Operational — internal policy on tagging, permitted resource types, permitted regions, cost ceilings.

All three reduce to the same mechanical question: does this plan satisfy a set of rules? That question is answerable automatically.


Topic 2: The Plan Is the Input

Policy engines do not read your HCL. They read the JSON plan, which is the fully-resolved set of changes after variables, locals, functions and modules have been evaluated.

terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json

This matters: a rule written against HCL can be defeated by indirection — a variable, a module, a computed value. A rule written against the plan sees the actual resource attributes about to be created, whatever route they took to get there.

{
  "resource_changes": [{
    "address": "aws_s3_bucket.artifacts",
    "type": "aws_s3_bucket",
    "change": {
      "actions": ["create"],
      "after": { "bucket": "org-artifacts", "acl": "private" }
    }
  }]
}

Topic 3: An Embedded Policy Framework

The commercial Terraform offerings ship a policy language that evaluates before any change applies:

import "tfplan/v2" as tfplan

main = rule {
  all tfplan.resource_changes as _, rc {
    rc.change.after.instance_type in ["t3.micro", "t3.small"]
  }
}

Policies are attached to workspaces and enforced at run time, with severity levels — advisory (warn), soft-mandatory (overridable by an authorised person), hard-mandatory (blocks unconditionally).


Topic 4: A General-Purpose Policy Engine

An open-source alternative, usable with many tools, typically run in CI:

package terraform.s3

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  resource.change.after.server_side_encryption_configuration == null
  msg := sprintf("Bucket %s must have encryption enabled", [resource.address])
}

deny[msg] {
  resource := input.resource_changes[_]
  required := {"Environment", "Owner", "ManagedBy"}
  provided := {k | resource.change.after.tags[k]}
  missing := required - provided
  count(missing) > 0
  msg := sprintf("%s missing required tags: %v", [resource.address, missing])
}
- name: Policy check
  run: |
    terraform show -json plan.tfplan > plan.json
    opa eval --data policy.rego --input plan.json --fail-defined "data.terraform.s3.deny[_]"

--fail-defined makes the step exit non-zero when any deny rule produces a message, which is what fails the build.


Topic 5: Policies Worth Writing First

Ranked by how much trouble they prevent per line:

PolicyPrevents
Encryption required on storage and databasesThe most common audit finding
No publicly-readable object storageThe most common breach headline
Required tags presentUntraceable cost and unattributable resources
Instance types from an approved listRunaway spend
Regions from an approved listData-residency violations
No security group open to 0.0.0.0/0 on admin portsThe most common intrusion path
prevent_destroy on stateful resource typesAccidental data loss

Start with tags and encryption. They are cheap to write, unambiguous to evaluate, and unlock the reporting everything else depends on.


Topic 6: The Compliance Challenges Policy Cannot Solve Alone

Configuration drift. Policy evaluates plans; it says nothing about changes made outside Terraform entirely. Pair it with scheduled terraform plan -refresh-only and alert on non-empty output.

Secret sprawl. Covered properly in the security lesson — secret managers, least-privilege IAM, encrypted state.

Regulatory reporting. Auditors want evidence over time, not a passing build today. That means immutable audit logs of every API call Terraform made:

resource "aws_cloudtrail" "audit" {
  name                          = "terraform-audit"
  s3_bucket_name                = aws_s3_bucket.audit_logs.id
  include_global_service_events = true
  is_multi_region_trail         = true
  enable_logging                = true
}

The equivalent elsewhere is a diagnostic setting capturing administrative actions with a retention policy — commonly 365 days to cover an annual audit cycle.

Combined with version-controlled configuration history and stored policy-evaluation results, that gives you the three things an auditor actually asks for: what the intended state was, what actually happened, and what checks it passed.


Topic 7: Continuous Compliance

The goal is checks in every pipeline run rather than an annual scramble:

  • Standardise policies across teams in one repository, versioned like any other code.
  • Review and update them as requirements evolve — a policy set nobody has touched in two years is describing a system that no longer exists.
  • Audit regularly so unauthorised change is detected early rather than discovered later.
  • Make the failure message actionable. “Policy violation” teaches nobody; “Bucket X must have encryption enabled — add a server_side_encryption_configuration block” fixes the problem without a support request.

Try it yourself: Write the required-tags policy and run it against a plan for your existing infrastructure. Almost every estate fails this on the first run, and the list of failures is a useful inventory of what nobody owns.

Common mistake: Writing policies against the HCL source instead of the JSON plan. Source-level rules miss anything expressed through a variable, a module or a function — which is most things in a mature configuration, and precisely where the violations hide.