Operational Rules: your platform standards, applied at terraform init.
A lifecycle block cannot take a variable, so many teams fork a module to hold one. Operational Rules add it at download, and the fork can go.
One source line instead of a fork. The block arrives with the module.
module "s3_bucket" {
source = "https:/ / registry.compliance.tf/ terraform-aws-modules/ s3-bucket/ aws? add_rules= lifecycle_prevent_destroy_data"
}
# added at terraform init
lifecycle { prevent_destroy = true }See the exact change in the Playground, or read the docs to learn more.
The Playground needs no account.
Operational Rules for your Terraform modules
- For
- Your platform team, on Terraform or OpenTofu
- Applied
- When the module downloads, after the compliance controls
- Set by
- Your org admins, or one source line
Scope
Covers
- terraform-aws-modules served from a framework host or your org endpoint. The rules change ordinary HCL at download, before Terraform reads the module, so what arrives in
.terraform/modules/is the upstream module with the change in it. - The 16 rules in the catalog (chapter 4): lifecycle blocks, provisioner removal, instance-type restriction, deletion protection, S3 abort-incomplete-uploads and allowed regions.
- Terraform and OpenTofu alike, with the same source line.
Does not cover
- Rules your team writes. Every rule in the catalog is written and maintained by compliance.tf.
- Resources outside the modules compliance.tf serves. A rule changes the module it is served with, nothing else.
- Problems no rule answers. The workarounds pages keep the native option for each module, which is sometimes the better answer anyway.
Why teams fork
Terraform does not let you pass prevent_destroy or ignore_changes into a module as a variable. It has been an open request since 2018, with hundreds of reactions across related issues. Teams that need one work around it:
| Workaround | What it costs |
|---|---|
| Fork every module | A copy with lifecycle blocks added. Every upstream release means a rebase, a review to confirm the block still lands on the resource it was meant for, and a publish, repeated for each module. |
| Write wrapper modules | Another layer of indirection that still needs lifecycle blocks inside. |
| Add PR review checklists | "Did you add prevent_destroy?" Human memory does not scale. |
| OPA or Sentinel policies | Can flag a missing lifecycle block, but cannot add one to the code. |
Known limits, module by module
No module in any registry can expose prevent_destroy or ignore_changes as an input. The docs collect the problems this causes in each terraform-aws-modules module: the native workaround first, the rule that answers it where one exists, and the diff the Playground shows. Free to read, free to try.
S3 bucketRDSEKSLambdaVPCDynamoDB tableAll modules with known limits
The full story: Stop paying the Terraform fork tax, and three forks replaced in From fork to one source line.
Two layers in one download. Your standards come second.
Compliance controls come from your framework. Operational Rules come from your platform team. Both are applied when the module is served, and you read the result as plain HCL.
- Upstream releaseterraform-aws-modules, unchanged
- Compliance controlsThe settings your framework requires
- Operational RulesYour Baseline, rulesets and per-request rules
- terraform initThe module arrives with both applied
Two layers, applied at download time
| Aspect | Compliance controls | Operational Rules |
|---|---|---|
| What it enforces | Regulatory requirements | Organizational standards |
| Driven by | Frameworks: SOC 2, PCI DSS, HIPAA, NIST, ISO 27001 and others | Platform team decisions |
| Examples | Encryption at rest, access logging, public access blocking | prevent_destroy, ignore_changes, instance restrictions |
| Who configures | compliance.tf, defined by the framework | Your org admins |
Controls first
The module is served with the controls for your framework first. Which controls apply, and the clauses each maps to, is on each framework page, for example SOC 2, and in the control catalog.
Rules second, merged additively
Operational Rules run after the controls. Where both change the same block, the changes merge additively.
No new tooling
Developers run terraform init with a compliance.tf module source. No new CLI or policy daemon. The module arrives with your standards already in the code.
16 rules. Every rule has a page with its diff, limits and failure modes.
Filter the catalog, or search by rule ID, resource or attribute, for example prevent_destroy or RDS.
16 of 16 shown
Each rule ID links to its public docs page. Every rule is applied at download time, to every module your organization enables it for.
Rule catalog
The rules in the catalog, with the change each one makes and the resources it applies to.
| Rule | What it does | Applies to |
|---|---|---|
| Prevent destroy datalifecycle | Adds a lifecycle block to stateful resources so accidental terraform destroy can't wipe production data. lifecycle { prevent_destroy = true }S3, RDS instances and clusters, DynamoDB, EFS, ElastiCache | S3, RDS instances and clusters, DynamoDB, EFS, ElastiCache |
| Ignore tag changeslifecycle | Adds a lifecycle block so external tag changes (AWS Config rules, cost allocation tools, FinOps automation) won't trigger plan diffs. lifecycle { ignore_changes = [tags, tags_all] }All taggable resources | All taggable resources |
| Ignore autoscaling changeslifecycle | Adds a lifecycle block so DynamoDB auto-scaling doesn't fight with Terraform on every plan. lifecycle { ignore_changes = [read_capacity, write_capacity] }DynamoDB | DynamoDB |
| Ignore AMI changeslifecycle | Adds a lifecycle block so AMI updates from image pipelines don't cause unexpected instance replacements. lifecycle { ignore_changes = [ami, image_id] }EC2 instances, launch templates | EC2 instances, launch templates |
| Prevent destroy encryptionlifecycle | Adds a lifecycle block to encryption keys and secrets. Losing a KMS key means losing access to everything it encrypted. lifecycle { prevent_destroy = true }KMS, Secrets Manager | KMS, Secrets Manager |
| No provisionersprovisioner | Removes all provisioner blocks from module resources. Provisioners run arbitrary commands outside Terraform's state model and break idempotency. provisioner blocks removedAll resources | All resources |
| Restrict instance typesresource | Denies GPU and specialty instance types (p3, p4, x1, x2, u-*) that can generate five- and six-figure monthly bills from a single terraform apply. denies p3, p4, x1, x2, u-* typesEC2 | EC2 |
| Allowed regionsvariable | Adds a validation block to the module's region variable so a region outside your organization's allowlist is rejected before anything is created. validation { condition = contains(allowlist, var.region) }Modules with a region variable | Modules with a region variable |
| Ignore selected tag keyslifecycle | Adds a lifecycle block that ignores only the tag keys your organization names, so scanner or cost tags written outside Terraform stop showing up as drift. lifecycle { ignore_changes = [tags["<key>"]] }S3 by default | S3 by default |
| Create before destroylifecycle | Adds a lifecycle block so a security group or ACM certificate is replaced create-first, and whatever is still attached to the old one keeps working through the swap. lifecycle { create_before_destroy = true }Security groups, ACM certificates | Security groups, ACM certificates |
| Ignore deployed artifactslifecycle | Adds a lifecycle block so the zip, image or task definition your pipeline deployed is not rolled back by the next terraform plan. lifecycle { ignore_changes = [s3_key, image_uri, task_definition] }Lambda functions, ECS services | Lambda functions, ECS services |
| Ignore scaling changeslifecycle | Adds a lifecycle block so the capacity an autoscaler sets on Auto Scaling groups stops showing as drift. terraform-aws-eks and the terraform-aws-ecs service module already ignore it; the rule covers node groups and services written outside them. lifecycle { ignore_changes = [desired_capacity] }Auto Scaling groups; EKS node groups and ECS services outside terraform-aws-modules | Auto Scaling groups; EKS node groups and ECS services outside terraform-aws-modules |
| Set EKS node group desired sizeeks | Opt-in. terraform-aws-eks ignores desired_size after create, so Terraform cannot resize a node group. This rule pushes desired, min and max size changes with the AWS CLI from terraform_data, so a min_size raise no longer fails. terraform_data runs aws eks update-nodegroup-configEKS managed node groups (terraform-aws-eks) | EKS managed node groups (terraform-aws-eks) |
| Ignore secret rotationlifecycle | Adds a lifecycle block so a database password that rotates outside Terraform stops proposing a reset on every plan. lifecycle { ignore_changes = [password] }RDS instances, Aurora clusters | RDS instances, Aurora clusters |
| Set deletion protectionresource | Turns on the provider's deletion protection where a module leaves it unset, so AWS refuses a stray delete from Terraform, the console or the API. deletion_protection = trueRDS, Aurora, DynamoDB, load balancers | RDS, Aurora, DynamoDB, load balancers |
| Abort incomplete multipart uploadss3 | Adds a validation to the module's lifecycle_rule variable requiring a rule that aborts incomplete multipart uploads, so parts that are billed but never listed stop accumulating. validation { abort_incomplete_multipart_upload_days <= 7 }S3 buckets | S3 buckets |
Set once for every team. Change one download when you need to.
Your org admins pick the rules once. Teams get them through the source line they already write, and the defaults are on record for every download.
- Baseline
- Every module from bobthecorp.compliance.tf
- Ruleset
- The modules that ask for it by name
- Per download
- Add or drop one rule in a single source line
Configured on the Operational Rules page of the compliance.tf console, or through the API.
Configure and override
Baseline
Org admins set a Baseline that applies to every module your organization downloads from its own host, from the Operational Rules page in the console or the API.
Named rulesets
A named ruleset applies to the modules that select it. On your organization's host, a module selects one in its source line. A ruleset adds to the Baseline; a source line cannot use it to subtract a rule.
source = "https:// bobthecorp.compliance.tf/ terraform-aws-modules/ s3-bucket/ aws? ruleset= ignore-scanner-tags"
One download, on a framework host
Per-request query parameters drop one of your organization's rules for that download, or add one. A bare ?rules= list replaces your rules rather than adding to them, so it cannot be combined with ?add_rules=.
# add one rule for this download source = "https:// soc2.compliance.tf/ terraform-aws-modules/ s3-bucket/ aws? add_rules= lifecycle_prevent_destroy_data" # drop one of your organization's rules for this download source = "https:/ / soc2.compliance.tf/ terraform-aws-modules/ s3-bucket/ aws? rules= -lifecycle_ignore_tags"
The SOC 2 host serves accounts on a trial or a paid plan. Dropping a rule needs an organization with rules of its own.
Plans
Per-request rules, ?rules= on the source line, work on the free tier: you choose which to include. A Baseline and named rulesets need an organization on a trial, paid or enterprise plan. See pricing and Baseline rules and rulesets.
Read the diff before a module changes. Check what arrived after.
The Playground and the Preview API show the exact change a rule makes before anyone downloads the module. Every module built for you carries a manifest of the rules it received.
Review and record
Rules Playground
A browser view of a rule's effect on a module before download: real engine, real module, no account. The preview reads the module root only.
Rules Preview API
- Public route: no account, allowlisted pinned module versions,
?rules=required, rate limited. - Private route: your dashboard session, any catalog module, your org Baseline plus
?add_rules=or?ruleset=.
The manifest
A module compliance.tf builds for you carries a compliancetf-manifest.json, written by terraform init into the module directory under .terraform/modules/. It names every rule the build received with an outcome per rule, the file paths it changed, and a variant fingerprint, so you can check that two environments pulled the same rules-applied artifact. It records files, not resource addresses.
Version and exit
The module version you run stays in the source line, with the same inputs and outputs. Going back is the same edit in reverse: restore the old source line and run terraform init. Resource addresses do not change, so there is no state migration.
Start with the rule your fork carries.
Pick your module and rule in the Playground. If the diff is the block you maintain, change one source line and re-plan.
No credit card or AWS account needed to start.
Next step
To replace a fork
- Read the diff in the Playground for your module and the rule your fork carries.
- Register at the free trial page and create a registry token. For an
https://source line the token goes in~/.netrcor a CI secret, never in the source line. - Put the version you run today into the URL with the rule, run
terraform initandterraform plan, and delete the fork once the downloaded HCL matches the block it carried.
Reference
Operational Rules docs, rule definitions and the adoption guide.
Not convinced yet, or missing a feature you need? Send us an email - we really want to hear your feedback.