compliance.tf

Operational Rule Definitions

Which rules run is decided before your source string gets a say. An organization host applies what that organization's admins configured. A framework host applies your own organization's Baseline when its rules state is Enforced, and ?rules= then replaces or subtracts from that set for a single download, while ?add_rules= adds to it. Either way, picking a rule starts with knowing that it exists and what it would do.

This page is that list. Every selectable rule appears below with its id, its parameters, its shipped defaults, and the transformers it runs, in the HCL the engine loads. Read the one you are considering before you put its id in a source line.

Each rule has its own anchor, so a rule can be linked to directly. eks_node_group_set_desired_size is /rules/definitions/#eks_node_group_set_desired_size.

What this page is, and what it is not

It is the list of rules you can name, with what each one would change. The definitions are rendered from the rule catalog that ships with the engine, so a change in the engine changes this page.

It is not a reusable artifact. There is no licence attached to these definitions and no grant of any kind is made. They are published so you can choose a rule, not so they can be lifted into another tool.

It is not the engine's source files byte-for-byte. See How this page is rendered.

It is not a record of what ran on a download. A module compliance.tf builds for you carries a compliancetf-manifest.json naming every rule the build received, each with an outcome saying what it actually did — a listed rule may have enforced nothing. That is the per-download record; this is the menu.

Rules are operational, not regulatory — see Operational Rules for how the two layers differ.


How this page is rendered

The definitions come from the rule catalog the engine publishes (rules-catalog.json, engine version 0.27.0), which is the engine's own serialization of its rule files. This site renders a vendored snapshot of that catalog rather than reading it live, so the version above is the snapshot's own: when it lags the engine the registry is running, a definition on this page lags with it.

Three things follow from rendering the catalog rather than copying the source files, and they matter when you are reading a definition to decide on it:

  • The intent block is not shown. It selects which blocks a rule inspects. The catalog stores a derived, lossy form of it, and reconstructing one would mean printing HCL that a rule does not have.
  • A field left at an empty or wildcard default is absent from the transformer block. The catalog drops those. The parameter table in each definition is still complete, so a parameter you see with a [] default is real even when no transformer line mentions it.
  • A value that equals a parameter's default is shown as param.<name>. That is how the rules are written, and it keeps a configurable value from reading as a hard-coded one.

The per-rule pages carry the parts a definition cannot show you: before-and-after HCL, known limits, failure modes, and the break-glass path.


Naming a rule

Append the rule id to the module source, exactly as it is written below. Nothing else about the source line changes.

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

Parameters never travel in the URL, so a rule named this way runs with the defaults printed below. Changing a parameter means configuring the rule in an organization Baseline or ruleset — see Baseline rules and rulesets.


eks_node_group_set_desired_size

Let Terraform change a terraform-aws-eks managed node group's desired, min and max size after create, with the AWS CLI from terraform_data (upstream ignores desired_size). Opt-in: needs bash, the AWS CLI and credentials for the node group's account where terraform apply runs; the first apply pushes desired_size once, so set it to the live value first

