terraform-aws-modules/terraform-aws-lambda examples/simple (tag v8.8.1)
Source: https://github.com/terraform-aws-modules/terraform-aws-lambda.git at v8.8.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 | 13s | 8 resources, genuinely cold, genuinely unmarked |
| Migrate | pass | 14s | 3 stamped, 4 recorded, 0 failed, 1 skipped |
| Replan from nothing | pass | 2s | no resource change proposed |
| No-op apply | pass | 5s | no-op apply (0 added, 0 changed, 0 destroyed); tofu-estate-tagged object count unchanged at 3; markers and record store intact |
| Drift and reconverge | pass | 19s | one object tampered (memory_size 128->256), exactly module.lambda_function.aws_lambda_function.this[0] proposed by both choudoufu and stock with the identical change, apply changed 1 and memory_size reads back as 128 |
| Rename | pass | 25s | moved block: module.lambda_function renamed to module.lambda_function_moved with zero churn (0 add, 3 change, 0 destroy) across all seven of its stateful children, three taggable markers rewritten in place, three record-located children moved via their own per-resource moved blocks with zero diff, one config-derived child (aws_iam_role_policy.logs) needing none; stock oracle over the identical seven-resource move on cold_deploy’s own state also shows zero churn beyond the module’s own pre-existing null_resource.archive[0] package-timestamp noise (confirmed present on an unrelated baseline replan too); live-mv: module.lambda_function_moved renamed to module.lambda_function_final across all three taggable children (the function, the role, the log group), one call each, zero churn, markers rewritten in place - the internal/live/mv/mv.go materialize() RecordStore wiring gap (build.go:1676’s “Record-backed instance with no record store”) is fixed; all three live objects unchanged throughout, read via the AWS CLI; final replan is empty |
| Remove a block | pass | 12s | choudoufu: deleting module.lambda_function_final’s block proposed 7 destroys (the function, the role, its inline aws_iam_role_policy.logs[0] CloudWatch Logs policy, and all three record-located children always; the log group’s only when floci’s GetResources happens to index it - a documented emulator gap, confirmed by reading logs:list-tags-for-resource directly against the same live object), applied cleanly, the function, the role and the inline log policy genuinely gone from the live account (read via the AWS CLI, not choudoufu’s own report), and the next plan proposes no further resource action; classifyOrphans did not withhold any destroy as a possible rename. WANT_DESTROY_COUNT moved from 5/6 to 6/7 in this same commit: the inline log policy was previously missing from this stage’s own checklist entirely - a genuine leak (an untaggable IAM permission left behind on every destroy of this estate), not a stale assertion, fixed as part of the day2_replace unit that re-measured this stage |
| Change count | pass | 27s | synthetic count block, and the header says why: .corpus/lambda declares 38 count/for_each expressions and not one is scalable - 36 are boolean create toggles (count = … ? 1 : 0), the for_each maps are empty in this example, and the only two numeric ones (number_of_policies, number_of_policy_jsons) are gated off by default and drive the untaggable aws_iam_role_policy_attachment, so turning either on would both change the estate every earlier stage measured and leave no marker to read back. So aws_iam_role.count_test (count = 2), a self-contained block of a type this estate already exercises, added after Part E’s completed removal. choudoufu: scaling 2 -> 1 proposed exactly 0 add, 0 change, 1 destroy, destroying count_test[1] and never naming count_test[0]; after the apply lambda-simple-count-test-1 no longer answers GetRole at all (verified absence) while count_test[0] kept its server-minted RoleId AROA2ENGPG5QBV97X3TZ and its tofu-address=aws_iam_role.count_test:0 marker, both read through the AWS CLI. Scaling 1 -> 2 proposed exactly 1 add, 0 change, 0 destroy and count_test[1] came back as a genuinely NEW object - RoleId AROAVOM62TQCT6T7PNMQ -> AROAJM7H1AYUKB7LV9Z5 under the same deterministic name, the witness an IAM role’s ARN cannot provide since it is rebuilt from account plus name (the same reason this estate’s own Lambda function ARN could not have witnessed it) - carrying tofu-address=aws_iam_role.count_test:1, with count_test[0] untouched throughout; the next plan is empty. Stock oracle (G0): the identical block stood up for real with plain terraform in its own working directory on the idle greenfield-oracle endpoint shows the identical shape - destroy the higher index only (0 add, 0 change, 1 destroy), create the higher index back under a new RoleId (AROAYU2CIN7JC3CE5OF8 -> AROAARZJM95N0B4KHL7R), the lower index’s RoleId (AROAJ6P6BPUF59UL1JKF) unchanged both times. BREAK_COUNT=1 asserts the WRONG instance (count_test[0]) was destroyed and reports this stage fail, so the assertion is load-bearing |
| Replace with create_before_destroy | pass | 18s | choudoufu: changing module.lambda_function_final’s ForceNew logging_log_group-derived name proposed exactly one replace at the same declared address (the log group; -/+ destroy and then create) cascading into two expected in-place updates (the function’s logging_config, the inline log policy’s document) and nothing else beyond the module’s own pre-existing null_resource.archive[0] package-timestamp noise; applied cleanly; the old object (arn:aws:logs:eu-west-1:000000000000:log-group:/aws/lambda/exact-moray-lambda-simple) is confirmed gone and the new object (arn:aws:logs:eu-west-1:000000000000:log-group:/aws/lambda/exact-moray-lambda-simple-v2) 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 (/aws/lambda/exact-moray-lambda-simple -> /aws/lambda/exact-moray-lambda-simple-v2); the next plan proposes no resource action beyond the same known noise; stock oracle on cold_deploy’s own state (F-ORACLE) also proposes exactly one replace plus the same in-place cascade (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 | 14s | one argument edited (module.lambda_function’s cloudwatch_logs_retention_in_days, unset -> 14, which reaches module.lambda_function.aws_cloudwatch_log_group.lambda[0] and nothing else - the module call carries no tags argument and every tags-shaped knob it has would reach three children at once), “plan -out=approved.tfplan” wrote a 31817-byte stock-format plan file whose whole change set is that one in-place update; the world then moved out of band (exact-moray-lambda-simple’s memory_size 128->256, 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 the extra row as " module.lambda_function.aws_lambda_function.this[0] Update exact-moray-lambda-simple" - both module.lambda_function.aws_lambda_function.this[0] and the live identity it was computed against - with “Exit status 3” spelled out for a pipeline; nothing was applied - /aws/lambda/exact-moray-lambda-simple still carried no retentionInDays, 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 memory_size put back and nothing else changed, the IDENTICAL file applied - 0 added, 1 changed, 0 destroyed - and the log group read back with retentionInDays=14, so the refusal is earned by the drift and not handed out to every plan file. Reverted and reconverged in P5 (retention unset again, memory_size still 128, next plan proposes no resource action, no state file left behind) 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 | 25s | 8 resources from nothing (3 taggable + 5 record-backed/config-derived), all three module-nested markers verified via the AWS CLI, 8 records in the local record store (#364 A2), replan empty, stock oracle in its own namespace matches structurally (runtime, handler, memory, timeout, log-group retention) |
| 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 2m54.9s.
Oracle: stock terraform 1.16.1, stock tofu 1.12.6 (matches the current pin).
Re-verified 2026-08-18 in a fresh isolated worktree off local main (bcf78bacbd), for real (Docker/floci/AWS CLI, not read from a prior note). Stages 1 and 2 still pass exactly as landed: cold apply creates 8 resources, live-import verifies ‘3 of 8 resource instance(s) are eligible for stamping’, -approve reports ‘3 resource(s) newly stamped, 0 already stamped, 0 failed, 5 skipped’, and module.lambda_function.{aws_lambda_function,aws_iam_role,aws_cloudwatch_log_group} carry the expected module-qualified tofu-address/tofu-estate tags read straight through the AWS CLI. #303 (the count=var.enable_x?1:0 zero-instance admission gap on aws_lambda_function_url.this and aws_lambda_function_recursion_config.this) is CONFIRMED FIXED: re-running live-plan against current main, neither type appears anywhere in the diagnostics any more, as an error or a warning - stage 3 now fails on exactly one Error block, not two-plus. That block is local_file.archive_plan (module.lambda_function’s package.tf:44, count = var.create && var.create_package ? 1 : 0, both true by default in this example so a real non-zero instance, not a zero-count block #303’s fix would clear), refused under the logical-resource rule. Investigated whether this is a bug or correct behavior: it is correct, deliberate, and already ruled on. Issues #237 and #238 (both closed 2026-08-18) put local_file through exactly this question and #238’s closing comment states local_file is ‘deliberately left OTHER_REFUSED with a documented reason: neither of lint’s two classes fits it correctly (its identity is argument-derived, not record-backed, and promoting it would silently reopen a count.index collision hazard a dedicated test already guards) - a genuine third-classification gap, not an omission, correctly left open rather than forced.’ local_file’s identity is a filename on the local disk of whatever machine ran apply - not a cloud object, nothing taggable, nothing an AWS CLI call could ever read back to confirm it still exists - so there is no live counterpart for a stateless replan to reconcile against. Considered scoping the estate around it the way corpus-vpc-complete/corpus-sumaform-aws scope around their own out-of-scope resources (the module’s create_package=false + local_existing_package=output blocks render as + new on every run. Root cause: internal/live/projection.Manager.GetRootOutputValues always returns an empty map - nothing evaluates the config’s own output blocks against the reconstructed prior state before the plan graph asks for it. A genuinely new, generalizable gap (neither corpus-mastino-dns nor corpus-evoteum-modules, the two other closest-to-5/5 crossings, declares any root-level output, so nothing had hit this before). Filed as INTENTIUS/choudoufu#348, not attempted - core projection-architecture work, not a quick derivation. Stages remain 2 of 5; the estate’s real blocker moved from floci#83 to #348, which is now the sole thing standing between this estate and an empty stage-3 plan. Follow-up pass 2026-08-20 (primary checkout, local main db1f412cfd, real Docker/floci run - GitHub issue #340 verification): #340 was found ALREADY FIXED on main (d30daa156f/8d34e3ded9, confirmed by commit history and by internal/live/liveimport/record_test.go’s TestApprove_SeedsTheRecordStoreForARecordBackedInstance/TestRecordBackedTypeReadsTheGeneratedTable, the latter covering random_pet/null_resource/terraform_data/local_file/time_sleep/random_id generically), so this pass re-verified rather than re-fixed. Re-crossing confirms #340’s own fix by absence again: ’no record-backed resource is proposed for creation: the migrate seeded all four’ and ’no sensitivity-only diff on local_file.archive_plan: the record carries its marks’ both print in stage 3’s own output. #349 (‘see through provably-zero-instance blocks when evaluating root outputs’, 88d7e3961e, landed after this note’s #348 paragraph) cut the output-only diff from all 23 root outputs to exactly 2: ’lambda_function_arn_static’ and ’local_filename’, both still ‘+’ on every run. Stage 3 (test_plan) is still BLOCKED - not yet empty - but the wall is now two output lines, not twenty-three, and #340 itself contributes zero diagnostics and zero sites to what remains. Stages unchanged at 2 of 5 pass; the residual 2-output gap is #348/#349’s remaining scope, not #340’s, and was not investigated further here (out of this issue’s scope). Follow-up pass 2026-08-21 (isolated worktree off local main 860c29e129, real Docker/floci/AWS CLI run, identical harness run TWICE with only TOFU_BIN swapped - not read from any prior note): #349’s sub-problem 2, the root-output data-source read, is now built, and this estate’s stage-3 output diff went from 2 lines to 1. Measured: at 860c29e129 the plan’s ‘Changes to Outputs:’ block carries ’lambda_function_arn_static’ and ’local_filename’; with the fix it carries ’local_filename’ alone. lambda_function_arn_static vanishes from the diff entirely rather than rendering as ‘~ old -> new’, which is the stronger result: the plan graph independently computed the same value the pre-plan read computed for the prior side, so they cancel. The three data sources behind it (data.aws_partition.current, data.aws_region.current and data.aws_caller_identity.current, all in module.lambda_function) are read live before the plan through the same configured aws provider instance the projection already reads this estate through. local_filename is UNCHANGED and stays refused ON PURPOSE: it reaches data.external.archive_prepare, whose read runs package.py on the machine running the plan, and the new demand class is confined to providers this configuration manages live objects through (dataread.LiveProviders) - the external provider serves no managed resource type at all, in this or any configuration, so it is excluded structurally rather than by name. Stages are UNCHANGED at 2 of 5: cold_deploy pass, migrate pass, test_plan still FAIL (one output line is still one output line, so the plan is still not empty), test_apply and drift_reconverge still not reached. This narrowed the stage-3 diagnostic count; it did not clear the stage. Follow-up pass 2026-08-29 (isolated worktree off local main 499f9f5e80, real Docker/floci/AWS CLI run - GitHub issue #498, ‘migrate pass and fail 23 minutes apart at the same emulator pin’): CONFIRMED as a real, deterministic defect, not a flake and not #497’s runner-resource-pressure hypothesis. Reproduced 100% of the time under a controlled variable rather than by chance: migrate PASSED 7/7 consecutive local runs (this machine’s stock terraform, v1.15.8) with byte-identical output every time, then FAILED 2/2 runs the instant HashiCorp Terraform v1.16.0 was forced first on PATH for stage 1’s cold deploy, with the exact nightly failure text (’live-import -approve did not stamp 3 and record 4 of 8 resources cleanly’). Root cause, isolated to one byte: Terraform >=1.16.0’s built-in terraform_data resource gained a new ‘store’ nested block (verified directly via terraform providers schema -json against terraform.io/builtin/terraform, no choudoufu involved) that choudoufu’s own terraform_data schema (internal/builtin/providers/tf/resource_data.go, unchanged since the OpenTofu fork) does not declare; decoding a stock-terraform-1.16-produced cold.tfstate’s terraform_data instance against that older schema failed with ‘unsupported attribute “store”’ (internal/live/liveimport/ratify.go’s ratifyRecordBacked, its inst.Current.Decode(schema.Block.ImpliedType()) call), demoting terraform_data.package_filename_for_hash from RECORDED to SKIPPED and changing live-import -approve’s summary line, which trips run.sh’s exact-string assertion. The ‘pass at 10:23:19Z, fail at 10:46:59Z, same commit’ shape in #498 was never nondeterminism in the same environment: the passing row was a local worker’s run against an older pinned-by-brew terraform (like this pass’s own baseline), the failing row was CI’s hashicorp/setup-terraform@v3 with terraform_version: latest (confirmed 1.16.0 from the nightly’s own log) - two different environments measuring the identical commit near-simultaneously, not one environment flip-flopping. Downloaded the eks-basic sibling failure from the same nightly run for comparison and confirmed it is a DIFFERENT failure (a runner tofu-on-PATH casualty per #497, mis-attributed to whatever CURRENT_STAGE was set to when it died) - #498’s own caveat about #497 does not reach this estate’s failure, which is fully explained by the terraform_data schema gap alone. Fixed generically for the one type it can reach (dataStoreResourceSchema() now declares ‘store’ as a NestedType Object attribute mirroring the real schema’s own field shape and WriteOnly/Sensitive flags, verified by decoding the real terraform-1.16.0-produced state); this is the resource’s own implementation file, not classification control flow, so it carries no live/derivation_guard_test.go entry. ctyjson.Unmarshal was independently verified (a standalone throwaway program, not assumed) to default a type attribute missing from raw JSON to null, so old choudoufu-written terraform_data state predating this field decodes unaffected - TestManagedDataUpgradeStateMissingStore pins that boundary directly. Re-crossing after the fix: migrate PASSES under terraform 1.16.0 too (2/2), with the correct ‘3 stamped, 4 recorded, 0 failed, 1 skipped’ split restored, and the whole estate clears end to end (exit 0) under both terraform versions - no other stage regressed. Not verified: HashiCorp Terraform versions between 1.15.8 and 1.16.0 (whichever one first introduced ‘store’) and any version after 1.16.0 that might change the block’s shape again; the fix targets the exact field shape read off 1.16.0 and would need re-verification against a materially different future schema.
Reproduce it#
go run ./tools/gauntlet run corpus-lambda-simpleNeeds 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-lambda-simple/run.sh; BREAK=1 corrupts its assertions to show they are load-bearing.