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
Not clear yet.
| Stage | Verdict | Detail |
|---|
| Cold deploy | pass | 8 resources, genuinely cold, genuinely unmarked |
| Migrate | pass | 3 stamped, 4 recorded, 0 failed, 1 skipped |
| Replan from nothing | pass | no resource change proposed |
| No-op apply | pass | 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 | 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 | 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 | FAIL | choudoufu’s remove plan proposes 7 destroys, expected exactly 6 |
| Change count (planned) | not run | |
| Replace with create_before_destroy | not run | |
| Crash between create and destroy (planned) | not run | |
| Teardown (planned) | not run | |
| Plan, review, apply (planned) | not run | |
| Greenfield apply | pass | 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 (planned) | not run | |
Last run at commit db15df773b on 2026-08-25T17:47:34Z, exit code 1.
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= toggle skips package.tf’s local_file entirely) and rejected it: unlike sumaform’s provision=false, which picks between the module’s own equally-real published deployment modes to route around an infra-emulation gap in floci, swapping to a pre-built zip would replace the actual thing ‘simple’ demonstrates - the module’s own default packaging pipeline - with a materially different scenario this corpus entry was never meant to test. Left as a real, reported block; run.sh’s header carries the full investigation. One piece of relevant good news found along the way: #275 (closed 2026-08-18) built a record_store-gated residue mechanism for exactly the aws_lambda_function.filename/source_code_hash/publish phantom-diff problem a filename-deployed Lambda would otherwise hit under stateless replanning - this estate already declares a record_store, so once local_file gets its own identity class nothing here looks likely to re-hit that problem. test_apply and drift_reconverge remain not_run because test_plan does not pass; not attempted this pass since attempting them against a still-refused plan would prove nothing. No issue currently tracks the missing ‘argument-derived-but-safe’ LogicalClass itself (the actual unblock for this estate) - #237/#238 are both closed and did not spawn a follow-up; one may be worth filing if this crossing is prioritized again. Follow-up pass 2026-08-18 (#313 cross-check, fresh worktree off main at d303d9d425, real Docker/floci/AWS CLI run): confirmed the local_file.archive_plan block above is NOT #313’s data.aws_availability_zones/static-context wall - grepped the full live-plan output, zero occurrences of that diagnostic. Filed the missing-LogicalClass follow-up this note flagged as worth filing but hadn’t been: #314 (’local_file needs a fourth LogicalClass (argument-derived identity)’), citing #237/#238’s rulings and the count-index guard test. No fix attempted - real multi-package work (lint + identity resolution + row-gen), not a quick derivation. Follow-up pass 2026-08-19 (#314 fixed and merged, d878aa914b/e7bb1f4b41): the fourth LogicalClass exists, but not the one the issue’s own framing predicted - two of that framing’s premises turned out false, checked rather than assumed. hashicorp/local’s provider implements NO ImportState for local_file at all (confirmed against stock tofu: ‘This resource does not support import’), so an argument-derived identity handed to internal/live/projection would have turned a lint refusal into a hard Cannot-import-for-projection error - strictly worse. And this estate’s own filename argument isn’t static anyway (it reads a data.external result). The real fix is ClassExternalAdmitted/EXTERNAL_ADMITTED: a record_store admits local_file the same way it admits ClassRecordAdmitted, resolving through ClassRecordBacked - the record is the only carrier that can bring prior state back for a type the provider itself cannot re-derive. Reaches exactly one type (local_file; local_sensitive_file is secret-bearing) but the RULE generalizes: live/logical-schemas.json’s per-provider store_only now SELECTS between the two admitted classes instead of gating whether a type derives a row at all, which also retired the hand-written local_sensitive_file exception in ClassifyLogicalType - a net type-name-literal deletion, not an addition. Count-index guard (TestLocalFileKeepsItsCountIndexCheck) confirmed still holding via two separate mutation checks. Real re-crossing: local_file is admitted and appears nowhere in live-plan’s diagnostics any more (asserted by absence) - but test_plan stays fail, now BLOCKED at 5 sites, a FOURTH wall newly reached rather than caused. All five trace to one expression, function_name = “${random_pet.this.id}-lambda-simple”: random_pet.this is RECORD_ADMITTED so its id lives only in the record store, and the identity resolver declines to read that carrier for the three dependent resources (aws_iam_role.lambda, aws_iam_role_policy.logs, aws_lambda_function.this, aws_cloudwatch_log_group.lambda, one cascade) even though all three are already stamped and CLI-verified by stage 2 of the same run - choudoufu already holds the value and the objects are already marked, so this reads as an identity-resolver gap rather than a missing carrier. Not filed (no issue number assigned) - worth a slot. test_apply/drift_reconverge remain not_run, blocked on this new wall. Two real corrections made along the way: the crossing script had no AWS provider version pin (silently drifted to whatever the newest release was, now pinned =6.59.0 matching corpus-cloudfront’s discipline), and live/LIMITATIONS.md’s local-file section stated ’no cloud counterpart to reconcile against’ as fact - false; the local filesystem is the counterpart, now corrected there too. Follow-up pass 2026-08-19/20 (#336 fixed and merged, 821c769715/c41279989a): #336’s own diagnosis was wrong in two places, checked rather than assumed. The identity resolver was NOT declining to read the record-store carrier - resolver.parentPart already read random_pet.this.id correctly on unmodified main. What actually refused was coalesce(): iam.tf/main.tf’s role_name/policy_name/log-group-name chains all select through coalesce(var.X, var.Y, “*”)-shaped expressions, and resolver.isSymbolic reads only an expression’s traversal ROOTS - var/local are never symbolic, so a selection sitting behind a module argument or a local looked entirely static, failed whole-expression evaluation, and had nothing left to try (the decomposition switch is only reached when a resource is named directly inside the expression). Fixed generically (internal/live/identity/coalesce.go, new): resolveCoalesceCall decides which argument the language selects using two proofs (provably-null-or-empty to skip, provably-non-null-non-empty to select), declining the whole call on anything undecidable rather than silently falling through - mutation-tested three ways (drop the non-emptiness proof, fall through on undecidable, remove the call entirely), each caught. Measured reach: refusal-probe sites 16075->15964 (-111), instances 4499->4522 (+23), 12 entries improved across six unrelated sources, 0 worse - schema-less mode, an under-report since it’s blind to the record-backed half of this estate’s own chain. Real re-crossing: live-plan diagnostics 5->0, the plan runs to completion for the FIRST time - but test_plan still FAILS, now on a genuinely NEW, fifth wall: ’live-plan is not empty’, proposing to create every record-backed resource (random_pet.this first) from scratch. Root cause, and #336’s second wrong premise: live-import’s Approve loop only calls #327’s recordResidueFor for a STAMPED entry; a record-backed resource is by definition not stampable (no live cloud object to tag) and hits OutcomeSkipped, continuing past the residue call entirely - so the record store is empty for every record-backed instance after a clean migrate, not populated as #336 assumed. Filed as #340, not attempted - the fix is migrate seeding the record store from the migrated state’s own object for every record-backed instance, a sibling call to recordResidueFor on the skipped-because-record-backed path. test_apply/drift_reconverge remain not_run, now blocked on #340 instead of #336’s five diagnostics. Follow-up pass 2026-08-20 (#340 fixed and merged, d30daa156f/8d34e3ded9): the issue’s own framing was half right - recordResidueFor does NOT gate on ‘stamped’, it runs for any entry with an eligible; the real gate is one line earlier, Ratify never building an eligible for a record-backed type at all. Fixed with Approve’s second write path, the sibling of the tag write: projection.SeedRecordForInstance writes a record-backed instance’s object into the record store, byte-identical to what an apply’s WriteBack would write, reading before writing so an already-correct record is a no-op and a genuinely different one refuses rather than clobbers. Keys on identity.TypeIdentity.RecordBacked and nothing else - 15 types across 4 providers (local/null/random/time/terraform_data), no aws_/random_ name in the control flow. Real re-crossing: STAGE 2 migrate reports ‘3 newly stamped, 0 already stamped, 4 newly recorded, 0 already recorded, 0 failed, 1 skipped’, the store’s own files grepped for random_pet.this’s generated id. STAGE 3: live-plan now raises ZERO diagnostics for the first time ever on this estate, every identity resolves, no record-backed resource is proposed for creation - but the plan is not empty: 0 to add, 2 to change, 0 to destroy, a SIXTH wall. Both changes are real and distinct from every prior wall: (1) a nested-block round-trip on aws_lambda_function (- environment {} / + logging_config { log_format = “Text” }, floci’s Lambda read vs the module’s config), (2) a sensitivity-only diff on local_file.content (OpenTofu’s own renderer says ‘The value is unchanged’ - a genuine limitation the fixing agent found and pinned rather than hid: ResourceInstanceObjectSrc.Decode re-applies AttrSensitivePaths so the decoded value is marked, ctyjson.Marshal panics on a marked leaf, the fix unmarks before encoding, and projection.recordPayload has nowhere to store the sensitivity path - so the record carries the value but not the mark. projection.WriteBack shares this same hole, worse (no unmark of its own, would panic on the identical object after a real apply) - unfiled, not touched, worth a slot). test_apply/drift_reconverge remain not_run, blocked on this sixth wall now. Separately, #341 (found by the sibling corpus-mastino-dns crossing, same ratify.go gate but a different population - untaggable ordinary AWS types like aws_route53_record, architecturally guaranteed never to be RecordBacked per TestResolveNeverEmitsRecordBackedForAWSEstate) is CONFIRMED STILL OPEN after #340 - the two issues share a root-cause line but #340’s fix is deliberately scoped to the disjoint RecordBacked population and does not reach it. #340’s own recordable/Approve sibling-carrier pattern is flagged as a reasonable template for #341’s eventual fix. Follow-up pass 2026-08-20 (worktree ../wt/lambda-simple-sixth-wall, branch live/lambda-simple-sixth-wall, off LOCAL main ea9fd62fc0): the sixth wall was REPRODUCED for real first, not read off this note - STAGE 1 PASS, STAGE 2 PASS, STAGE 3 with zero diagnostics and ‘Plan: 0 to add, 2 to change, 0 to destroy’, both changes verbatim as recorded above. Stages are UNCHANGED at 2 of 5, and the reason is now measured rather than argued: the two changes belong to two different projects. (a) local_file.archive_plan’s sensitivity-only diff IS choudoufu’s and is FIXED (f66bc9e043). projection.recordPayload gained SensitiveAttrs, encoded exactly the way a state file encodes sensitive_attributes: encodeRecordPayload splits the value from its marks itself so no caller unmarks, decodeRecordPayload puts them back the way ResourceInstanceObjectSrc.Decode does, and materializeRecord re-marks after the schema conversion so obj.Encode derives AttrSensitivePaths from them. The mechanism the wall turned on is that live-plan runs the plan graph with SkipRefresh (live_plan.go:499), so a projected object’s AttrSensitivePaths is the ONLY marks the plan’s ‘before’ side ever has - upstream re-marks a refreshed object at node_resource_abstract_instance.go:1106 and that line is never reached - while the ‘after’ side is re-marked from the config and the provider schema every run at :1383. Derived from the object’s own marks and nothing else, so it reaches any record-backed type with any sensitive attribute at any path; a mark that is not marks.Sensitive is refused rather than dropped. Four mutation checks, each caught, and the shape test consults an EXTERNAL source (it writes a real state file through internal/states/statefile and requires the same JSON for the same paths) rather than round-tripping against itself. TestIdentityGolden 0 changed, 0 added, 0 removed; ./internal/live/…, ./tools/…, ./live/…, ./cmd/… and ./internal/command/ all green. (b) aws_lambda_function’s ‘- environment {}’ / ‘+ logging_config { log_format = “Text” }’ pair is NOT choudoufu’s, and this is now proven by a CONTROL rather than reasoned about: run.sh’s new step 3b runs plain terraform, its own state file, its own refresh, ’terraform plan -detailed-exitcode’ immediately after its own cold apply with no choudoufu anywhere in the run, and it replans NOT EMPTY on exactly module.lambda_function.aws_lambda_function.this[0] and nothing else. Asserted by value, so a new emulator gap breaks the script instead of hiding in a bucket labelled expected, and a fixed one breaks it too. Filed as lex00/floci#83 with both causes located in LambdaController.buildFunctionConfiguration: Environment is emitted unconditionally (‘SDK expects it even when empty’) where real AWS omits it for a function that never had one - which is why terraform-provider-aws reads it under ‘if function.Environment != nil’ - and LoggingConfig is neither stored nor emitted at all, where real AWS always returns one defaulting to Text. The module declares zero environment blocks (main.tf:90, a dynamic block over an empty map) and one logging_config unconditionally (main.tf:136), so both fire on the module’s DEFAULTS. Stage 3 now splits its own plan against the step-3b control with comm and names only the remainder as choudoufu’s. WHAT IS NOT VERIFIED, stated rather than implied: the post-fix crossing was started and its choudoufu init was killed by the harness before stage 2, so the sensitivity fix has NOT been observed clearing the diff in a real end-to-end run - it is verified by unit tests that drive the two real paths (WriteBack after an apply, then BuildWith/materializeRecord on the next plan) and assert inst.Current.AttrSensitivePaths by value. The next run of this script is what settles it, and it should still end at ’live-plan is not empty’ with the Lambda alone until lex00/floci#83 lands. Two further issues filed from this pass, neither fixed: #343 (builder.materialize applies no schema.Block.ValueMarks to what a provider Read returned, so the identical perpetual diff exists for any CONCRETE cloud object with a Sensitive attribute - separate because it also changes what b.live means for identity composition, and unmeasured over the corpus) and #344 (a record written before SensitiveAttrs existed now conflicts with its own re-migration though the value is identical, because SeedRecordForInstance compares bytes; population is local_file plus any config-derived mark, and the format is one day old). Stages 4 and 5 remain unwritten: an empty stage-3 plan is their precondition and lex00/floci#83 is what stands between this estate and one. Follow-up pass 2026-08-20 (worktree ../wt/lambda-simple-floci83, branch live/lambda-simple-floci83): lex00/floci#83 is FIXED and CLOSED, settled against real AWS (one throwaway Lambda function + IAM role created and immediately deleted, disclosed to the user per standing permission) rather than assumed - GetFunction/GetFunctionConfiguration omits Environment entirely for a function that never had one (not present-but-empty) and always returns LoggingConfig, defaulting to Text. Fixed in LambdaController/LambdaService/LambdaFunction, verified three ways (local instance, direct probe, and the same probe against the published GHCR image). Re-crossing after the re-pin: STAGE 3 (test_plan) now raises zero diagnostics and proposes changing ZERO resources for the first time ever on this estate - both prior sixth-wall diffs (the sensitivity-only local_file.content diff and the aws_lambda_function nested-block mismatch) are gone, verified by reading the raw terraform plan output directly rather than trusting extracted variables. But the plan is still not empty: all 23 of the example module’s root-level 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.
Reproduce it#
go run ./tools/gauntlet run corpus-lambda-simple
Needs 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.