Severitywarning
Transformerslifecycle (safe), file_create (danger)
Familyprovisioner_remove_blocks — at most one rule in a family applies to a given resource type
PageSet EKS Node Group Desired Size
rule "eks_node_group_set_desired_size" {
  description = "Let Terraform change a terraform-aws-eks managed node group's desired, min and max size after create, with the AWS CLI from terraform_data (upstream ignores desired_size). Opt-in: needs bash, the AWS CLI and credentials for the node group's account where terraform apply runs; the first apply pushes desired_size once, so set it to the live value first"
  severity    = "warning"
  family      = "provisioner_remove_blocks"
  ack_risk    = true

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = ["scaling_config[0].min_size", "scaling_config[0].max_size"]
    resource_types = ["aws_eks_node_group"]
  }

  transformer "file_create" {
    path      = "pofix_eks_node_group_scaling.tf"
    content   = "# Generated by the pofix rule eks_node_group_set_desired_size. Do not edit: pofix\n# rewrites this file every time it serves the module.\n#\n# terraform-aws-eks ignores desired_size after create, and the rule also ignores\n# min_size and max_size on aws_eks_node_group.this. These two resources push\n# changes to those variables with the AWS CLI instead, in one\n# update-nodegroup-config call, so raising min_size above the live desired size\n# does not fail. Requires bash, the AWS CLI and credentials for the node group's\n# account on the machine that runs terraform apply.\n\nlocals {\n  pofix_scaling_script = <<-EOT\n    set -euo pipefail\n    fail() { echo \"pofix eks_node_group_set_desired_size: $*\" >&2; exit 1; }\n    command -v aws >/dev/null 2>&1 || fail \"the AWS CLI is required on the machine that runs terraform apply\"\n    for v in MIN MAX DESIRED; do\n      [[ $${!v} =~ ^[0-9]+$ ]] || fail \"$v='$${!v}' is not a non-negative integer\"\n    done\n    (( MIN <= MAX )) || fail \"min_size ($MIN) is greater than max_size ($MAX)\"\n    ng=(--cluster-name \"$CLUSTER\" --nodegroup-name \"$NODEGROUP\")\n    upd=(--name \"$CLUSTER\" --nodegroup-name \"$NODEGROUP\")\n\n    account=$(aws sts get-caller-identity --query Account --output text) || fail \"cannot resolve the caller's AWS account\"\n    [ \"$account\" = \"$(cut -d: -f5 <<<\"$ARN\")\" ] || fail \"credentials are for account $account, the node group is $ARN\"\n\n    live=$(aws eks describe-nodegroup \"$${ng[@]}\" --query 'nodegroup.scalingConfig.[minSize,maxSize,desiredSize]' --output text) \\\n      || fail \"cannot describe node group $NODEGROUP\"\n    read -r live_min live_max live_desired <<<\"$live\"\n    for v in live_min live_max live_desired; do\n      [[ $${!v} =~ ^[0-9]+$ ]] || fail \"unexpected describe-nodegroup output: '$live'\"\n    done\n\n    # limits: set min/max, keep the live desired size unless it falls outside them.\n    # desired: set the desired size only, inside the live min/max.\n    if [ \"$MODE\" = limits ]; then\n      target=$(( live_desired < MIN ? MIN : live_desired > MAX ? MAX : live_desired ))\n      [ \"$live_min\" = \"$MIN\" ] && [ \"$live_max\" = \"$MAX\" ] && [ \"$live_desired\" = \"$target\" ] && exit 0\n      config=\"minSize=$MIN,maxSize=$MAX,desiredSize=$target\"\n    else\n      (( live_min <= DESIRED && DESIRED <= live_max )) \\\n        || fail \"desired_size $DESIRED is outside the node group's min/max ($live_min/$live_max)\"\n      [ \"$live_desired\" = \"$DESIRED\" ] && exit 0\n      config=\"desiredSize=$DESIRED\"\n    fi\n\n    # Another update in progress rejects this one with ResourceInUseException; retry.\n    err=$(mktemp)\n    trap 'rm -f \"$err\"' EXIT\n    deadline=$((SECONDS + 1800))\n    until id=$(aws eks update-nodegroup-config \"$${ng[@]}\" --scaling-config \"$config\" --query update.id --output text 2>\"$err\"); do\n      if ! grep -q ResourceInUseException \"$err\" || (( SECONDS >= deadline )); then\n        cat \"$err\" >&2\n        fail \"update-nodegroup-config failed\"\n      fi\n      sleep 30\n    done\n\n    while :; do\n      status=$(aws eks describe-update \"$${upd[@]}\" --update-id \"$id\" --query update.status --output text)\n      case $status in\n        Successful) break ;;\n        Failed | Cancelled) aws eks describe-update \"$${upd[@]}\" --update-id \"$id\" --query update.errors >&2; fail \"update $id ended $status\" ;;\n      esac\n      (( SECONDS < deadline )) || fail \"update $id still $status after 30 minutes\"\n      sleep 15\n    done\n    echo \"pofix: $NODEGROUP scaling set to $config (update $id)\"\n  EOT\n\n  pofix_scaling_environment = {\n    ARN                = try(aws_eks_node_group.this[0].arn, \"\")\n    AWS_REGION         = try(split(\":\", aws_eks_node_group.this[0].arn)[3], \"\")\n    AWS_DEFAULT_REGION = try(split(\":\", aws_eks_node_group.this[0].arn)[3], \"\")\n    AWS_RETRY_MODE     = \"standard\"\n    AWS_MAX_ATTEMPTS   = \"10\"\n    CLUSTER            = try(aws_eks_node_group.this[0].cluster_name, \"\")\n    NODEGROUP          = try(aws_eks_node_group.this[0].node_group_name, \"\")\n    MIN                = var.min_size\n    MAX                = var.max_size\n    DESIRED            = var.desired_size\n  }\n}\n\n# min_size / max_size changes. Keeps the live desired size (an autoscaler's\n# choice) unless it falls outside the new range, where it is clamped.\nresource \"terraform_data\" \"pofix_scaling_limits\" {\n  count = var.create ? 1 : 0\n\n  triggers_replace = [aws_eks_node_group.this[0].id, var.min_size, var.max_size]\n\n  provisioner \"local-exec\" {\n    interpreter = [\"/bin/bash\", \"-c\"]\n    command     = local.pofix_scaling_script\n    environment = merge(local.pofix_scaling_environment, { MODE = \"limits\" })\n  }\n}\n\n# desired_size changes. Overwrites the live desired size, including an\n# autoscaler's choice. Runs after the limits, so both can change in one apply.\nresource \"terraform_data\" \"pofix_desired_size\" {\n  count = var.create ? 1 : 0\n\n  triggers_replace = [aws_eks_node_group.this[0].id, var.desired_size]\n\n  provisioner \"local-exec\" {\n    interpreter = [\"/bin/bash\", \"-c\"]\n    command     = local.pofix_scaling_script\n    environment = merge(local.pofix_scaling_environment, { MODE = \"desired\" })\n  }\n\n  depends_on = [terraform_data.pofix_scaling_limits]\n}"
    overwrite = true
  }
}

