This is a lab in the Solutions Architect Associate hands-on track. The reference
posts argue the decisions; this one writes a lifecycle configuration and then
reads the bill it produced. The full lab is in
lab-saa-05-lifecycle-ladder.zip.
If this is your first lab, do the one-time, once-per-account setup: run the
zip’s preflight.sh to confirm your account is ready, then deploy the
lab reaper, a standing backstop that auto-deletes
any lab you forget to tear down after 24 hours. This one runs in two sittings
about a day apart, so its deploy.sh sets LAB_TTL_HOURS=48 and the reaper
leaves the stack alone until the second sitting is done. Everything runs in
ap-southeast-2, and every price below is the Sydney list price in USD$.
The scenario
Sounding Line Hydrographics runs multibeam sonar surveys out of Hobart and has
never deleted anything. One bucket holds ten thousand current objects under
three prefixes: 2,000 raw swath files under swath/ at 160 KB each, 1,000
gridded bathymetry products under grids/ at 192 KB, and 7,000 chart tiles
under tiles/ at 24 KB. Versioning is on and the gridding job has run four
times over the same thousand keys, so every grid carries three noncurrent
versions behind it.
The swaths are read once, during processing, and then not again. The grids are read weekly. The tiles are served to a chart viewer all day. Five hundred of the swath files belong to a survey leg that is under dispute. Counsel has asked that those stay where they can be read in a click, with no restore step and no retrieval charge to argue about later.
There is no lifecycle configuration on the bucket. The S3 line on the bill is storage in S3 Standard, and it has grown every month for three years.
What you’re given
CloudFormation builds the bucket with versioning enabled and one bucket tag,
cost-centre=survey-ops. It also builds an S3 Storage Lens dashboard scoped to
that bucket on the free tier, and a storage class analysis configuration
filtered to swath/.
Storage Lens collects daily. Free metrics stay queryable for 14 days and advanced metrics for 15 months, and prefix-level aggregation exists only in the advanced tier, which is charged per million objects. Storage class analysis shows data in the console 24 to 48 hours after you configure a filter, then observes access patterns for 30 days or longer before it produces a recommendation. It only ever recommends S3 Standard to S3 Standard-IA. Neither will tell you anything this afternoon, which is why the stack turns them on before you need them.
scripts/load.py writes the 13,000 objects: four passes over grids/, one over
each of the others, about 1.2 GiB in total. It tags 1,500 of the swath files
retention=archive and the 500 disputed ones retention=hold, on the PUT
itself rather than in a second call.
scripts/count.py walks ListObjectVersions and prints current objects and
noncurrent versions per prefix, broken down by storage class. That is the
authoritative answer and it is available immediately, which is worth knowing
before you go hunting through a dashboard that updates once a day.
The gap is src/lifecycle.json. It ships as an empty rule list.
Your task
Four rules. Write them into src/lifecycle.json and apply with
./scripts/apply-lifecycle.sh.
One: the swaths, minus the disputed leg.
{
"ID": "archive-swaths",
"Filter": { "And": {
"Prefix": "swath/",
"Tags": [ { "Key": "retention", "Value": "archive" } ] } },
"Status": "Enabled",
"Transitions": [ { "Days": 0, "StorageClass": "GLACIER_IR" } ]
}
Days accepts 0 or any positive integer, so a transition can fire on the day an
object is created. Until July 2026 that was not true for S3 Standard-IA and S3
One Zone-IA, which required 30 days in S3 Standard first. That waiting period is
gone. The 30-day minimum storage duration charge on those two classes is not,
and the two are easy to conflate.
The tag filter is here because lifecycle has no way to say “everything except”.
A rule filter carries one prefix and zero or more tags, and multiple prefixes in
one rule are not supported either. The documented way to express an exception is
to tag the objects you want included, which is what the loader did. A filter
combining a prefix with tags has to be wrapped in And.
Two: the old grid versions.
{
"ID": "grid-versions",
"Filter": { "Prefix": "grids/" },
"Status": "Enabled",
"NoncurrentVersionTransitions": [
{ "NoncurrentDays": 1, "StorageClass": "STANDARD_IA" } ],
"NoncurrentVersionExpiration": {
"NoncurrentDays": 1, "NewerNoncurrentVersions": 2 }
}
NoncurrentDays must be a non-zero positive integer, so one day is the floor
and this rule cannot act until tomorrow. That single constraint is why the lab
has two sittings.
NewerNoncurrentVersions says how many noncurrent versions to retain, and
specifying it without a Filter element returns InvalidRequest. Here both
actions land on the same versions on the same day. Amazon S3 resolves that with
a documented precedence: permanent deletion wins over transition, so the oldest
of the three noncurrent versions is deleted rather than moved, and the two
newest transition.
Three: the tiles.
{
"ID": "archive-tiles",
"Filter": { "Prefix": "tiles/" },
"Status": "Enabled",
"Transitions": [ { "Days": 0, "StorageClass": "GLACIER_IR" } ]
}
Write this exactly as it stands. It is valid, it will be accepted, it will be enabled, and tomorrow it will have moved nothing at all. Work out why before you read If it fails.
Four: the housekeeping.
{
"ID": "housekeeping",
"Filter": { "Prefix": "" },
"Status": "Enabled",
"AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 },
"Expiration": { "ExpiredObjectDeleteMarker": true }
}
Neither of these actions is allowed in a rule whose filter uses object tags, so they live in a rule of their own. Expiring current versions does not remove incomplete multipart uploads, and an upload nobody completed is billed as storage for as long as its parts exist.
Then activate the tag. In the Billing and Cost Management console, under
Cost allocation tags, activate cost-centre. Do this at the start of the
first sitting. An applied tag key can take up to 24 hours to appear on that page,
and up to another 24 hours to activate after you select it. A tag attaches to
usage only from the moment it is on the resource.
One more thing about that tag, and it catches people. cost-centre is on the
bucket. Cost allocation tags label buckets. The retention tags the lifecycle
rule filters on are object tags, and they never reach the bill as a dimension.
They do carry a charge of their own, APS2-TagStorage-TagHrs, at USD$0.0065 per
10,000 tags a month.
Run it
./scripts/deploy.sh # bucket, Storage Lens dashboard, analytics config
./scripts/load.py # 13,000 PUTs, ~1.2 GiB, about 10 minutes
./scripts/apply-lifecycle.sh # put-bucket-lifecycle-configuration
./scripts/count.py # the baseline
# --- come back the next day ---
./scripts/count.py # what moved
./scripts/cost.py # Cost Explorer, by usage type, filtered on the tag
./scripts/teardown.sh
Apply the configuration early in your day. Amazon S3 evaluates rules daily and
stamps the resulting action at midnight UTC, which is 08:00 in Perth and 10:00
or 11:00 on the east coast depending on daylight saving. Objects that already
meet the criteria when a rule is created may be processed straight away, so the
Days: 0 transitions often start within hours; the noncurrent-version work
waits for the next evaluation either way.
Two runs of count.py look like this:
baseline
swath/ current 2,000 STANDARD 2,000
grids/ current 1,000 STANDARD 1,000 noncurrent 3,000 STANDARD
tiles/ current 7,000 STANDARD 7,000
next day
swath/ current 2,000 STANDARD 500 GLACIER_IR 1,500
grids/ current 1,000 STANDARD 1,000 noncurrent 2,000 STANDARD_IA
tiles/ current 7,000 STANDARD 7,000
cost.py calls GetCostAndUsage grouped by usage type, filtered to the
cost-centre tag, and prints the S3 lines:
APS2-Requests-Tier1 13,000 requests USD$0.072
APS2-Requests-Tier4 3,500 requests USD$0.050
APS2-TimedStorage-ByteHrs USD$0.002
Requests-Tier4 is the lifecycle transition into S3 Glacier Instant Retrieval,
S3 Intelligent-Tiering, S3 Standard-IA or S3 One Zone-IA. Transitions into S3
Glacier Flexible Retrieval and S3 Glacier Deep Archive land under
Requests-Tier3 instead, at USD$0.036 and USD$0.06 per 1,000 in Sydney against
USD$0.01 for the Standard-IA transition. The thousand versions the rule deleted
produce no request line at all; expiry is not a transition.
Now do the arithmetic. Those 3,500 transitions moved about 0.6 GiB and cost USD$0.05. The monthly storage saving on that 0.6 GiB is about USD$0.009: the 1,500 swaths drop from USD$0.025 to USD$0.005 a gigabyte-month, the 2,000 grid versions from USD$0.025 to USD$0.0138. Six months of nobody touching the data goes by before the move comes out ahead, and the 90-day minimum storage duration on Glacier Instant Retrieval applies whether the data stays that long or not.
Then the tiles, where the rule did nothing. Had it worked, 7,000 transitions would have cost USD$0.14. S3 Glacier Instant Retrieval also has a minimum billable object size of 128 KB, so 7,000 tiles of 24 KB would be billed as 0.85 GiB instead of 0.13 GiB. At USD$0.005 a gigabyte-month that is USD$0.0043 a month, against USD$0.0033 for leaving them in S3 Standard. The transition would have raised the storage bill and charged USD$0.14 to do it. The 128 KB default floor, which S3 applied to all transitions from September 2024, is there to stop exactly this.
Tear down the day after the second sitting. Deleting objects out of S3
Standard-IA before 30 days and Glacier Instant Retrieval before 90 produces
prorated early-deletion charges, which show up as APS2-EarlyDelete-SIA and
APS2-EarlyDelete-GIR. Here that is a cent or so. On forty terabytes it would
not be.
The whole lab is about USD$0.20.
If it fails
- Nothing moved overnight. Confirm with
aws s3api get-bucket-lifecycle-configurationthat the rules on the bucket are the ones you wrote; a configuration is replaced wholesale on eachPUT, not merged. A new or changed configuration takes a few minutes to propagate. Transitions and expirations are then queued asynchronously, so hours between a rule being satisfied and the object actually moving is normal. Billing changes when the rule is satisfied rather than when the object moves, with S3 Intelligent-Tiering the one exception. InvalidRequestwhen applying.NewerNoncurrentVersionswas specified without aFilterelement in that rule.tiles/is still entirely in S3 Standard. Objects smaller than 128 KB are not transitioned to any storage class by default. Add anObjectSizeGreaterThanto the filter and they will move. Read the arithmetic above before you do, because the bill goes up.- The disputed swaths moved too. The filter is missing its
Andwrapper, or the tags are not what you think. Check one object withaws s3api get-object-tagging. Tag-based rules are re-evaluated when the action runs, so a tag removed after the evaluation may or may not stop the action in time. - The object count went up. Expiring a current version in a versioning
enabled bucket writes a delete marker, and delete markers count as objects.
The
ExpiredObjectDeleteMarkeraction in rule four cleans up the markers left with no versions behind them. - Cost Explorer shows nothing under the tag. The tag key is not activated yet, or it was activated after the charges were incurred. Group by usage type with no tag filter in the meantime; usage types need no activation. Cost Explorer refreshes at least once every 24 hours and holds 13 months of daily and monthly data, with hourly data for the last 14 days only if you enable it.
- You wanted to stop a rule with a bucket policy. A
Denyin a bucket policy does not stop lifecycle. Lifecycle actions do not go throughDeleteObject; they run on internal S3 endpoints. Disable or delete the rule.
Reveal the solution
SRC=solution ./scripts/apply-lifecycle.sh && ./scripts/count.py
What you just learned
- A lifecycle rule filter holds one prefix and zero or more tags, combined with
And. There is no exclusion and no list of prefixes, so an exception is expressed by tagging the objects you want the rule to catch. - The 128 KB floor on transitions is a default, not a limit. It exists because a transition is charged per object and the small classes bill a 128 KB minimum, so moving small objects raises the bill twice over.
- Transitions are charged per 1,000 requests and storage savings accrue per gigabyte-month, so a move recovers its cost on a timescale measured in months. Work out the crossover before writing the rule, not after reading the bill.
NoncurrentDayscannot be zero,NewerNoncurrentVersionsneeds aFilter, and when an expiry and a transition both apply to a version on the same day, the deletion happens and the transition does not.- Cost allocation tags label buckets. Object tags drive lifecycle filters and carry their own small storage charge, and they never appear as a dimension in Cost Explorer.
- Storage Lens is a daily instrument with a 14-day query window on the free tier,
and storage class analysis needs 30 days of observation and only ever compares
S3 Standard with S3 Standard-IA.
ListObjectVersionsis what answers “did it move” right now.
Next
The rest of the Solutions Architect Associate lab track is in the README.