Abort incomplete multipart uploads
For platform engineers deciding whether to apply s3_lifecycle_abort_incomplete_uploads: what it changes, how to enable it, and how to check it worked.
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).
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 outside what the rule allows 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 value 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.
Affected resources
| Resource | Service | Why |
|---|---|---|
aws_s3_bucket_lifecycle_configuration | Amazon S3 | Where a bucket's lifecycle rules live |
Before and after
After (with Abort incomplete multipart uploads applied, shipped defaults):
variable "lifecycle_rule" {
# ... the module's own type and default, unchanged ...
validation {
condition = try(anytrue([for r in try(jsondecode(var.lifecycle_rule), var.lifecycle_rule) : 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 lifecycle_rule 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."
}
}The condition and message are the rule's own, at its shipped defaults; the engine writes the message as a heredoc. The exact HCL is in the rule's definition.
The module's arguments and outputs are unchanged. What changes is which values it accepts: the added validation rejects a value the rule does not allow, 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.
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.
Default configuration
These are the values the rule ships with. Parameters never travel in a module source URL, so a rule named there runs with exactly these defaults. Your organization sets them per rule, in its Baseline or in a ruleset.
| 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 HCL this rule is written in, with its parameters and the transformers it runs, is published at s3_lifecycle_abort_incomplete_uploads.
How to enable
On a framework host, add the rule on top of your organization's Baseline with ?add_rules= in the HTTPS module source:
module "s3_bucket" {
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=x.y.z&add_rules=s3_lifecycle_abort_incomplete_uploads"
}On your organization's host, an org admin adds the rule to the Baseline, which reaches every module served from that host, or to a named ruleset that a module selects with ?ruleset=:
module "s3_bucket" {
source = "https://bobthecorp.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=x.y.z&ruleset=<ruleset-name>"
}Replace x.y.z with the release you use and bobthecorp with your organization's alias. Which parameters each host accepts, and which it refuses with a 400, is on Rulesets in the console.
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.
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. - Works with Terraform 1.x and OpenTofu 1.6 and later. The rule reads ordinary HCL, and anything it changes stays ordinary HCL.
Break-glass path
On a framework host, add ?rules=-s3_lifecycle_abort_incomplete_uploads to the module source URL and run terraform init; the source changed, so Terraform installs a fresh copy without this rule. When you are done, remove the override and run terraform init again to restore it. 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.
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. |
Related
- Allowed regions
- Rulesets in the console - where an org admin adds this rule to the Baseline or a ruleset
- Rule catalog - every rule, with what each one changes