lifecycle_create_before_destroy

Stand up the replacement before destroying the old resource, so a security group still attached to an ENI or a certificate still bound to a listener can be replaced without a failed apply or an outage

Severitywarning
Transformerslifecycle (safe)
PageCreate Before Destroy
rule "lifecycle_create_before_destroy" {
  description = "Stand up the replacement before destroying the old resource, so a security group still attached to an ENI or a certificate still bound to a listener can be replaced without a failed apply or an outage"
  severity    = "warning"

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_security_group", "aws_acm_certificate"]
    description = "Resource types to replace create-first"
  }

  transformer "lifecycle" {
    action                = "add"
    create_before_destroy = true
    resource_types        = param.resource_types
  }
}

lifecycle_ignore_ami_changes

Ignore AMI changes on EC2 instances and launch templates to prevent unnecessary replacements. On terraform-aws-ec2-instance the native ignore_ami_changes does the same but moves the instance to a new state address

Severitywarning
Transformerslifecycle (safe)
PageIgnore AMI Changes
rule "lifecycle_ignore_ami_changes" {
  description = "Ignore AMI changes on EC2 instances and launch templates to prevent unnecessary replacements. On terraform-aws-ec2-instance the native ignore_ami_changes does the same but moves the instance to a new state address"
  severity    = "warning"

  param "ignore_changes" {
    type        = "list(string)"
    default     = ["ami", "image_id"]
    description = "Lifecycle attributes to ignore"
  }

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_instance", "aws_launch_template"]
    description = "Target resource type patterns"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.ignore_changes
    resource_types = param.resource_types
  }
}

lifecycle_ignore_autoscaling_changes

Ignore autoscaling-managed capacity on DynamoDB tables to prevent drift. On terraform-aws-dynamodb-table the native autoscaling_enabled does the same but moves the table to a new state address

Severitywarning
Transformerslifecycle (safe)
PageIgnore Autoscaling Changes
rule "lifecycle_ignore_autoscaling_changes" {
  description = "Ignore autoscaling-managed capacity on DynamoDB tables to prevent drift. On terraform-aws-dynamodb-table the native autoscaling_enabled does the same but moves the table to a new state address"
  severity    = "warning"

  param "ignore_changes" {
    type        = "list(string)"
    default     = ["read_capacity", "write_capacity"]
    description = "Lifecycle attributes to ignore"
  }

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_dynamodb_table"]
    description = "Target resource type patterns"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.ignore_changes
    resource_types = param.resource_types
  }
}

