DevOps Peach

Advanced Terraform · Testing and CI/CD · Lesson 4 of 4

Policy as Code on Terraform Plans

Turn rules such as "no privileged containers" or "every resource has an owner tag" into code that checks the JSON plan in CI with OPA and conftest, or with Sentinel in HCP Terraform.

Practice interactively →

Review catches a lot, but people get tired. Policy as code checks every plan against rules automatically, before apply.

flowchart LR
  PL["terraform plan -out=tfplan"] --> JS["terraform show -json > plan.json"]
  JS --> CF["conftest test plan.json"]
  RG["policy/*.rego"] --> CF
  CF -->|"pass"| AP["Apply allowed"]
  CF -->|"deny"| BL["Pipeline fails with messages"]
  %% step PL: The pipeline creates a saved plan as usual.
  %% step JS: The JSON plan lists every resource change with before and after values.
  %% step RG: Rules are written in Rego and kept in Git next to the code.
  %% step CF: conftest evaluates the rules against plan.json.
  %% step BL: Any deny message fails the job and tells the author exactly what to fix.

What the JSON plan looks like

{
  "resource_changes": [
    {
      "address": "docker_container.debug",
      "type": "docker_container",
      "change": {
        "actions": ["create"],
        "after": { "name": "debug", "privileged": true, "labels": [] }
      }
    }
  ]
}

A Rego policy for conftest

# policy/containers.rego
package main

deny contains msg if {
  rc := input.resource_changes[_]
  rc.type == "docker_container"
  rc.change.after.privileged == true
  msg := sprintf("%s must not run privileged", [rc.address])
}

deny contains msg if {
  rc := input.resource_changes[_]
  rc.change.actions[_] == "delete"
  startswith(rc.address, "module.db")
  msg := sprintf("%s: deleting database resources needs a manual change request", [rc.address])
}
terraform show -json tfplan > plan.json
conftest test plan.json                # reads ./policy by default

deny rules fail the run; warn rules only print. Test your policies too: opa test policy/ with example inputs.

Other options

Tool Where it runs Language
OPA / conftest Any CI Rego
Sentinel HCP Terraform / Terraform Enterprise Sentinel
OPA in HCP Terraform Policy sets on workspaces Rego
Trivy / Checkov Any CI, on code or plan JSON Built-in checks plus custom ones
Cloud-native policies AWS SCPs, Azure Policy, GCP org policies Enforced by the cloud at API level

Plan-time policies give fast feedback; cloud-native policies are the last line of defence because they also stop changes made outside Terraform.

Good rules to start with

Traps: a policy that checks change.after misses values that are (known after apply), which appear under after_unknown; and a rule written for the old Rego syntax (deny[msg] { ... }) fails to parse in OPA 1.x, which requires contains and if.

Practice interactively →