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, two
nights apart, so its deploy.sh sets LAB_TTL_HOURS=48 and the reaper does not
delete the stack before the second sitting. 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 has anything to show 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 for every destination storage class, so
a rule can move an object at the first evaluation after it is written, with no
holding period in S3 Standard first. What does apply is the 30-day minimum storage
duration charge on S3 Standard-IA and S3 One Zone-IA, and the two are easy to
conflate: a Days: 0 transition into either class is accepted, and the object is
then billed for 30 days whether it stays that long or not.
The tag filter is here because a lifecycle filter cannot express “everything
except”.
A rule filter carries one prefix and zero or more tags, and multiple prefixes in
one rule are not supported either. That leaves tagging the objects you want
included as the way to express an exception, 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.
Amazon S3 adds that day to the moment the successor version was written and
rounds the result up to the next midnight UTC, which puts the action two
midnights after the load rather than one. That is why the lab has two sittings,
and why the second one comes two mornings after the first.
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 when the evaluation runs 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 two mornings later ---
./scripts/count.py # what moved
./scripts/cost.py # Cost Explorer, by usage type, filtered on the tag
./scripts/teardown.sh
Run the load after lunch rather than first thing. Amazon S3 works out when a
rule is satisfied by adding the rule’s days to the object’s own clock and
rounding the result up to the next midnight UTC, which falls at 08:00 in Perth
and 10:00 or 11:00 on the east coast depending on daylight saving. Both actions
land on one of those morning boundaries: the Days: 0 transitions on the first,
the noncurrent-version work on the second. Every hour earlier you load is an
hour longer you wait, and a breakfast start puts the second sitting right on the
reaper’s 48 hours. Objects that already met a rule’s criteria before the rule
existed can be processed immediately, but nothing here has, since the loader
wrote them minutes ago.
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
second morning
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, at USD$0.02 per 1,000
into Glacier Instant Retrieval and USD$0.01 per 1,000 into the other three.
Those two rates over 1,500 swaths and 2,000 grid versions are the USD$0.05.
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.
The thousand versions the rule deleted produce no request line at all; expiry is
not a transition.
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.16 GiB. At USD$0.005 a gigabyte-month that is USD$0.0043 a month, against USD$0.0040 for leaving them in S3 Standard. The transition would have raised the storage bill by about seven percent 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 at the end of 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 three lines above come to about USD$0.12. With the early deletions, the whole lab is about USD$0.13.
If it fails
- Nothing moved by the second sitting. 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. S3 runs lifecycle actions internally rather than as API calls your bucket policy authorises, so a policy denying every action to every principal leaves the configuration running. 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.
- A rule’s days are added to the object’s own clock and rounded up to the next
midnight UTC, so
Days: 0acts the morning after the write andNoncurrentDays: 1the morning after that.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.