lifecycle_ignore_deployed_artifacts

Ignore the artifact pointer a deployment pipeline rewrites on Lambda functions and ECS services, so the next Terraform run stops rolling the deployed version back. Assumes something other than Terraform deploys the code: do not enable it where Terraform builds the artifact (create_package = true) or builds the container image. Native alternatives: ignore_task_definition_changes on terraform-aws-ecs (moves the service to a new state address) and ignore_source_code_hash on terraform-aws-lambda (covers the zip hash only)

Severitywarning
Transformerslifecycle (safe), lifecycle (safe)
PageIgnore Deployed Artifacts
rule "lifecycle_ignore_deployed_artifacts" {
  description = "Ignore the artifact pointer a deployment pipeline rewrites on Lambda functions and ECS services, so the next Terraform run stops rolling the deployed version back. Assumes something other than Terraform deploys the code: do not enable it where Terraform builds the artifact (create_package = true) or builds the container image. Native alternatives: ignore_task_definition_changes on terraform-aws-ecs (moves the service to a new state address) and ignore_source_code_hash on terraform-aws-lambda (covers the zip hash only)"
  severity    = "warning"

  param "lambda_ignore_changes" {
    type        = "list(string)"
    default     = ["s3_key", "s3_object_version", "source_code_hash", "image_uri"]
    description = "Attributes naming the deployed Lambda artifact (zip in S3, its version, its hash, or the container image). Override to [] to contribute nothing to the merged set"
  }

  param "lambda_resource_types" {
    type        = "list(string)"
    default     = ["aws_lambda_function"]
    description = "Resource types carrying the Lambda artifact pointer"
  }

  param "ecs_service_ignore_changes" {
    type        = "list(string)"
    default     = ["task_definition"]
    description = "Attributes naming the deployed ECS task definition revision. Assumes an external deployer publishes revisions; if Terraform publishes them this pins the service to a stale revision. Override to [] to contribute nothing to the merged set"
  }

  param "ecs_service_resource_types" {
    type        = "list(string)"
    default     = ["aws_ecs_service"]
    description = "Resource types carrying the ECS task definition pointer"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.lambda_ignore_changes
    resource_types = param.lambda_resource_types
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.ecs_service_ignore_changes
    resource_types = param.ecs_service_resource_types
  }
}

lifecycle_ignore_scaling_changes

Ignore the capacity attribute an external autoscaler owns on ASGs, EKS node groups and ECS services, so Terraform stops reverting the scaler. No effect on terraform-aws-eks node groups or the terraform-aws-ecs service module, which already ignore it. On terraform-aws-autoscaling the native ignore_desired_capacity_changes does the same but moves the group to a new state address

Severitywarning
Transformerslifecycle (safe), lifecycle (safe), lifecycle (safe)
PageIgnore Scaling Changes
rule "lifecycle_ignore_scaling_changes" {
  description = "Ignore the capacity attribute an external autoscaler owns on ASGs, EKS node groups and ECS services, so Terraform stops reverting the scaler. No effect on terraform-aws-eks node groups or the terraform-aws-ecs service module, which already ignore it. On terraform-aws-autoscaling the native ignore_desired_capacity_changes does the same but moves the group to a new state address"
  severity    = "warning"

  param "autoscaling_group_ignore_changes" {
    type        = "list(string)"
    default     = ["desired_capacity"]
    description = "Attributes to ignore on autoscaling groups. Override to [] to contribute nothing to the merged set"
  }

  param "autoscaling_group_resource_types" {
    type        = "list(string)"
    default     = ["aws_autoscaling_group"]
    description = "Resource types carrying the autoscaling group capacity attribute"
  }

  param "eks_node_group_ignore_changes" {
    type        = "list(string)"
    default     = ["scaling_config[0].desired_size"]
    description = "Attributes to ignore on EKS node groups; the desired size sits inside the scaling_config block. Override to [] to contribute nothing to the merged set"
  }

  param "eks_node_group_resource_types" {
    type        = "list(string)"
    default     = ["aws_eks_node_group"]
    description = "Resource types carrying the EKS node group capacity attribute"
  }

  param "ecs_service_ignore_changes" {
    type        = "list(string)"
    default     = ["desired_count"]
    description = "Attributes to ignore on ECS services. Override to [] to contribute nothing to the merged set"
  }

  param "ecs_service_resource_types" {
    type        = "list(string)"
    default     = ["aws_ecs_service"]
    description = "Resource types carrying the ECS service capacity attribute"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.autoscaling_group_ignore_changes
    resource_types = param.autoscaling_group_resource_types
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.eks_node_group_ignore_changes
    resource_types = param.eks_node_group_resource_types
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.ecs_service_ignore_changes
    resource_types = param.ecs_service_resource_types
  }
}

