Compliance as Code in Terraform
Compliance as Code means encoding security standards, regulatory requirements, and organizational policies directly into the Terraform workflow — automatically enforced at plan time rather than discovered during audits or after incidents.
The Compliance Pipeline Layer
Code commit ↓ terraform fmt + validate ↓ tfsec / Checkov (static analysis — catches 95% of issues instantly) ↓ OPA / Conftest (custom policy rules) ↓ Sentinel (HashiCorp's policy engine, Terraform Cloud/Enterprise) ↓ terraform plan (with cost estimation) ↓ Human review of plan + policy report ↓ terraform apply (only if all gates pass)Checkov: Open-Source Static Analysis
Checkov scans Terraform files for hundreds of predefined security misconfigurations:
# Installpip install checkov
# Scan Terraform directorycheckov -d ./infrastructure
# Scan and output JUnit XML for CIcheckov -d ./infrastructure --output junitxml > checkov-results.xml
# Scan a specific filecheckov -f main.tf
# Run only specific checkscheckov -d . --check CKV_AWS_23,CKV_AWS_116
# Example output:# Check: CKV_AWS_23: "Ensure every security groups rule has a description"# FAILED for resource: aws_security_group.web# File: /infrastructure/main.tf:45## Check: CKV_AWS_116: "Ensure that AWS Lambda function is configured for a DLQ"# FAILED for resource: aws_lambda_function.processortfsec: Terraform-Specific Security Scanner
# Installbrew install tfsec
# Scantfsec ./infrastructure
# With severity threshold (fail only on HIGH/CRITICAL)tfsec ./infrastructure --minimum-severity HIGH
# JSON output for CItfsec ./infrastructure --format json | jq '.results | length'Common issues tfsec catches:
- S3 buckets without encryption or public access block
- Security groups open to
0.0.0.0/0on sensitive ports - IAM policies with
*actions or resources - RDS instances with no backup retention
- CloudTrail without log validation
- EBS volumes without encryption
OPA / Conftest: Custom Policy Rules
Open Policy Agent with Conftest lets you write custom policies in Rego:
# Install conftestbrew install conftest
# Generate JSON planterraform plan -out=tfplanterraform show -json tfplan > tfplan.json
# Test policies against planconftest test tfplan.json --policy ./policies/package main
required_tags = {"Environment", "Team", "CostCenter", "ManagedBy"}
deny[msg] { resource := input.resource_changes[_] resource.change.actions[_] == "create" resource.type == "aws_instance"
actual_tags := {k | resource.change.after.tags[k]} missing := required_tags - actual_tags count(missing) > 0
msg := sprintf( "Resource %s is missing required tags: %v", [resource.address, missing] )}package main
deny[msg] { resource := input.resource_changes[_] resource.type == "aws_s3_bucket_public_access_block"
resource.change.after.block_public_acls == false
msg := sprintf( "S3 bucket %s must have block_public_acls = true", [resource.address] )}
deny[msg] { resource := input.resource_changes[_] resource.type == "aws_security_group_rule" resource.change.after.cidr_blocks[_] == "0.0.0.0/0" resource.change.after.from_port <= 22 resource.change.after.to_port >= 22
msg := sprintf( "Security group rule %s allows SSH from the public internet", [resource.address] )}Sentinel: HashiCorp’s Policy Engine
Available in Terraform Cloud and Terraform Enterprise, Sentinel policies are enforced server-side before any apply can proceed:
import "tfplan/v2" as tfplan
# All EBS volumes must be encryptedebs_encrypted = rule { all tfplan.resource_changes as _, change { change.type is not "aws_ebs_volume" or change.change.after.encrypted is true }}
# All RDS instances must use encrypted storagerds_encrypted = rule { all tfplan.resource_changes as _, change { change.type is not "aws_db_instance" or change.change.after.storage_encrypted is true }}
main = rule { ebs_encrypted and rds_encrypted}Terraform Compliant Resource Examples
Writing resources that pass common compliance checks from the start:
# Compliant S3 bucketresource "aws_s3_bucket" "data" { bucket = "my-compliant-bucket" tags = local.required_tags}
resource "aws_s3_bucket_public_access_block" "data" { bucket = aws_s3_bucket.data.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" { bucket = aws_s3_bucket.data.id rule { apply_server_side_encryption_by_default { sse_algorithm = "aws:kms" } }}
resource "aws_s3_bucket_versioning" "data" { bucket = aws_s3_bucket.data.id versioning_configuration { status = "Enabled" }}
# Compliant security group — no 0.0.0.0/0 on SSHresource "aws_security_group" "app" { name = "app-sg" description = "Application security group — web traffic only" vpc_id = aws_vpc.main.id
ingress { description = "HTTPS from load balancer only" from_port = 443 to_port = 443 protocol = "tcp" security_groups = [aws_security_group.alb.id] }
egress { description = "All outbound traffic" from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }}CI/CD Compliance Gate
- name: Run Checkov run: | checkov -d infrastructure/ \ --output github_failed_only \ --soft-fail-on MEDIUM \ --hard-fail-on HIGH,CRITICAL
- name: Run tfsec run: tfsec infrastructure/ --minimum-severity HIGH
- name: Run Conftest run: | terraform plan -out=tfplan terraform show -json tfplan > tfplan.json conftest test tfplan.json --policy policies/All three tools must pass before the apply job can run. This creates an automated compliance gate that requires zero manual effort on every infrastructure change.