From Fork to One Source Line: Operational Rules for terraform-aws-modules

Three forks of terraform-aws-modules that exist to hold one lifecycle block, and the source line that replaces each of them. See the diff first in the Rules Playground, then register a free account and change one line.

Anton Babenko

I maintain terraform-aws-modules, and I see a lot of forks of it. Most of them carry one commit that matters: a lifecycle block on one resource. prevent_destroy = true on the bucket. ignore_changes = [tags] on everything. ignore_changes = [desired_capacity] on the Auto Scaling group. The rest of the fork is upstream, rebased.

That one commit costs more than it looks. Every upstream release means a rebase, a review to confirm the block still lands on the resource it was meant for, and a publish to whatever registry your teams pull from. Skip a release and you carry the bug it fixed. Do it for ten modules and it is a job.

This post takes three of those forks and replaces each with a source line. You can see the diff before you change anything, and the whole thing takes a free account, a token in ~/.netrc, and one edited source line.

Terraform and OpenTofu

Everything here applies to Terraform and OpenTofu alike. The rule changes ordinary HCL at download, before Terraform reads the module.

Fork one: the bucket that must not go

The commit adds prevent_destroy = true to aws_s3_bucket, or aws_db_instance, or aws_dynamodb_table. It is there because a rename once replaced the resource and took the data with it.

The rule is lifecycle_prevent_destroy_data. At download time it adds the block to the covered resources in the module root and its first-level submodules: S3 buckets, RDS instances and Aurora clusters, DynamoDB tables, EFS file systems, ElastiCache replication groups. What arrives in .terraform/modules/ is the upstream module with the block in it. The S3 bucket workarounds page shows the exact diff.

Fork two: the pipeline deploys, Terraform rolls it back

The commit adds ignore_changes = [s3_key, source_code_hash] to the Lambda function, or ignore_changes = [task_definition] to the ECS service. The pipeline deploys on Friday; Monday's routine apply puts the old version back, because the configuration still names the old artifact.

The rule is lifecycle_ignore_deployed_artifacts. It ignores the artifact pointer on Lambda functions (s3_key, s3_object_version, source_code_hash, image_uri) and the task_definition on ECS services. Do not enable it where Terraform builds the artifact itself; the rule assumes something else deploys the code. The Lambda and ECS pages have the diffs.

Fork three: the autoscaler and the rotated password

Two commits, same shape. One adds ignore_changes = [desired_capacity] on the Auto Scaling group, or scaling_config[0].desired_size on the EKS node group, or desired_count on the ECS service, so every plan stops proposing to undo the scaler. The other adds ignore_changes = [password] on the RDS instance because the password comes from a secret that rotates.

The rules are lifecycle_ignore_scaling_changes and lifecycle_ignore_secret_rotation. The scaling rule leaves min_size and max_size under Terraform's control and ignores only the attribute the scaler moves. The rotation rule ignores password on aws_db_instance and master_password on aws_rds_cluster, and nothing else. It assumes something outside Terraform sets the credential on the database, which is why the value keeps changing in the first place. See the EKS, Auto Scaling, and RDS pages.

Your fork versus the registry

Your fork versus the compliance.tf registry Swimlane: in your fork every upstream release means rebasing the fork, re-applying the lifecycle block, reviewing, publishing to your own registry and then terraform init, repeated every release; on the compliance.tf registry the upstream release is served with your rules applied at terraform init, and a rule is added or removed per request with add_rules or a -id entry. YOUR FORK COMPLIANCE.TF REGISTRY EVERY RELEASE UNCHANGED Upstreamnew release Rebase forkmerge upstream Re-apply blocklifecycle {} Review, publishyour registry terraform initfork source Upstreamnew release Served with your rulesadd_rules=id / rules=-id terraform initregistry source LEGEND step you own and repeat done by the registry, toggled per request repeats every release

In the fork, the block is yours. Every upstream release runs the top lane again: rebase, re-apply, review, publish, init.

On the registry, the upstream module is served with the rule applied at terraform init. The rule is a parameter on the source line: on for the download that names it, and, unless your organization has a Baseline, off for the one that does not. Your organization can also set it once for every module as a Baseline, or for a named group of modules as a ruleset. Every download carries a compliancetf-manifest.json naming the rules the build received and an outcome per rule, so the rules in effect for that download are on record. How the transformation works is in the fork tax post; I will not repeat it here.

See it before you believe it

Open the Rules Playground. It opens on the S3 bucket module at the version it pins, with lifecycle_prevent_destroy_data selected; press Run preview and the diff appears, with the outcome enforced. It is the same engine on the real module, and it needs no account. Change the module, tick another rule, run it again.

Do it in five minutes

  1. Read the diff. In the Playground, pick your module and the rule your fork carries. If the diff is the block you maintain, continue.
  2. Register a free account at compliance.tf/free-trial. No card.
  3. Create a registry token. For an https:// source line Terraform reads ~/.netrc, not the CLI credentials block; the Registry Tokens page shows the entry for registry.compliance.tf. The token stays in that file or a CI secret, never in the source line and never in git.
  4. Change one source line. Put the version you run today into the URL, and delete the version argument, which Terraform accepts only on registry-protocol sources:
terraform
module "s3_bucket" {  source = "https://registry.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.15.4&add_rules=lifecycle_prevent_destroy_data"   # the same inputs you pass today}
  1. Run terraform init, then terraform plan. Same inputs, same outputs. Open .terraform/modules/s3_bucket/main.tf and the block is there; compliancetf-manifest.json next to it says enforced.
  2. Delete the fork branch once the downloaded HCL matches the block your fork carried and the plan is clean for every environment that used it.

What the free tier covers

?add_rules= on the source line works on the free tier. A Baseline that applies to every module your organization downloads, and named rulesets, need an organization on a trial, paid or enterprise plan. See Baseline rules and rulesets.

What you give up

The module you run is built by the compliance.tf registry from the upstream release, with the rule applied. It does not come from GitHub. The manifest records every rule and its outcome, so a colleague can see which rules ran on that download without reading a fork; the downloaded HCL is the proof of what changed. You can subtract a rule per download with ?rules=-<id> when an operation needs it. 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. And for the problems no rule answers yet, the workarounds pages keep the native option, which is sometimes the better answer anyway: deletion_protection on the instance, manage_master_user_password on the database.

The fork existed to hold one block. The block now lives in a URL parameter you can turn off, and the fork can go.

Related: what shipped for Operational Rules since March and the Rule Catalog.

Continue the conversation

Discuss this post with the community or share it with your network.

Next Step

Replace the fork with one source line

Register a free account, put the token in place, and point one module at the compliance.tf registry with the rule you forked for. The plan tells you whether the fork can go.
Register a free account