lifecycle_ignore_secret_rotation

Ignore the database credential attribute so a value that changes between plans - a regenerated random_password, a rotated secrets-manager or Vault data source, a variable supplied differently per runner - stops showing as a perpetual diff

Severitywarning
Transformerslifecycle (safe), lifecycle (safe)
PageIgnore Secret Rotation
rule "lifecycle_ignore_secret_rotation" {
  description = "Ignore the database credential attribute so a value that changes between plans - a regenerated random_password, a rotated secrets-manager or Vault data source, a variable supplied differently per runner - stops showing as a perpetual diff"
  severity    = "warning"

  param "db_instance_ignore_changes" {
    type        = "list(string)"
    default     = ["password"]
    description = "Credential attributes on RDS instances. Override to [] to contribute nothing to the merged set"
  }

  param "db_instance_resource_types" {
    type        = "list(string)"
    default     = ["aws_db_instance"]
    description = "Resource types whose credential attribute is named `password`"
  }

  param "rds_cluster_ignore_changes" {
    type        = "list(string)"
    default     = ["master_password"]
    description = "Credential attributes on Aurora clusters. Override to [] to contribute nothing to the merged set"
  }

  param "rds_cluster_resource_types" {
    type        = "list(string)"
    default     = ["aws_rds_cluster"]
    description = "Resource types whose credential attribute is named `master_password`"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.db_instance_ignore_changes
    resource_types = param.db_instance_resource_types
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.rds_cluster_ignore_changes
    resource_types = param.rds_cluster_resource_types
  }
}

lifecycle_ignore_tag_keys

Ignore drift on specific resource tag keys (e.g. an external scanner tag) via lifecycle ignore_changes, without exposing an arbitrary lifecycle value

Severitywarning
Transformerslifecycle (safe)
Familylifecycle_ignore_tags — at most one rule in a family applies to a given resource type
PageIgnore Selected Tag Keys
rule "lifecycle_ignore_tag_keys" {
  description = "Ignore drift on specific resource tag keys (e.g. an external scanner tag) via lifecycle ignore_changes, without exposing an arbitrary lifecycle value"
  severity    = "warning"
  family      = "lifecycle_ignore_tags"

  param "tag_keys" {
    type        = "list(string)"
    default     = []
    description = "AWS tag keys to ignore drift on. Each is emitted as ignore_changes = [tags[\"<key>\"]] with safe literal encoding; empty = unconfigured no-op"
  }

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_s3_bucket"]
    description = "Target resource type patterns. A module with none of these is not applicable"
  }

  transformer "lifecycle" {
    action         = "add"
    resource_types = param.resource_types
  }
}

lifecycle_ignore_tags

Ignore tag changes in lifecycle to prevent Terraform drift on managed resources

Severitywarning
Transformerslifecycle (safe)
PageIgnore Tag Changes
rule "lifecycle_ignore_tags" {
  description = "Ignore tag changes in lifecycle to prevent Terraform drift on managed resources"
  severity    = "warning"

  param "ignore_changes" {
    type        = "list(string)"
    default     = ["tags", "tags_all"]
    description = "Lifecycle attributes to ignore"
  }

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_*"]
    description = "Target resource type patterns"
  }

  transformer "lifecycle" {
    action         = "add"
    ignore_changes = param.ignore_changes
    resource_types = param.resource_types
  }
}

lifecycle_prevent_destroy_data

Prevent accidental destruction of data-storage resources (S3, RDS instances and clusters, DynamoDB, EFS, ElastiCache replication groups)

