Skip to main content
emnode / learn
Compliance

Configure lifecycle and versioning policies

One capability across S3 buckets, versioned buckets and ECR repositories: attach lifecycle rules so storage tiers, expires and prunes on a schedule you choose, instead of accumulating every object and image forever.

13 min·10 sections·AWS

Last reviewed

Remediates AWS Security Hub: ECR.3S3.10S3.13

Lifecycle and versioning: the basics

Why storage grows forever unless you tell it to stop

By default, storage in AWS keeps everything at full price indefinitely. An S3 bucket holds every object in its original storage class until someone deletes it. A versioned bucket keeps every old copy of every object as a noncurrent version that nothing ages out. An ECR repository keeps every image you have ever pushed, every CI build and every short-lived branch artifact. A lifecycle policy is the small JSON document that changes that: rules attached to the resource that transition data to cheaper tiers, expire old versions, or prune stale images on a schedule you choose.

AWS Security Hub turns each of these into its own control. S3.13 fails any general-purpose bucket with no lifecycle configuration at all. S3.10 fails a versioning-enabled bucket that has no lifecycle configuration to age out its noncurrent versions. ECR.3 fails a private repository with no lifecycle policy. They read as separate findings, but they are one capability: attach a retention rule so storage plateaus instead of compounding.

These controls catch absence, not quality. S3.13 and S3.10 fire the moment GetBucketLifecycleConfiguration returns no configuration; ECR.3 fires the moment a repository has no policy. A bucket can be wildly mis-tiered, or a versioning policy can fail to actually expire old versions, and still pass, because the rule checks that some configuration exists. So compliance and real cost control are two separate goals: the finding asks for a policy, but a useful policy is the one that actually transitions and expires the data nobody reads.

In this lesson you will learn how AWS storage compounds without lifecycle rules across object storage, versioned buckets and container registries, how to surface the hidden noncurrent-version and stale-image footprint the console does not show by default, and how to write policies that tier, expire and prune on a defensible schedule. The Controls this lesson covers section lists every Security Hub control in this capability, each linking to a deep page with the exact check and a copy-and-paste fix.

Fun fact

The 9 TB bucket that was only 800 GB

A data team turned on versioning for a staging-artifacts bucket so they could roll back bad builds. Two years later the bucket showed 800 GB of current objects in the console, but the storage bill said 9 TB. The build pipeline overwrote the same few hundred artifact files dozens of times a day, and every overwrite left a noncurrent version that S3 dutifully kept forever. A single lifecycle rule expiring noncurrent versions after 30 days reclaimed roughly 8 TB and cut that bucket's monthly storage cost by about 90%, with zero impact on anyone, because no build older than a week had ever been rolled back to. The same story plays out in ECR, where a single CI repository nobody opened reached 38 TB before one ten-line policy dropped it to 240 GB.

Finding compounding storage across an estate

Priya picks up a batch of Security Hub findings during the weekly triage. Several buckets are failing S3.10, versioning enabled with no lifecycle configuration, and the noisiest is a CI artifacts bucket whose storage line has climbed steadily for eighteen months even though the team swears the build volume is flat.

Rather than work the findings one by one, she confirms the hidden cost first. The console shows the current objects but not the noncurrent versions billing underneath, so she lists object versions to see the true footprint before writing a policy.

Confirm the hidden cost by comparing current versus noncurrent versions on the suspect bucket. The console shows only current objects by default.

$ aws s3api list-object-versions --bucket ci-build-artifacts --query '{Current: length(Versions[?IsLatest==`true`]), Noncurrent: length(Versions[?IsLatest==`false`]), DeleteMarkers: length(DeleteMarkers)}'
{
"Current": 1842,
"Noncurrent": 51367,
"DeleteMarkers": 940
}
# 51k dead versions behind 1.8k live objects, roughly 8 TB billing for data nobody reads.

Noncurrent versions vastly outnumber live objects. The storage bill is mostly retained dead copies the console hides.

How lifecycle rules work and how the controls evaluate themdeep dive

A lifecycle policy is an ordered set of rules, each with a selection (by age, count, tag status or prefix) and an action. For S3, current-object rules transition objects to cheaper classes or expire them after an age; version-aware actions, NoncurrentVersionTransition and NoncurrentVersionExpiration, move and delete old versions, with NewerNoncurrentVersions keeping the most recent few as an undo buffer. AbortIncompleteMultipartUpload clears orphaned upload fragments that bill as storage but are invisible in the object listing. For ECR, rules expire untagged images after a few days, cap recent tagged images per version, and clean up disposable prefixes, evaluated in priority order with the first match winning.

The controls check presence, not quality. S3.13 (Config rule s3-lifecycle-policy-check) and S3.10 (s3-version-lifecycle-policy-check) fire when GetBucketLifecycleConfiguration returns no configuration; ECR.3 (ecr-private-lifecycle-policy-configured) fires when a repository has none. A minimal rule that does not actually expire anything passes the audit while doing nothing for cost, which is why a correct cost-control policy needs the version-aware and abort actions, not just a placeholder. The S3 console's object count reflects only current versions, so the true footprint hides until you query list-object-versions or read S3 Storage Lens.

