compliance.tf

Prevent Destroy Data

Adds prevent_destroy = true to data-bearing resources. Blocks accidental deletion.

When to use this rule

Use this when: Your modules manage S3 buckets, RDS databases, DynamoDB tables, or EFS file systems that hold production data.

Do not use this when: You run ephemeral test environments where resources are destroyed after each CI run, or you are actively migrating data and need to delete old resources.


Why this rule exists

Accidentally destroying an S3 bucket, RDS instance, DynamoDB table, or EFS file system can cause data loss that is difficult or impossible to recover from. A single terraform destroy or a careless resource rename can trigger deletion of production data.

Terraform's prevent_destroy lifecycle argument is the built-in safeguard. But it cannot be passed as a variable into a module. Every team using upstream terraform-aws-modules must either fork the module to add it, or rely on review checklists.

RepositoryIssueTitle
hashicorp/terraform#3116Cannot use interpolations in lifecycle attributes
hashicorp/terraform#18367Feature request: support prevent_destroy for modules
hashicorp/terraform#27360A method to override configuration and meta arguments within a module

Affected resources

ResourceServiceWhy
aws_s3_bucketAmazon S3Object storage for logs, backups, application data
aws_db_instanceAmazon RDSRelational databases with primary application data
aws_dynamodb_tableAmazon DynamoDBNoSQL tables for application state and session data
aws_efs_file_systemAmazon EFSShared file storage mounted by compute workloads

What this rule does

Adds a lifecycle { prevent_destroy = true } block to each matching resource. Terraform and OpenTofu will refuse to destroy these resources during terraform destroy or resource replacement.


Before and after

Before (upstream module):

resource "aws_s3_bucket" "this" {
  # ... resource configuration ...

  tags = var.tags
}

After (with Prevent Destroy Data applied):

resource "aws_s3_bucket" "this" {
  # ... resource configuration ...

  tags = var.tags

  lifecycle {
    prevent_destroy = true
  }
}

The only change is the rule transformation. All existing arguments, outputs, and module behavior remain identical.

Real-world scenario

A developer renamed an S3 bucket variable during refactoring. Without prevent_destroy, terraform apply deleted the production bucket containing 3 years of audit logs and created a new empty one.


Known limits

  • Does not cover aws_rds_cluster (Aurora), aws_redshift_cluster, aws_elasticache_cluster, aws_neptune_cluster, or any other stateful resource not listed in affected resources.
  • Does not prevent deletion of S3 objects inside the bucket, only the bucket resource itself.
  • A resource that already declares prevent_destroy = false keeps that value. The rule adds the argument only where the lifecycle block does not already mention it.
  • Only .tf and .tofu files are read. Modules written in JSON syntax (.tf.json, .tofu.json) are left untouched.
  • Only the module's own directory and its first-level modules/* submodules are processed. Modules that this module calls by source URL are separate downloads and are handled on their own request.

Compliance framework support

This rule is not a compliance control. It supports these framework objectives as an operational safeguard:

FrameworkControlsRole
SOC 2CC6.1, CC7.2Supports data protection and availability objectives
NIST 800-53CP-9, SI-12Supports backup and information management

Default configuration

These are the values the rule ships with. They are not settable from a module source URL: ?rules= carries rule names only, so a per-request enable uses exactly the defaults below. Your organization sets them per rule, in its Baseline or in a ruleset, from the Operational Rules page; see Baseline rules and rulesets.

ParameterTypeDefaultDescription
resource_typeslist(string)["aws_s3_bucket", "aws_db_instance", "aws_dynamodb_table", "aws_efs_file_system"]Data-storage resource types to protect

How to enable

On a framework host, per request:

Add ?rules=lifecycle_prevent_destroy_data to your HTTPS module source. Use the full namespaced id. A bare lifecycle_prevent_destroy_data matches no rule: it is skipped server-side and your terraform init still succeeds, without the rule.

module "example" {
  source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=lifecycle_prevent_destroy_data"
}

On your organization's host:

Add the rule to the organization's Baseline, which applies to every module served from the bare organization form <alias>.compliance.tf/<namespace>/<name>/<provider>, or to a named ruleset that one module selects with ?ruleset=<name> on the HTTPS form of that address. A project environment (<alias>.compliance.tf/<project>/<env>/<module>) serves the rules frozen in its bound snapshot instead: there the rule goes into the config draft and reaches the environment by promotion. On both organization forms ?rules= is refused with a 400 — including the subtractive - prefix — rather than dropped, so you cannot mistake an ignored override for an applied one. Change the configuration, not the URL. See Baseline rules and rulesets for the configuration flow, and registry resolution for how the address forms differ.


Break-glass path

On a framework host, add ?rules=-example/prevent_destroy_data to the module source URL and run terraform init -upgrade to fetch a copy without this rule. The copy in .terraform/modules/ stays until the next upgrade, so remove the override when you are done. On your organization's host the parameter is refused with a 400 rather than ignored. The rule comes from the organization's configuration: a Baseline rule applies to every module served from the bare organization form, so an admin disables it for the duration of the operation, or keeps it out of the Baseline and in a ruleset that only the modules needing protection select. On a project environment the posture is the bound snapshot and its waivers. See Baseline rules and rulesets.


Failure modes

ScenarioResult
Rule targets a module with no matching resource typesNo-op. Module is delivered unchanged. No error.
Developer runs terraform destroy on a protected resourceTerraform exits with error: "Instance cannot be destroyed because of prevent_destroy."
Resource rename causes replacement planTerraform will error on destroy of the old resource. Use terraform state mv to rename without destroy.

Terraform and OpenTofu compatible

This rule works with both Terraform (1.x+) and OpenTofu (1.6+). It reads ordinary HCL, and anything it changes stays ordinary HCL, so the module behaves the same under either tool.

Help us improve this page

Operational Rules are a new feature. We'd love your feedback on this rule page — what's useful, what's missing, what's confusing. Share feedback.

On this page

Ask AI about this

Help improve this page