Severityerror
Transformerslifecycle (safe)
PagePrevent Destroy Data
rule "lifecycle_prevent_destroy_data" {
  description = "Prevent accidental destruction of data-storage resources (S3, RDS instances and clusters, DynamoDB, EFS, ElastiCache replication groups)"
  severity    = "error"

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_s3_bucket", "aws_db_instance", "aws_rds_cluster", "aws_dynamodb_table", "aws_efs_file_system", "aws_elasticache_replication_group"]
    description = "Data-storage resource types to protect"
  }

  transformer "lifecycle" {
    action          = "add"
    prevent_destroy = true
    resource_types  = param.resource_types
  }
}

lifecycle_prevent_destroy_encryption

Prevent destruction of KMS keys and Secrets Manager secrets

Severityerror
Transformerslifecycle (safe)
PagePrevent Destroy Encryption
rule "lifecycle_prevent_destroy_encryption" {
  description = "Prevent destruction of KMS keys and Secrets Manager secrets"
  severity    = "error"

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_kms_key", "aws_secretsmanager_secret"]
    description = "Encryption resource types to protect"
  }

  transformer "lifecycle" {
    action          = "add"
    prevent_destroy = true
    resource_types  = param.resource_types
  }
}

provisioner_remove_blocks

Remove provisioner blocks which are an anti-pattern in reusable modules

Severitywarning
Transformersblock_removal (caution)
PageNo Provisioners
rule "provisioner_remove_blocks" {
  description = "Remove provisioner blocks which are an anti-pattern in reusable modules"
  severity    = "warning"

  param "resource_types" {
    type        = "list(string)"
    default     = ["*"]
    description = "Target resource type patterns"
  }

  transformer "block_removal" {
    block_type = "provisioner"
  }
}

resource_restrict_instance_types

Restrict EC2 instance types to cost-effective options

Severityerror
Transformersattribute_restriction (safe)
PageRestrict Instance Types
rule "resource_restrict_instance_types" {
  description = "Restrict EC2 instance types to cost-effective options"
  severity    = "error"

  param "denied" {
    type        = "list(string)"
    default     = ["p3.*", "p4.*", "x1.*", "x2.*", "u-*"]
    description = "Denied instance type patterns"
  }

  param "message" {
    type        = "string"
    default     = "GPU and high-memory instance types are not permitted without approval"
    description = "Validation error message"
  }

  transformer "attribute_restriction" {
    resource_type = "aws_instance"
    attribute     = "instance_type"
    denied        = param.denied
    message       = param.message
  }
}

resource_set_deletion_protection

Turn on deletion protection for databases and load balancers that leave it unset, so a stray destroy cannot remove them

Severityerror
Transformersattribute_set (safe), attribute_set (safe), attribute_set (safe)
PageSet Deletion Protection
rule "resource_set_deletion_protection" {
  description = "Turn on deletion protection for databases and load balancers that leave it unset, so a stray destroy cannot remove them"
  severity    = "error"

  transformer "attribute_set" {
    attribute      = "deletion_protection"
    value          = true
    resource_types = ["aws_db_instance", "aws_rds_cluster"]
  }

  transformer "attribute_set" {
    attribute      = "deletion_protection_enabled"
    value          = true
    resource_types = ["aws_dynamodb_table"]
  }

  transformer "attribute_set" {
    attribute      = "enable_deletion_protection"
    value          = true
    resource_types = ["aws_lb"]
  }
}

s3_lifecycle_abort_incomplete_uploads

Require an enabled S3 lifecycle rule that aborts incomplete multipart uploads, so parts that are billed but never listed stop accumulating