The durable version of this capability is preventive. Bake a default lifecycle policy into the bucket and repository provisioning modules so a resource is born with a retention decision attached, and back it with an AWS Config rule or Service Control Policy that catches a resource created without one. Pair ECR lifecycle with image tag immutability so pruning old tags cannot enable a ghost-tag re-push. The detective control catches drift; the provisioning default stops the waste accumulating in the first place.

What is the impact of running without lifecycle rules?

The direct impact is storage cost on data nobody reads. S3 Standard at roughly $0.023 per gigabyte-month and ECR at $0.10 per gigabyte-month both bill every byte regardless of whether anything has touched it in years. A high-churn versioned bucket routinely carries more retained noncurrent versions than live data, sometimes by five or ten times, and a busy CI repository can reach terabyte scale on its own. Multiply across an estate and the number reaches four or five figures monthly with nothing to show for it.

The waste compounds rather than sitting flat. Versioning cost grows with write activity, not with useful data, so the buckets generating the most retained-version cost are often the ones where the live data is small and stable. Incomplete multipart uploads and stale images ride along on the same neglect, invisible in the obvious dashboards. The cost curve bends upward on its own, which is why these resources quietly become the largest line in a storage bill long before anyone notices.

There is a supply-chain and compliance angle that earns ECR.3 its severity beyond cost. A registry full of old, unreviewed images is attack surface: if an attacker can re-push an old tag to point at malicious content, every pipeline still referencing it pulls the poisoned image. Lifecycle pruning combined with tag immutability closes that window. And an estate with dozens of unmanaged buckets and repositories produces a wall of these findings that drowns out higher-priority work and erodes the credibility of the compliance dashboard, so resolving them clears real noise at the same time as it cuts cost.

How do you configure lifecycle and versioning safely?

Work the capability as one loop rather than chasing individual findings. The order matters: measure the hidden footprint and confirm what is genuinely needed before you expire anything, so you do not delete something someone still depends on.

1. Inventory the resources with no lifecycle rule

Enumerate S3 buckets with no lifecycle configuration, versioned buckets specifically (the S3.10 set), and ECR repositories with no policy. Prioritise by storage size and by churn, since high-churn buckets and busy CI repositories accumulate waste fastest even when their live data is small. Start from the Security Hub findings, but treat the inventory as the source of truth.

2. Measure the hidden footprint and confirm what is needed

Before changing anything, quantify the prize and confirm the diagnosis. Use S3 Storage Lens or list-object-versions to compare noncurrent versus current storage, and describe-images with lastRecordedPullTime to see what is actually pulled. A noncurrent footprint several times larger than the live data, or images nobody has pulled in months, confirms the cause and tells you what you will reclaim. Agree the retention window with the data owners, not by defaulting to keep everything.

3. Apply a policy with a defensible retention window, then preview

For S3, write current-object transition and expiration plus NoncurrentVersionExpiration, ideally a NoncurrentVersionTransition first, and AbortIncompleteMultipartUpload after a week, keeping the last few versions as an undo buffer. For ECR, expire untagged images, cap recent tagged versions, and prune disposable prefixes, ordered least-aggressive first. Use the ECR lifecycle-policy preview to see what would expire without deleting, then apply. The finding clears on the next Config evaluation; storage ages out over the following days.

4. Make it the default at creation

A one-off pass is worthless if new resources keep arriving unmanaged. Bake a default lifecycle policy into the bucket and repository provisioning modules, and back it with an AWS Config rule or Service Control Policy so a resource created without one is caught at creation. Pair ECR lifecycle with image tag immutability. This converts the capability from a recurring cleanup chore into a setting nobody has to think about.

# Find versioned S3 buckets that have no lifecycle configuration.
for b in $(aws s3api list-buckets --query 'Buckets[].Name' --output text); do
  ver=$(aws s3api get-bucket-versioning --bucket "$b" --query Status --output text 2>/dev/null)
  if [ "$ver" = "Enabled" ] && ! aws s3api get-bucket-lifecycle-configuration --bucket "$b" >/dev/null 2>&1; then
    echo "S3.10 FAIL: $b (versioned, no lifecycle)"
  fi
done

# Apply an S3 lifecycle config that tiers and expires noncurrent versions and clears failed uploads.
aws s3api put-bucket-lifecycle-configuration \
  --bucket ci-build-artifacts --lifecycle-configuration file://lifecycle.json

# Preview an ECR policy before applying it, then attach it.
aws ecr start-lifecycle-policy-preview \
  --repository-name services/checkout-api --lifecycle-policy-text file://ecr-policy.json
aws ecr put-lifecycle-policy \
  --repository-name services/checkout-api --lifecycle-policy-text file://ecr-policy.json

Quick quiz

Question 1 of 5

Security Hub shows S3.13, S3.10 and ECR.3 failures across the estate. What is the most efficient way to think about them?

You can now treat lifecycle and versioning as one capability rather than a scatter of findings: inventory the unmanaged buckets and repositories, surface the hidden noncurrent-version and stale-image footprint, apply policies that actually tier, expire and prune the data nobody reads, and make a retention policy the default at creation so the waste cannot rebuild. The Controls this lesson covers section below links every control in this group to its deep page and fix.

Back to the library

Controls this lesson covers

One capability, many AWS Security Hub controls. This lesson is the shared playbook; each control below keeps its own deep page with the exact check, severity and a copy-and-paste fix.