Terraform's lifecycle block has a design constraint that every platform team eventually runs into: it only accepts literal values. You cannot pass prevent_destroy = true as a variable. You cannot interpolate ignore_changes from a list. There is no way to configure it from outside the module.
This has been an open request since 2015. The original issue — hashicorp/terraform#3116, "Cannot use interpolations in lifecycle attributes" — was closed as a design constraint. But the need didn't go away. In 2020, hashicorp/terraform#24188 asked for dynamic blocks and meta-arguments and collected 586+ reactions. People filed variations from every angle: #18367 asked for prevent_destroy at the module level. #21546 asked to pass ignore_changes into a module. #22544 hit the same wall: "Variables may not be used here." OpenTofu picked it up too — opentofu/opentofu#1329 has 160+ upvotes and is one of their most-requested features. OpenTofu shipped support for expressions in prevent_destroy in v1.12 (#2522), but ignore_changes and other lifecycle arguments remain static.
Nine years of issues. This isn't a niche complaint.
So when your platform team decides that all S3 buckets need prevent_destroy = true, that tag drift should be suppressed with ignore_changes, that provisioner blocks are banned, and that nobody should be launching p4d.24xlarge instances from a shared module — you have a problem. The upstream module doesn't have inputs for any of that. There's no variable you can set.
So you fork the module. You add the lifecycle blocks by hand. You do it again for the next module. And the next. And eventually you're maintaining 10 forked copies of terraform-aws-modules, each with your organization's operational standards manually spliced in.
Welcome to the fork tax.
The cost is real and it compounds. Every fork is a maintenance multiplier: when the upstream module releases a new version, you don't get it automatically. Someone has to notice, pull the changes, resolve merge conflicts with your customizations, test the result, and cut a new internal release. Multiply that by the number of forked modules and the number of upstream releases per year. The platform team becomes a bottleneck for module updates. New engineers inherit a pile of internal modules with tribal knowledge baked into the diffs. Version drift between teams and environments becomes normal. Eventually, people stop updating their forks altogether.
Not every team forks. Some write wrapper modules that call the upstream module and add lifecycle blocks in the wrapper. Some add items to PR review checklists: "Did you add prevent_destroy? Did you set ignore_changes on tags?" Some wire up OPA or Sentinel policies to flag missing lifecycle blocks in CI.
But all of these are workarounds for the same root problem: Terraform doesn't let you parameterize lifecycle blocks, and upstream modules don't include your organization's operational standards.
Terraform and OpenTofu
Compliance.tf works with both Terraform and OpenTofu. This article uses "Terraform" for brevity, but all examples and concepts apply equally to OpenTofu users. Terragrunt is also supported — same registry protocol, same URLs.
Why the Workarounds Don't Scale
Each workaround breaks in its own way.
Forked modules are the most common approach and the most expensive to maintain. You get full control over the code, but you also get full responsibility for keeping it current. A fork of terraform-aws-modules/s3-bucket at version 4.1.0 doesn't automatically become 4.2.0 when the upstream releases. You have to do that work. For five modules, that's manageable. For twenty, across multiple compliance frameworks, it becomes someone's full-time job.
Wrapper modules reduce duplication but don't eliminate it. The wrapper still needs lifecycle blocks inside it, and the wrapper itself becomes another module to version, test, and maintain. You've added a layer of indirection without solving the underlying problem. Developers now need to understand both the upstream module's interface and your wrapper's interface.
PR review checklists depend on human memory and attention. They work when the team is small and the reviewer knows the rules by heart. They stop working when the team grows, when reviewers rotate, or when someone is merging at 4 PM on a Friday. The checklist is documentation, not enforcement.
OPA and Sentinel policies are excellent at what they do: evaluating Terraform plans and blocking violations. If a plan is missing prevent_destroy, a well-written policy will catch it. But the policy cannot add the lifecycle block for you. It says "this plan is non-compliant" and the developer has to figure out what to add and where. For lifecycle blocks that should be on every resource of a given type, this is busywork. The policy engine identifies the gap. Someone still has to close it, every time, in every module.
All of these approaches treat the symptom. The root cause remains: the modules your developers download don't include your operational standards, and Terraform doesn't provide a mechanism to inject them.
A Different Approach: Module Transformation
What if the module your developers downloaded already had your operational standards applied?
That's the idea behind Operational Rules in compliance.tf. Rules are applied at download time, during terraform init. Your developers point their module source at the compliance.tf registry instead of the public Terraform registry. When they run terraform init, the module they receive has lifecycle blocks, provisioner restrictions, and instance type validations already in place.
The source change is one line:
module "s3" { source = "soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws" version = "~> 4.2"}Same module, same version, same terraform init / plan / apply workflow. The difference is that the module arrives with your organization's operational standards already in the code.
To be clear about what's happening: the module bytes are different. Lifecycle blocks are added to the HCL. Provisioner blocks are removed if your rules say so. Instance type validations are injected. But the module interface — its inputs, outputs, and version — stays the same. Your Terraform workflow, CI/CD pipeline, and state files all stay the same.
You can also specify rules directly in the module source URL, without any server-side configuration:
module "s3" { source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=pofix/ignore_tag_changes,pofix/prevent_destroy_data"}This is useful for trying rules out, for per-module overrides, or for teams that haven't set up org-level defaults yet.
This is different from compliance controls. Compliance.tf has two layers. Compliance Controls enforce regulatory requirements from frameworks like SOC 2, PCI DSS, and HIPAA — encryption at rest, access logging, public access blocking. Operational Rules enforce your organization's own standards: lifecycle blocks, tag management, provisioner removal, instance restrictions. Controls come from framework specs. Rules come from your platform team. Both are applied at download time.
The Operational Rules
Seven rules ship today. Here's what each one does to your module code.
Prevent Destroy Data
Adds lifecycle { prevent_destroy = true } to data-bearing resources: S3 buckets, RDS instances, DynamoDB tables, and EFS file systems.
resource "aws_s3_bucket" "this" {
bucket = var.bucket
+ lifecycle {
+ prevent_destroy = true
+ }
}
This is the rule that addresses the most common fork motivation. When an S3 bucket holds production data, accidental destruction during a terraform apply is a serious incident. prevent_destroy makes Terraform refuse to destroy the resource, requiring explicit removal of the lifecycle block before destruction is possible.
Ignore Tag Changes
Adds lifecycle { ignore_changes = [tags, tags_all] } to all AWS resources.
resource "aws_s3_bucket" "this" {
bucket = var.bucket
tags = var.tags
+ lifecycle {
+ ignore_changes = [tags, tags_all]
+ }
}
Tags often drift when external systems (AWS Config remediation rules, cost allocation tools, or manual console changes) modify them outside of Terraform. Without ignore_changes, every terraform plan shows a diff, and every apply resets the tags. This rule prevents that noise.
Ignore Autoscaling Changes
Adds lifecycle { ignore_changes = [read_capacity, write_capacity] } to DynamoDB tables. Useful when DynamoDB auto-scaling adjusts capacity outside of Terraform.
Ignore AMI Changes
Adds lifecycle { ignore_changes = [ami] } to EC2 instances. Prevents Terraform from replacing instances when an AMI ID changes in the configuration but the running instance should keep its current AMI.
Prevent Destroy Encryption
Adds lifecycle { prevent_destroy = true } to KMS keys and Secrets Manager secrets. Losing an encryption key means losing access to everything encrypted with it. This rule makes that harder to do accidentally.
No Provisioners
Removes all provisioner blocks from resources.
resource "aws_instance" "this" {
ami = var.ami
instance_type = var.instance_type
- provisioner "local-exec" {
- command = "echo ${self.private_ip}"
- }
}
Provisioners are an anti-pattern in Terraform. They run arbitrary commands outside of Terraform's state model and are difficult to test. Many platform teams ban them. This rule enforces that ban at the module level.
Restrict Instance Types
Denies GPU and specialty instance types (p3.*, p4.*, x1.*, x2.*, u-*) on EC2 instances. A single p4d.24xlarge costs over $32/hour. This rule prevents surprise compute bills from modules that accept arbitrary instance types as input.
Two Pillars, One Product
Controls handle the regulatory side — what your auditor checks. Rules handle the operational side — what your platform team enforces. Together, you get audit-ready modules with your operational standards already in the code. Neither layer requires changes to the Terraform workflow.
How It Works
Step 1: Choose your rules. You can specify them in the module source URL:
module "s3" { source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=pofix/ignore_tag_changes,pofix/prevent_destroy_data"}Or configure org-level defaults through the compliance.tf API, so they apply to every module download for every developer automatically.
Step 2: Developers authenticate with the compliance.tf registry (one-time setup via terraform login).
Step 3: Developers run terraform init. The module arrives with both compliance controls and operational rules already applied.
There's no extra CLI to install and no policy daemon to run. After one-time registry authentication, the entire experience is terraform init.
Per-request overrides let developers deviate from defaults when needed — replacing a DynamoDB table during a migration, for example:
# Add a rule for this specific module downloadsource = "https://soc2.compliance.tf/...?version=5.0.0&rules=+pofix/prevent_destroy_data" # Remove a default rule for this specific module downloadsource = "https://soc2.compliance.tf/...?version=5.0.0&rules=-pofix/ignore_tag_changes"The output is standard HCL — you can open the downloaded .tf files and inspect every change. A rules manifest (.ctf-rules-manifest.json) and SHA-256 rules hash in response headers are planned for a future release to make cross-environment verification even easier.
What's Next
Rule parameterization is coming — you'll be able to customize deny lists, choose which attributes to ignore, and scope rules to specific resource types. A Preview API will show you the exact diff a rule produces before you download the module. And org-authored rules will let platform teams write their own transformations for standards we don't cover.
Get Started
Operational Rules are available now for all compliance.tf organizations.
Get started with compliance.tf — from account setup to a compliant terraform plan in under 10 minutes.
Get started with Operational Rules — enable rules for your org in under 5 minutes.
Browse the full rule catalog or the module catalog.
Read the rules documentation.
If you're currently maintaining forked modules to add lifecycle blocks, this is the feature that lets you stop.
Continue the conversation
Discuss this post with the community or share it with your network.
