Exam Room · Solutions Architect

Lab: Move Ten Thousand Objects Down a Lifecycle Ladder

· 0 min read

Cloud Architecture · part of The Exam Room

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.

Three prefixes, three lifecycle rules, and what each one moves overnight Three rows, each running left to right from a prefix, through a rule, to an outcome. Row one: the swath slash prefix holding 2,000 objects of 160 KB goes through a rule filtered on that prefix and the tag retention equals archive, transitioning at day zero to Glacier Instant Retrieval; 1,500 objects move and 500 tagged retention equals hold stay in S3 Standard. Row two: the grids slash prefix holding 1,000 current objects and 3,000 noncurrent versions of 192 KB goes through a rule that transitions noncurrent versions to Standard-IA after one day and expires noncurrent versions beyond the two newest; 2,000 versions move to Standard-IA and 1,000 are deleted. Row three: the tiles slash prefix holding 7,000 objects of 24 KB goes through a rule transitioning at day zero to Glacier Instant Retrieval; nothing moves, because objects under 128 KB are not transitioned by default. A footer strip reads: 3,500 transitions charged, about five cents US; storage saved, about nine tenths of a cent US a month. swath/ 2,000 objects 160 KB each Prefix + tag filter retention = archive Transition, Days 0 to Glacier Instant Retrieval 1,500 moved GLACIER_IR 500 stay STANDARD (tagged retention = hold) grids/ 1,000 current 3,000 noncurrent 192 KB each Noncurrent versions after 1 day to Standard-IA expire beyond the two newest 2,000 moved STANDARD_IA 1,000 deleted no request charge tiles/ 7,000 objects 24 KB each Prefix filter Transition, Days 0 to Glacier Instant Retrieval nothing moved under 128 KB, and that is the default floor 3,500 transitions charged: about USD$0.05 storage saved: about USD$0.009 a month

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-configuration that the rules on the bucket are the ones you wrote; a configuration is replaced wholesale on each PUT, 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.
  • InvalidRequest when applying. NewerNoncurrentVersions was specified without a Filter element 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 an ObjectSizeGreaterThan to 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 And wrapper, or the tags are not what you think. Check one object with aws 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 ExpiredObjectDeleteMarker action 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 Deny in a bucket policy does not stop lifecycle. Lifecycle actions do not go through DeleteObject; 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.
  • NoncurrentDays cannot be zero, NewerNoncurrentVersions needs a Filter, 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. ListObjectVersions is what answers “did it move” right now.

Next

The rest of the Solutions Architect Associate lab track is in the README.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.