What it is and why
terraform-compliance is an open-source utility written in Python. It takes a
Terraform plan in JSON and a set of .feature files written in Gherkin (the
same style as Cucumber or behave). It checks each step of a feature against
the plan.
Running it:
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
terraform-compliance --planfile plan.json --features ./policies
The idea is that rules are written in a language the security or compliance team can read, not just an engineer. They can add policies themselves without knowing HCL.
A minimal feature file
policies/s3-buckets.feature:
Feature: S3 buckets must be encrypted
Scenario: Encryption is required
Given I have aws_s3_bucket defined
Then it must contain server_side_encryption_configuration
Scenario: Versioning is required
Given I have aws_s3_bucket defined
Then it must contain versioning
And it must contain enabled
And its value must be true
This walks every aws_s3_bucket in the plan and checks that the rules hold.
If even one bucket lacks encryption, the exit code is 1 and CI fails.
Gherkin structure for terraform-compliance
The basic steps:
| Step | What it does |
|---|---|
Given I have <resource_type> defined | Filter: consider only resources of this type. |
When its <property> is "value" | Extra filter, only resources with this property. |
Then it must contain <key> | Assertion: the resource must have this field. |
Then it must not contain <key> | The opposite assertion. |
Then its value must be <X> | Value comparison. |
Then its value must match the "regex" | Regex match against the value. |
The extended steps:
| Step | What it does |
|---|---|
Given I have any resource defined | All resources. |
Given I have <module> action_name defined | Filter by module. |
Then it must have tags | A common tag check. |
Then it must contain tags ([k1,k2,k3]) | Specific keys in the tags. |
Tags in features
You can label a scenario and run a subset:
@encryption @critical
Scenario: Encryption is required
Given I have aws_s3_bucket defined
Then it must contain server_side_encryption_configuration
Run only the critical ones:
terraform-compliance -p plan.json -f ./policies --tags critical
CI integration
GitHub Actions:
jobs:
plan-and-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- run: terraform init
- run: terraform plan -out=plan.tfplan
- run: terraform show -json plan.tfplan > plan.json
- name: Install terraform-compliance
run: pip install terraform-compliance==1.3.50
- name: Validate
run: terraform-compliance -p plan.json -f ./policies/
It returns a non-zero exit on any failed scenario, so the PR turns red.
When terraform-compliance, when OPA
| Aspect | terraform-compliance | OPA + Rego (conftest) |
|---|---|---|
| Syntax | Gherkin, reads as English | Rego, reads awkwardly |
| Learning curve | Low for the rule author | Noticeable, a different language |
| Cross-resource checks | Weak, each rule is local | Strong, you can check relationships |
| External data (lookup) | No | Yes, through data |
| Maturity / ecosystem | Smaller, narrowly focused | Wider, OPA is a general policy engine |
| Speed on large plans | Noticeably slower | Faster |
| Vendor support | Open-source, has a maintainer | Open-source, CNCF, vendor support through Styra |
For a small team with simple policies ("every bucket has a CostCenter tag"), terraform-compliance is quicker to get running. For mature infrastructure with dozens of complex rules, or for cross-account and cross-region logic, use OPA. See tf-policy-as-code.
Example of a grown-up policy suite
policies/
├── tagging.feature
├── encryption.feature
├── naming.feature
├── public-access.feature
└── networking.feature
tagging.feature:
Feature: All resources must have mandatory tags
Scenario: CostCenter tag
Given I have any resource defined
When it contains tags
Then it must contain CostCenter
Scenario Outline: Allowed environments
Given I have any resource defined
When it contains tags
Then it must contain Environment
And its value must be one of <envs>
Examples:
| envs |
| dev,stage,prod |
Note the Scenario Outline: rules parameterized through a table.
Pitfalls
-
It only checks what is in the plan. If a resource already exists and the plan has no changes for it, terraform-compliance does not see it. This is not a tool for auditing current state. It is a gate on new changes.
-
It does not understand expressions.
tags = local.standard_tagsis already expanded into a map in the plan ({"CostCenter": "foo"}), which is fine. But if a value is(known after apply), terraform-compliance has no data to check and treats the scenario as "not applicable". That is a gap a rule can slip through. -
Competing markup. Some steps from older versions stop working in newer ones, and the documentation lags well behind the changelog. Before you adopt it, pin the version (
==1.3.50) and upgrade deliberately. -
CI integration needs a plan file. That means a pipeline of plan → show -json → terraform-compliance. If the plan is already an artifact passed between jobs (see tf-plan-apply-ci), it slots in easily. If the pipeline is linear and the plan lives only in memory, you have to rework it.
-
It does not replace static analysis of HCL.
terraform validate,tflint, andcheckovwork on the source. terraform-compliance works on the plan. These are different layers, and you want both.