terraform-aws-modules/terraform-aws-iam examples/iam-read-only-policy
Source: https://github.com/terraform-aws-modules/terraform-aws-iam.git at v6.8.0.
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 | 15s | Apply complete! Resources: 1 added, 0 changed, 0 destroyed.; 0 objects carry tofu-estate=iam-read-only-policy-crossing before migration |
| Migrate | pass | 1m4s | 1 of 1 stamped, carrying tofu-slot=0 read back through IAM (choudoufu #372); Apply complete! Resources: 0 added, 0 changed, 0 destroyed. - nothing left to converge |
| Replan from nothing | pass | 2s | no resource change proposed, nothing foreign; identity re-check (via the AWS CLI) unchanged |
| No-op apply | pass | 3s | genuine no-op: 1 objects before, 1 after, no state file either time |
| Drift and reconverge | pass | 5s | one object tampered (arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-c0eaa2d454c87ab752f0d537b2’s Example tag), plan proposed fixing exactly module.read_only_iam_policy.aws_iam_policy.policy[0], apply changed 1 and reconverged the tag |
| Rename | pass | 10s | moved block: module.read_only_iam_policy renamed to module.read_only_iam_policy_moved with zero churn (0 add, 1 change, 0 destroy), tofu-address marker rewritten in place; live-mv: module.read_only_iam_policy_moved renamed to module.read_only_iam_policy_final with zero churn, marker rewritten in place; stock oracle over the identical net rename on cold_deploy’s own state also shows a true no-op (0 add, 0 change, 0 destroy, outputs unchanged in value); the live policy ARN unchanged throughout, read via the AWS CLI |
| Remove a block | pass | 7s | choudoufu: deleting module.read_only_iam_policy_final’s block proposed exactly one destroy (0 add, 0 change, 1 destroy), applied cleanly (0 added, 0 changed, 1 destroyed), the object is genuinely gone from the live account (iam get-policy on the old ARN now returns NoSuchEntity, read via the AWS CLI, not choudoufu’s own report), and the next plan proposes no resource action; classifyOrphans did not withhold the destroy because no other aws_iam_policy.policy block anywhere in this config ever declares a real instance (count=0 on both remaining module calls) |
| Change count | pass | 19s | choudoufu: scaling aws_iam_policy.count_test from 2 to 1 destroyed exactly count_test[1] (0 add, 0 change, 1 destroy), leaving count_test[0]’s live PolicyId and tofu-address marker unchanged; scaling back from 1 to 2 created exactly count_test[1] under the SAME ARN (deterministic from name+path) but a NEW PolicyId (0 add, 0 change -> 1 add, 0 change, 0 destroy) while count_test[0] stayed untouched throughout; the next plan is empty; the G-ORACLE stock oracle on the same 2-instance count block, applied fresh in the idle greenfield-oracle account, shows the identical shape: destroy the higher index only, create the higher index back under the same ARN but a new PolicyId, the lower index’s PolicyId unchanged both times |
| Replace with create_before_destroy | pass | 7s | choudoufu: changing module.read_only_iam_policy_final’s ForceNew description argument proposed exactly one replace at the same declared address (1 add, 0 change, 1 destroy; -/+ destroy and then create), applied cleanly; the old object (arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-c0eaa2d454c87ab752f0d537b2) is confirmed gone and the new object (arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-816ce5c04fc83019c64c689f0d) carries the marker, both via the AWS CLI; the local record store’s record at the same address now names the new object’s import_id, not the destroyed one (arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-c0eaa2d454c87ab752f0d537b2 -> arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-816ce5c04fc83019c64c689f0d); the next plan proposes no resource action; stock oracle on cold_deploy’s own state (F-ORACLE) also proposes exactly one replace at the same address (plan only, not applied); BREAK=replace confirms a manufactured marker collision is reported loudly rather than silently proposed as nothing. 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. |
| Crash between create and destroy | not run | ||
| Teardown | not run | ||
| Plan, review, apply | pass | 19s | one argument edited (aws_iam_policy.approval_probe’s Reviewed tag, no -> yes), “plan -out=approved.tfplan” wrote a 27311-byte stock-format plan file whose whole change set is one update on aws_iam_policy.approval_probe; the world then moved out of band (arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-c0eaa2d454c87ab752f0d537b2’s Example tag, this estate’s own STAGE 5 mutation lifted, through the AWS CLI and never through choudoufu) 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.read_only_iam_policy.aws_iam_policy.policy[0] and the live arn:aws:iam::000000000000:policy/example/ex-iam-read-only-policy-c0eaa2d454c87ab752f0d537b2 it was computed against, with “Exit status 3” spelled out for a pipeline; nothing was applied - arn:aws:iam::000000000000:policy/example/iam-ro-approval-probe still read Reviewed=no 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 tag put back and nothing else changed, the IDENTICAL file applied - 0 added, 1 changed, 0 destroyed - and arn:aws:iam::000000000000:policy/example/iam-ro-approval-probe read back with Reviewed=yes, so the refusal is earned by the drift and not handed out to every plan file. The reviewed object is a self-contained synthetic aws_iam_policy.approval_probe (sanctioned fallback per live/GAUNTLET.md #8, same discipline as PART G’s count_test) because this estate has exactly ONE real object and the leg needs two disjoint rows; it is created in P0 and destroyed in P5, and the module policy’s ARN and Example tag are read back unchanged so PART D 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 | 33s | 1 resource from nothing, marker verified via the AWS CLI, 1 record in the local record store (#364 A2), replan empty, stock oracle in its own namespace matches structurally (path, description, policy document) |
| 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 3m4.6s.
Oracle: stock terraform 1.16.1, stock tofu 1.12.6 (matches the current pin).
Upgraded from a real but pre-#274-pipeline predecessor script (choudoufu apply from a live block present from the start, delete state, replan empty twice) to the current five-stage shape, same upgrade as corpus-iam-policy and following the same corpus-vpc-complete/corpus-lambda-simple structure. Verified for real in a fresh isolated worktree off local main (ff106e63a7), Docker/floci/AWS CLI throughout. All five stages pass cleanly: cold_deploy (plain terraform apply, “Apply complete! Resources: 1 added” - only the first of this module’s three instantiations contributes a resource, confirmed 0 objects tagged before migration), migrate (live-import dry run verifies “1 of 1 resource instance(s) are eligible for stamping”, -approve reports “1 resource(s) newly stamped, 0 already stamped, 0 failed, 0 skipped”, tofu-address=module.read_only_iam_policy.aws_iam_policy.policy:0 read directly through the AWS CLI against the real, server-assigned name IAM minted), test_plan (live-plan genuinely empty, identity re-read unchanged after the state file’s only copy was deleted), test_apply (“0 added, 0 changed, 0 destroyed”, object count unchanged at 1), and drift_reconverge (the one policy’s Example tag tampered directly against floci, live-plan proposes fixing exactly that object by address, apply reconverges to “0 added, 1 changed, 0 destroyed”). Same tofu-slot finding as corpus-iam-policy (see that entry) applies identically here - the module’s aws_iam_policy also declares count = var.create && var.create_policy ? 1 : 0 - and is folded into stage 2 the same way (“0 added, 1 changed, 0 destroyed” before stage 3 is attempted). Genuine constraint found here that corpus-iam-policy does not share: this estate creates exactly ONE real object (the other two module calls contribute nothing), so stage 5’s BREAK=1 cannot prove non-vacuousness by tampering a second object the way corpus-iam-policy’s or corpus-vpc-complete’s can - there isn’t one. Used the address-corruption technique instead (the same shape and resource type, naming a module that in fact creates nothing) and verified it independently at both sites it appears: run as committed it fails stage 3’s identity check; run separately with stage 3’s corruption disabled, it correctly fails stage 5’s exact-address assertion instead. That second, isolated verification also caught a real bug in this script’s first draft: the exact-equality comparison against a live-plan diff header’s address (bracket form, “policy[0]”) was written against the escaped tag-value form (“policy:0”) and could never have matched even on the correct object - fixed by keeping both address forms as separate variables, and the fix was re-verified with a full clean pass afterward.
Reproduce it#
go run ./tools/gauntlet run corpus-iam-read-only-policyNeeds 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-iam-read-only-policy/run.sh; BREAK=1 corrupts its assertions to show they are load-bearing.