terraform-aws-modules/terraform-aws-s3-bucket examples/complete (tag v5.9.1), its flagship example
Source: https://github.com/terraform-aws-modules/terraform-aws-s3-bucket.git at v5.9.1.
Set: core. Lane: terraform-popular.
Why it is in the core set: a most-downloaded terraform-aws-modules example, pinned by tag; the shape most people deploy
Clear. Every headline stage passes.
| Stage | Verdict | Duration | Detail |
|---|---|---|---|
| Cold deploy | pass | 1m14s | 30 resources added by plain terraform, 4 buckets confirmed live, no tofu-address tag |
| Migrate | pass | 1m26s | 6 of 30 stamped, 1 recorded (random_pet, issue #340), 23 skipped (untaggable), 0 failed, 26 identities recorded (#364 unit A2); markers survived the residue-classification apply |
| Replan from nothing | pass | 4s | no resource action proposed; 29 rendered identity occurrences (11 distinct), all naming known roots |
| No-op apply | pass | 11s | no-op apply (0 added, 0 changed, 0 destroyed); bucket count unchanged at 4 |
| Drift and reconverge | pass | 20s | accelerate config drifted to Enabled, exactly 1 change proposed and applied, reconverged to Suspended, final plan empty |
| Rename | pass | 23s | moved block: module.cloudfront_log_bucket renamed to module.cloudfront_log_bucket_renamed with zero churn (0 add, 1 change, 0 destroy), the bucket’s tofu-address marker rewritten in place; live-mv: module.simple_bucket renamed to module.simple_bucket_renamed with zero churn, marker rewritten in place; both live bucket names unchanged, read via the AWS CLI; the post-rename plan proposes no resource action |
| Remove a block | pass | 19s | choudoufu: deleting module.simple_bucket_renamed’s block proposed exactly two destroys (0 add, 0 change, 2 destroy: the bucket and its untaggable public_access_block child), applied cleanly (0 added, 0 changed, 2 destroyed), the bucket is genuinely gone from the live account (head-bucket on simple-tolerant-unicorn now fails, read via the AWS CLI, not choudoufu’s own report), and the next plan proposes no resource action; stock oracle on cold_deploy’s own state also proposes exactly the same two destroys for the same two objects; the target was chosen to avoid issue #404’s shape (a sibling policy re-reading the removed bucket’s own ARN) - module.log_bucket and module.s3_bucket are both left untouched |
| Change count | pass | 56s | synthetic block (this estate’s root configuration declares no count or for_each at all, and its only multi-instance knob - the intelligent_tiering input’s two-entry map - fans out onto aws_s3_bucket_intelligent_tiering_configuration, which has no tags argument, issue #410’s untaggable-child shape; so aws_s3_bucket.count_test, a taggable type this estate already exercises, at an address no other stage uses). choudoufu: scaling count_test from 2 to 1 proposed exactly one destroy (0 add, 0 change, 1 destroy), and it was the HIGHER index - count_test[1], s3-bucket-complete-count-test-1 - with count_test[0] not appearing in the plan at all; applied cleanly (0 added, 0 changed, 1 destroyed); head-bucket on s3-bucket-complete-count-test-1 then fails outright, while the survivor s3-bucket-complete-count-test-0 keeps both its CreationDate (2026-09-08T22:01:35+00:00) and its tofu-address=aws_s3_bucket.count_test:0 marker, read through the AWS CLI rather than choudoufu’s own report, and count_test[1]’s local record is tombstoned (has tombstone, no identity - the #398-guard shape) rather than deleted. Scaling back from 1 to 2 proposed exactly one create (1 add, 0 change, 0 destroy) for count_test[1] alone, and the recreated bucket is genuinely a NEW object: same deterministic name (an S3 bucket’s id IS its own name - probed against floci with no tofu in the loop) but a new CreationDate (2026-09-08T22:01:35+00:00 -> 2026-09-08T22:02:12+00:00), re-stamped tofu-address=aws_s3_bucket.count_test:1 (tags do not survive a delete+recreate on this image, so the marker is one the apply wrote again), and a record identity naming the recreated bucket; count_test[0]’s CreationDate and marker were unchanged at every step. The next plan proposes no resource action. G-ORACLE, plain terraform standing the identical 2-instance block up for real in the idle greenfield-oracle account, shows the identical shape: destroy the higher index only (0 add, 0 change, 1 destroy), recreate it under the same deterministic bucket name but a new CreationDate (2026-09-08T21:58:09+00:00 -> 2026-09-08T21:58:18+00:00), index 0’s CreationDate (2026-09-08T21:58:09+00:00) unchanged at both steps. BREAK_COUNT=1 asserts the wrong instance (count_test[0]) was destroyed and reports this stage fail, as the stage’s Break text requires. |
| Replace with create_before_destroy | pass | 20s | choudoufu: changing module.log_bucket’s ForceNew bucket argument proposed exactly one bucket replace at the same declared address, cascading into its ownership_controls, policy and public_access_block (all replaced) plus module.s3_bucket’s own logging target_bucket (updated in-place) - 4 to add, 1 to change, 4 to destroy, matching F-ORACLE’s own plan shape; applied cleanly; the old bucket (logs-tolerant-unicorn) is confirmed gone and the new bucket (logs-tolerant-unicorn-replaced) carries the marker, both via the AWS CLI; the local record store’s record at the same address now names the new bucket, not the destroyed one; the next plan proposes no resource action; BREAK=replace confirms a manufactured marker collision is reported loudly (“Live resource displaced from the address it is marked for”, naming the manufactured bucket, proposing nothing for it) rather than silently proposed as nothing - the name-derived-identity shape of this diagnostic, distinct from EC2/SQS’s fungible-set “Two live resources claiming one slot” because aws_s3_bucket’s identity resolves straight from the config’s own computed name rather than only through a marker sweep. Scope note: this exercises OpenTofu’s default destroy-then-create ordering, not the create_before_destroy variant the stage’s Title names - see this section’s own header comment and corpus-ec2-instance-complete’s/corpus-sqs-basic’s matching ones. |
| Crash between create and destroy | not run | ||
| Teardown | not run | ||
| Plan, review, apply | pass | 34s | one argument edited (module.cloudfront_log_bucket gains tags = { Reviewed = “yes” }, reaching aws_s3_bucket.this[0] and nothing else the module creates - that module call sets no attach_*_policy input, so the module’s own policy-document data sources are count = 0 for it and one tag is one row), “plan -out=approved.tfplan” wrote a 85387-byte stock-format plan file whose whole change set is one update on module.cloudfront_log_bucket.aws_s3_bucket.this[0]; the world then moved out of band (s3-bucket-tolerant-unicorn’s transfer-acceleration status flipped to Enabled through the AWS CLI, never through choudoufu - STAGE 5’s own proven mutation, on a DIFFERENT instance and a DIFFERENT live bucket from the one under review) and “apply approved.tfplan” refused with “The approved plan no longer matches the live system” at exit 3, classifying the drift under “This apply would do, and the approved plan does not include:” and naming both module.s3_bucket.aws_s3_bucket_accelerate_configuration.this[0] and the live s3-bucket-tolerant-unicorn it was computed against, with “Exit status 3” spelled out for a pipeline; nothing was applied - get-bucket-tagging on cloudfront-logs-tolerant-unicorn still returned no Reviewed tag, read back through the AWS CLI rather than from the absence of an “Apply complete!” line. Inverted control on the same run (the shape live/smoke/scenarios/apply-what-was-approved.sh reasons out): with the accelerate status put back and nothing else changed, the IDENTICAL file applied - 0 added, 1 changed, 0 destroyed - and cloudfront-logs-tolerant-unicorn read back with Reviewed=yes, so the refusal is earned by the drift and not handed out to every plan file. The edit was then reverted, re-applied and the estate replanned empty, so PART F starts where it would have. BREAK_APPROVAL=1 asserts stage 12’s own recorded Break line (apply the planfile after a mutation and expect success) and correctly fails |
| Greenfield apply | pass | 2m47s | 29 resources from nothing (SCOPE REDUCTION’s own reduced count, random_pet pinned to a literal on both sides), 3 of 4 bucket markers verified via the AWS CLI, 26 records in the local record store (#364 A2), replan empty, stock oracle in its own namespace matches structurally on all 4 buckets (versioning, default encryption, policy presence) |
| Strict profile (not a headline stage) | not run |
Last run at commit 933618dec4 on 2026-09-08T22:17:50Z, exit code 0, against emulator image ghcr.io/lex00/floci@sha256:a39185cc3971d0188663d61043cb038dff1260d8a975b1aa72c4e2bb1feac3cb. Stale: the current pin is ghcr.io/lex00/floci@sha256:d9207de14c919f4bfa50e956376cc441970f3679aabfdd43f3dbf4b779b20805. Total run time 8m34.6s.
Oracle: stock terraform 1.16.1, stock tofu 1.12.6 (matches the current pin).
Landed 1398574b03 (2026-08-18); re-verified for real 2026-08-18 in an isolated worktree against local main bcf78bacbd, full pipeline reproduced clean twice in a row. #306 is fixed (lex00/floci S3ControlController.tagResource now reads-merges-writes; reconciled into the currently-pinned floci image; internal/live/lifecycle/marker_tag_merge_live_test.go’s TestMarkerSurvivesIncrementalTagUpdate passes live against it). DELTA 6 (the tags-removal workaround) reverted - module.s3_bucket’s real tags = { Owner = “Anton” } is back, unmodified, and the stage 2c residue-classification apply leaves all four buckets’ markers intact with it restored. DELTA 5 (expected_bucket_owner) is UNCHANGED, for a reason independent of #306: it is ForceNew on most of these sub-resource types with no live representation at all (a request-header assertion, not a stored property), so a discovery-rebuilt prior would force a real replace on the very first onboarding apply - the same shape as DELTA 3’s random_pet. The prior investigation’s other open question - module.s3_bucket’s canned acl and website.routing_rules - was checked for real rather than assumed fixed, and DOES still block test_plan after #306’s fix, confirmed reproducibly. Traced past “the provider’s Read() needs a genuinely-remembered prior” (the prior investigation’s framing, which attributed this to #306 too - a conflation this session’s trace evidence rules out, since the mechanism is unrelated to tags/floci entirely): issue #275’s residue mechanism (internal/live/projection/residue.go) DOES run here and DOES try to classify acl - TF_LOG=trace on the stage 2c apply shows ‘residue candidate “acl”: readA=cty.StringVal(“private”) readB=cty.StringVal(“private”) applied=cty.StringVal(“private”)’, and classifyResidue’s own documented rule reads that as “the provider answers this from the remote”, so nothing is recorded as residue. Yet the SAME attribute, read moments later by internal/live/projection/build.go’s importAndRead (the function stage 3’s plan actually uses), comes back empty and proposes ‘+ acl = “private”’. The two reads disagree because their prior is built two different ways: residue.go’s identityOnly nulls every non-identity attribute outright, while importAndRead’s prior is whatever provider.ImportResourceState() returned - a zero-value stub (acl = “”, not null) for this SDKv2 resource. The provider’s Read() treats an explicit null differently from an SDKv2 zero-value string for this attribute; reconciling the two prior constructions is real work but importAndRead is the read path every projected resource in this fork goes through, so a fix needs corpus-wide validation, not a one-estate patch - out of scope here. SCOPED OUT instead (module.s3_bucket’s acl and website inputs removed, applied to BOTH the cold-deploy and choudoufu copies so the crossing tests one genuinely-reduced estate throughout, not choudoufu silently abandoning management of something the cold deploy still created) - same discipline corpus-sumaform-aws uses for its own two structural refusals. Estate is now 30 instances across 14 aws_s3_bucket_* types (down from 32/15; aws_s3_bucket_acl survives elsewhere via module.cloudfront_log_bucket’s own grant/owner-driven ACL). Also hit and fixed IN THIS SCRIPT (no choudoufu/floci change needed): THE OUTPUTS QUIRK already documented in corpus-iam-read-only-policy and corpus-root-dns-zones - module.s3_bucket’s outputs, re-exported at root, have no prior baseline across a stateless live-plan, so every run shows a permanent ‘Changes to Outputs’ section and OpenTofu’s renderer never prints a ‘Plan: N to add…’ line while that is true, empty of resource changes or not. Stage 3/5’s empty-plan assertions now check for the absence of a resource action header (‘will be (created|updated|destroyed)’) instead of grepping for ‘No changes.’/‘Plan: 0 to add…’, matching those two scripts’ own pattern; this was never exercised before since stage 3 had never previously reached an actually-empty-of-resource-changes plan. Also fixed the rendered-import-identity count check (N_IDS), which had silently never been exercised either: several of module.s3_bucket’s own sub-resource types (ownership_controls, versioning, policy, logging, cors_configuration, lifecycle_configuration, object_lock_configuration, public_access_block, accelerate_configuration, request_payment_configuration) import by the bucket’s own id alone, so after sort -u only 10 distinct strings remain for 30 instances - the raw (pre-dedup) occurrence count (29) is what actually approximates “one per instance” and is now what the sanity floor checks; the deduplicated set is still what ids_all_name_known_roots validates.
Reproduce it#
go run ./tools/gauntlet run corpus-s3-bucket-completeNeeds Docker (the emulator is pulled at the pinned digest), the AWS CLI, and a
stock terraform or tofu binary on PATH for the cold deploy. The script is
live/e2e/corpus-s3-bucket-complete/run.sh; BREAK=1 corrupts its assertions to show they are load-bearing.