Abort Incomplete Multipart Uploads
Adds a validation to the module's lifecycle_rule variable: it must contain an enabled rule that aborts incomplete multipart uploads within abort_days (7 by default).
When to use this rule
Use this when: Buckets that receive multipart uploads, which is any bucket written by the AWS CLI, SDKs or tooling that uploads large objects.
Do not use this when: The module has no lifecycle_rule variable, or the lifecycle configuration is managed by a resource outside the module. Set variable_name when the module names the variable differently.
Why this rule exists
Parts of an interrupted multipart upload are billed as storage but are not returned by ListObjects, so they accumulate unseen until a lifecycle rule aborts them. The S3 bucket module accepts lifecycle rules as an input and ships with none.
This rule does not write the lifecycle rule for you. It adds a validation to the input so a module that is called without one is rejected on your side, before anything is created.
Affected resources
| Resource | Service | Why |
|---|---|---|
aws_s3_bucket_lifecycle_configuration | Amazon S3 | Where a bucket's lifecycle rules live |
What this rule does
Adds a validation block to the module's lifecycle_rule variable. This one is enforced on your side: the assertion ships inside the module source, so a value your organization has not allowed is rejected when you run Terraform, before anything is created.
Terraform checks a variable validation when it evaluates that variable, and where that happens depends on how the module is used. Consumed as a child module — the normal case for a module you pull by source — the check runs at terraform validate, whether the region comes from the parent's argument or from the module's own default. Run as the root module, terraform validate does not evaluate input variables at all: neither terraform.tfvars, nor -var, nor the default is checked there, and every one of them surfaces at terraform plan instead. A parent argument Terraform cannot resolve yet - one derived from another resource - is unknown at validate time and is checked when it becomes known. Either way the value never reaches apply, but a clean terraform validate on a root module is not proof that anything was checked. That split was measured on Terraform 1.15.2; Terraform has moved variable-validation evaluation before, so confirm it against the version you run before building a gate on it.
Existing HCL is never rewritten — the block is only added. Applying this to a module that already carries the identical validation changes nothing.
Adds a validation block to the module's lifecycle_rule variable. This one is enforced on your side: the assertion ships inside the module source, so a value your organization has not allowed is rejected when you run Terraform, before anything is created.
Terraform checks a variable validation when it evaluates that variable, and where that happens depends on how the module is used. Consumed as a child module — the normal case for a module you pull by source — the check runs at terraform validate, whether the region comes from the parent's argument or from the module's own default. Run as the root module, terraform validate does not evaluate input variables at all: neither terraform.tfvars, nor -var, nor the default is checked there, and every one of them surfaces at terraform plan instead. A parent argument Terraform cannot resolve yet - one derived from another resource - is unknown at validate time and is checked when it becomes known. Either way the value never reaches apply, but a clean terraform validate on a root module is not proof that anything was checked. That split was measured on Terraform 1.15.2; Terraform has moved variable-validation evaluation before, so confirm it against the version you run before building a gate on it.
Existing HCL is never rewritten — the block is only added. Applying this to a module that already carries the identical validation changes nothing.
Before and after
Before (upstream module):
variable "lifecycle_rule" {
type = string
default = null
}After (with Abort Incomplete Multipart Uploads applied):
variable "lifecycle_rule" {
type = string
default = null
validation {
condition = var.lifecycle_rule == null ? false : contains(["eu-west-1", "eu-central-1"], var.lifecycle_rule)
error_message = <<EOF
Variable "lifecycle_rule" must be one of the regions allowed by your organization: ["eu-west-1", "eu-central-1"]. An empty or unknown value is rejected.
EOF
}
}That is the real output, indentation included — the message is a heredoc, not a quoted string, and the module's closing brace ends up indented. The region list is whatever your organization configured; the two above are an example.
Before (upstream module):
variable "lifecycle_rule" {
type = string
default = null
}After (with Abort Incomplete Multipart Uploads applied):
variable "lifecycle_rule" {
type = string
default = null
validation {
condition = var.lifecycle_rule == null ? false : contains(["eu-west-1", "eu-central-1"], var.lifecycle_rule)
error_message = <<EOF
Variable "lifecycle_rule" must be one of the regions allowed by your organization: ["eu-west-1", "eu-central-1"]. An empty or unknown value is rejected.
EOF
}
}That is the real output, indentation included — the message is a heredoc, not a quoted string, and the module's closing brace ends up indented. The region list is whatever your organization configured; the two above are an example.
The module's arguments and outputs are unchanged. What changes is which values it accepts: the added validation rejects a value outside the allowlist, including a null default that used to be usable. That is the point of the rule, but it does mean a module can stop accepting an input it accepted before.
Real-world scenario
A bucket used for nightly exports carried months of abandoned multipart parts from interrupted runs, invisible to every listing tool and present on every bill.
Known limits
- Validates the module input only. A lifecycle configuration set by a separate
aws_s3_bucket_lifecycle_configurationoutside the module is not seen. - A stricter (smaller)
abort_incomplete_multipart_upload_daysin the module still passes; the parameterabort_daysis the maximum, 7 by default and never below 1. - The optional second check, a bounded expiry for noncurrent versions (
expire_noncurrent, off by default), exempts buckets withobject_lock_enabledbecause Object Lock denies the expiration. terraform validateis not a complete gate. Consumed as a child module it checks the input; run as the root module it does not evaluate input variables at all and the failure moves toterraform plan.- Only
.tfand.tofufiles 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.
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.
This rule does nothing until your organization configures it
Parameters never travel in the URL, so a per-request ?rules= enable of this rule runs with an empty allowlist — and an empty allowlist injects nothing and enforces nothing. The build still runs and still writes a manifest; what is missing is the validation block, and the manifest records the rule as not_configured rather than enforced. The rule acts only on the regions your organization has configured, and on an organization's host a ?rules= override is rejected outright rather than quietly ignored.
| Parameter | Type | Default | Description |
|---|---|---|---|
abort_days | string | "7" | 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 |
expire_noncurrent | bool | false | Opt in to also requiring a bounded noncurrent-version expiry. Off by default; see the rule docs before enabling |
noncurrent_days | string | "90" | Only read when expire_noncurrent is true. Maximum days a noncurrent version may be retained |
newer_noncurrent_versions | string | "5" | 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 |
variable_name | string | "lifecycle_rule" | Name of the module variable carrying the lifecycle rules |
The rule's definition
The HCL this rule is written in - its parameters, their shipped defaults, and the transformers it runs - is published at s3_lifecycle_abort_incomplete_uploads, alongside every other selectable rule.
How to enable
On a framework host, per request:
Add ?rules=s3_lifecycle_abort_incomplete_uploads to your HTTPS module source, using the id exactly as written.
module "example" {
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=s3_lifecycle_abort_incomplete_uploads"
}A ?rules= list is either all bare names, which replace your organization's rules for that download, or all - prefixed names, which subtract from them. Mixing the two is refused with a 400 rather than silently treated as a replacement. To add this rule on top of your organization's rules instead, use ?add_rules=s3_lifecycle_abort_incomplete_uploads; a bare ?rules= list cannot be combined with it, because a bare list already replaced the set there was something to add to.
The + add prefix was removed. It reached the registry only when written %2B, and a literal + in a query string decodes to a space everywhere, so ?rules=+s3_lifecycle_abort_incomplete_uploads arrived as a bare name with a leading space — one step from silently replacing your organization's rules. Any +, in either spelling, is now a 400 naming ?add_rules=.
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= and ?add_rules= are 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=-s3_lifecycle_abort_incomplete_uploads 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 it select. On a project environment the posture is the bound snapshot and its waivers. See Baseline rules and rulesets.
How to check it worked
Read compliancetf-manifest.json in the module you received. Its entry for this rule carries an outcome: enforced means the change was made, not_applicable means nothing matched - no resource of a targeted type, or the module already carried the identical change. Do not infer enforcement from a successful terraform init; a rule that changes nothing also inits cleanly.
Failure modes
| Scenario | Result |
|---|---|
| The module is called without an abort rule, or with one that is disabled | As a child module terraform validate fails with a message naming the entry to add; as the root module the failure moves to terraform plan. Nothing is created either way. |
| The module already carries an identical validation | No second block is added and the manifest records not_applicable. |
| The module has no variable with the configured name | Nothing is injected. Check the manifest outcome for this rule rather than assuming enforcement, and set variable_name to the variable the module uses. |
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.