Severitywarning
Transformersvariable_assertion (safe), variable_assertion (safe)
PageAbort Incomplete Multipart Uploads
rule "s3_lifecycle_abort_incomplete_uploads" {
  description = "Require an enabled S3 lifecycle rule that aborts incomplete multipart uploads, so parts that are billed but never listed stop accumulating"
  severity    = "warning"

  param "abort_days" {
    type        = "string"
    default     = "7"
    description = "Maximum days an incomplete multipart upload may linger before it must be aborted. A stricter (smaller) value in the module still passes. Default 7 - 1 day kills legitimately long-running uploads"
  }

  param "expire_noncurrent" {
    type        = "bool"
    default     = false
    description = "Opt in to also requiring a bounded noncurrent-version expiry. Off by default; see the rule docs before enabling"
  }

  param "noncurrent_days" {
    type        = "string"
    default     = "90"
    description = "Only read when expire_noncurrent is true. Maximum days a noncurrent version may be retained"
  }

  param "newer_noncurrent_versions" {
    type        = "string"
    default     = "5"
    description = "Only read when expire_noncurrent is true. Minimum newer versions the expiry must retain. This is an S3 MODIFIER, not a standalone action - NoncurrentDays is what actually expires anything"
  }

  param "variable_name" {
    type        = "string"
    default     = "lifecycle_rule"
    description = "Name of the module variable carrying the lifecycle rules"
  }

  transformer "variable_assertion" {
    variable      = param.variable_name
    condition     = "try(anytrue([for r in try(jsondecode(var.{{variable}}), var.{{variable}}) : try(r.enabled, tobool(r.status), lower(r.status) == \"enabled\", false) && try(tonumber(r.abort_incomplete_multipart_upload_days), 0) >= 1 && try(tonumber(r.abort_incomplete_multipart_upload_days), 0) <= 7]), false)"
    error_message = "Variable {{variable}} must contain an enabled lifecycle rule that aborts incomplete multipart uploads within 7 day(s). Add an entry such as { id = \"abort-incomplete-multipart-upload\", enabled = true, abort_incomplete_multipart_upload_days = 7 }. Parts of an interrupted multipart upload are billed as storage but are not returned by ListObjects, so they accumulate unseen."
  }

  transformer "variable_assertion" {
    variable      = param.variable_name
    condition     = "var.object_lock_enabled ? true : try(anytrue([for r in try(jsondecode(var.{{variable}}), var.{{variable}}) : try(r.enabled, tobool(r.status), lower(r.status) == \"enabled\", false) && anytrue([for e in try(flatten([r.noncurrent_version_expiration]), []) : try(tonumber(e.days), tonumber(e.noncurrent_days), 0) >= 1 && try(tonumber(e.days), tonumber(e.noncurrent_days), 0) <= 90 && try(tonumber(e.newer_noncurrent_versions), 0) >= 5])]), false)"
    error_message = "Variable {{variable}} must contain an enabled lifecycle rule whose noncurrent_version_expiration retains a noncurrent version for at most 90 day(s) while keeping at least 5 newer version(s). Buckets with var.object_lock_enabled set are exempt: Object Lock denies the expiration, so it would save nothing there."
    enabled       = param.expire_noncurrent
  }
}

variable_allowed_regions

Restrict the module's region variable to an org-approved allowlist

Severityerror
Transformersvariable_assertion (safe)
PageAllowed Regions
rule "variable_allowed_regions" {
  description = "Restrict the module's region variable to an org-approved allowlist"
  severity    = "error"

  param "regions" {
    type        = "list(string)"
    default     = []
    description = "Allowed AWS regions (enumerated, no globs). Empty = unconfigured: the rule injects nothing and enforces nothing"
  }

  param "allow_unset" {
    type        = "bool"
    default     = false
    description = "Accept a null region (provider-inherited). Unenforced by consent - it does not permit a wrong region"
  }

  param "variable_name" {
    type        = "string"
    default     = "region"
    description = "Name of the module variable carrying the region"
  }

  param "resource_types" {
    type        = "list(string)"
    default     = ["aws_*"]
    description = "Resource types this rule applies to. A module with none is not applicable"
  }

  transformer "variable_assertion" {
    variable      = param.variable_name
    condition     = "var.{{variable}} == null ? {{allow_unset}} : contains({{values}}, var.{{variable}})"
    error_message = "Variable \"{{variable}}\" must be one of the regions allowed by your organization: {{values}}. An empty or unknown value is rejected."
  }
}

Rules that are not listed here

The registry also carries a small set of internal content-processing rules. They are not selectable — ?rules= does not accept them — so they are outside what this page is for, which is choosing a rule to name. They are not among the rules above and their definitions are not published.

They are not invisible either: a module compliance.tf builds for you carries a compliancetf-manifest.json naming each rule the build received, internal ones included, each with an outcome recording what it actually did.


Terraform and OpenTofu compatible

Every rule above reads ordinary HCL, and anything it changes stays ordinary HCL. Modules behave the same under Terraform (1.x+) and OpenTofu (1.6+).

On this page

Ask AI about this

Help improve this page