terraform-aws-modules/terraform-aws-ecs examples/fargate (tag v7.6.0), its Fargate flagship example

Source: https://github.com/terraform-aws-modules/terraform-aws-ecs.git at v7.6.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.

StageVerdictDurationDetail
Cold deploypass1m27s62 resources, once for real
Migratepass1m27s46 of 62 stamped
Replan from nothingpass29sgenuinely empty replan (“No changes. Your infrastructure matches the configuration.”) - #371, #378, #372, #110, #395 and #376 all fixed and stay fixed; the standalone task definition’s essential/mountPoints[].readOnly wall is gone (lex00/floci#131, published and repinned this unit) and essential defaulting to true was never an independent wall on its own (this unit’s own re-measurement). #395/#376: choudoufu keeps no persisted state, so every plan re-derives PriorState through ImportResourceState’s bare stub; internal/live/projection/build.go’s configuredAttrsSeed generalizes the tags-only import-stub seed (issue #287 item 8) to every Required-or-Optional-non-Computed attribute (fixing #376’s track_latest/skip_destroy directly), and internal/live/projection/residue.go’s residueConfigSourced widening of classifyResidue plus the new builder.residueSeedFor pre-read seed close #395’s managed-reference case (task_definition = aws_ecs_task_definition.this[0].arn) that configuredAttrsSeed’s static evaluator alone could not reach. Identities confirmed by value against the AWS CLI: $CLUSTER_ARN, $TD_SVC_ARN, $TD_STANDALONE_ARN, and #368’s scalable target $GOT_TARGET_RID.
No-op applypass6sgenuine no-op (0 added, 0 changed, 0 destroyed); 46 tofu-estate-tagged objects before, 46 after, no state file either time
Drift and reconvergepass11sone object tampered (VPC’s Name tag), plan proposed fixing exactly module.vpc.aws_vpc.this[0], apply changed 1 and the Name tag reconverged
Renamepass52smoved block: module.alb renamed with zero churn (0 add, 9 change, 0 destroy), marker rewritten in place; live-mv: aws_service_discovery_http_namespace.this renamed with zero churn, marker rewritten in place; stock oracle over the same two-object rename on cold_deploy’s own state also shows zero churn (0 add, 0 change, 0 destroy); both live ids unchanged, read via the AWS CLI
Remove a blockpass16schoudoufu: deleting module.ecs_task_definition’s block proposed exactly 8 destroys (0 add, 0 change, 8 destroy), address-for-address identical to stock’s oracle on cold_deploy’s own state; applied cleanly (0 added, 0 changed, 8 destroyed); the standalone task definition family (ex-fargate-standalone) genuinely has 0 active revisions afterward, read via the AWS CLI, not choudoufu’s own report; classifyOrphans did not withhold any destroy because no other module.ecs_task_definition block is declared anywhere in this config; the next plan is empty
Change countpass1m5schoudoufu: scaling aws_security_group.count_test from 2 to 1 destroyed exactly count_test[1] (Plan: 0 to add, 0 to change, 1 to destroy.), leaving count_test[0]’s live GroupId (sg-1c4e5ff12409b43f4), its tofu-address (aws_security_group.count_test:0) and its tofu-slot (0) unchanged, and count_test[1] (sg-009cfe307f6b6303d) genuinely absent - all read through the AWS CLI, never choudoufu’s own report; scaling back from 1 to 2 created exactly count_test[1] (Plan: 1 to add, 0 to change, 0 to destroy.) under a NEW server-minted GroupId (sg-2f7f715f5f6ad9cbf, was sg-009cfe307f6b6303d) carrying tofu-address=aws_security_group.count_test:1 and the same tofu-slot=1, while count_test[0] stayed untouched throughout; the next plan is empty. Stock oracle (H-ORACLE), the identical 2-instance block stood up for real with plain terraform in its own account and its own working directory - stock never had this block, so unlike day2_rename/day2_remove/day2_replace there is no cold_deploy state to reuse - shows the identical shape: Plan: 0 to add, 0 to change, 1 to destroy. destroying count_test[1]=sg-ae5f56321223d9800 only, then Plan: 1 to add, 0 to change, 0 to destroy. bringing it back under a new GroupId (sg-b59f43bdd5a35f1fa), with count_test[0]=sg-2895fe52bed4f2550 unchanged both times. SYNTHETIC block, and why: this estate’s corpus example (terraform-aws-modules/terraform-aws-ecs v7.6.0, examples/fargate) declares no scalable count knob at all - every count in its vendored modules is a ‘count = local.create ? 1 : 0’ boolean create toggle, which cannot hold two instances - and the only construct holding three, module.vpc’s private_subnets, is driven by local.azs, which the ECS service’s network configuration, the ALB and the NAT gateway all read, so scaling it churns a dozen unrelated resources instead of isolating which count instance a scale-down destroys. aws_security_group was chosen because this estate already exercises it three times in its own vendored modules (modules/cluster, modules/service, modules/express-service each declare aws_security_group.this) and because its destroy is witnessed TWICE on the pinned emulator - a new GroupId AND verified absence in between - established by reading the EC2 API directly with no tofu in the loop before any assertion here was written. Placed last, after every other stage reported, at an address nothing else in this configuration names, so no earlier stage’s assertions move. BREAK_COUNT=1 asserts the WRONG instance (count_test[0]) was destroyed and correctly fails.
Replace with create_before_destroypass17schoudoufu: changing module.ecs_task_definition’s ForceNew name argument (module CALL, passed through to the local module’s own family = coalesce(var.family, var.name)) proposed a forced replace at the same declared address (Plan: 8 to add, 0 to change, 8 to destroy.), applied cleanly; the old task definition is confirmed INACTIVE via the AWS CLI (ECS deregisters rather than deletes) and the new one (arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone-v2:1) carries the marker, moved via the tofu-address tag (arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone:1 -> arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone-v2:1); the next plan proposes no resource action; stock oracle on cold_deploy’s own state (F-ORACLE) also proposes replacing the task definition at the same address (Plan: 8 to add, 0 to change, 8 to destroy., plan only, not applied - it shares floci’s account with $ADOPTED_EST); BREAK=replace confirms a manufactured marker collision - a second, genuinely live task definition wearing this address’s marker, which no tombstone names as destroyed - is still reported loudly (“Indistinguishable instances without per-instance markers”, naming both ARNs) rather than pruned or silently proposed as nothing, which is #849’s own rule holding on this route too. Scope note: this exercises OpenTofu’s default destroy-then-create ordering, not the create_before_destroy variant the stage’s Title names; the local record store is read by value on both sides of the replace (#879): before it, the record names family=ex-fargate-standalone with identity.secondary_id=arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone:1; after it, identity.secondary_id=arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone-v2:1 with exactly one tombstone naming arn:aws:ecs:eu-west-1:000000000000:task-definition/ex-fargate-standalone:1, which is what lets the F2 plan tell the deregistered object’s lingering tag from a second live claimant instead of refusing “Indistinguishable instances without per-instance markers” forever; two earlier target choices each found a genuine, separate defect: aws_service_discovery_http_namespace.this_renamed’s (mv.go’s propagateModuleRename skipping MoveRecord for a same-module rename) is FIXED on the gauntlet/mv-rekey branch, GitHub issue #412 - see F-ORACLE’s own header comment and corpus-autoscaling-complete’s/corpus-eks-basic’s matching mv.go finding in this same unit, neither of which was re-run for #412; module.alb_renamed’s (the non-converging cascade, F-ORACLE’s own header comment, finding 2) remains a separate, open finding, not fixed here.
Crash between create and destroynot run
Teardownnot run
Plan, review, applypass28sone argument edited (module “ecs_service”’s service_tags ServiceTag, “Tag on service level” -> “Tag on service level, reviewed” - the service module merges service_tags into aws_ecs_service’s own tags and nowhere else, so it reaches exactly one instance where every tags = local.tags in this example fans out over a whole module), “plan -out=approved.tfplan” wrote a 147721-byte stock-format plan file whose whole change set is one update on module.ecs_service.aws_ecs_service.this[0]; the world then moved out of band (VPC vpc-37915144’s Name tag, through the AWS CLI, never through choudoufu - the same mutation STAGE 5 uses) 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.vpc.aws_vpc.this[0] and the live vpc-37915144 it was computed against, with “Exit status 3” spelled out for a pipeline; nothing was applied - arn:aws:ecs:eu-west-1:000000000000:service/ex-fargate/ex-fargate still read ServiceTag=“Tag on service level” through ecs list-tags-for-resource, not 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 VPC’s Name tag put back and nothing else changed, the IDENTICAL file applied - 0 added, 1 changed, 0 destroyed - and the service read back with the reviewed ServiceTag, 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 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 applypass2m49s62 resources from nothing, cluster marker verified via the AWS CLI, 62 of 62 records in the local record store (#364 A2; both aws_ecs_task_definition instances are now included, #671 having closed the numeric-wire-identity-component gap in internal/live/identity/located.go’s LocatedIdentityPlanFor that used to exclude them), replan empty, stock oracle in its own namespace matches structurally on cluster/service/standalone-task-definition/CloudMap-namespace/ALB/VPC
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 9m27.4s. Oracle: stock terraform 1.16.1, stock tofu 1.12.6 (matches the current pin).

RE-CROSSED FOR REAL 2026-08-21 at dbc5ebf575 (GitHub issue #346’s fix), twice, the second time instrumented to capture live-plan’s raw output. STAGES UNCHANGED at 2 of 5, and #346’s fix does not reach this estate either. Stage 1: ‘Apply complete! Resources: 62 added, 0 changed, 0 destroyed’, cluster confirmed carrying no tofu-address beforehand. Stage 2: ‘46 of 62 eligible (28 VERIFIED + 18 DRIFTED); 16 skipped’; ‘46 stamped, 1 recorded (time_sleep.this[0]), 0 failed, 15 skipped’; both markers confirmed by direct aws ecs describe-* calls against floci rather than through choudoufu’s own report (cluster/ex-fargate = module.ecs_cluster.aws_ecs_cluster.this:0, service/ex-fargate/ex-fargate = module.ecs_service.aws_ecs_service.this:0). Stage 3 fails on exactly 8 diagnostics, the same 8 as before, verified two ways (grep -c ‘^Error:’ over the raw output, and the script’s own hard count assertions): 1 ‘Module output not supported in static context’ on main.tf:68 (cluster_arn = module.ecs_cluster.arn), 1 ‘Unable to compute static value’ on modules/service/main.tf:1565 (aws_appautoscaling_target.this[0].resource_id), and 6 ‘Unresolvable identity’ cascading from it onto aws_appautoscaling_policy.this[‘cpu’] and [‘memory’]. Why the fix misses it: the same value-shaped module-CALL-argument route as corpus-rds-complete-postgres, PLUS a transform the part-shaped route could not express even if it reached - local.cluster_name = element(split(’/’, var.cluster_arn), 1), a function applied to the deferred value. identity.Formula holds literals and ParentRefs and has no way to say ‘split this parent attribute and take element 1’. That is a mechanism this repository does not have, not a gap in #346’s fix. A ‘Provider version does not match the admission evidence version’ warning (6.61.0 resolved against 6.59.0 admission evidence) also prints; the script’s own header calls it a caution, not a failure. PRIOR HISTORY BELOW. Landed dd83121592 (2026-08-18), 62 resources: ECS cluster (Container Insights, FARGATE/FARGATE_SPOT split), a BLUE_GREEN service behind an ALB, ECS Exec, ECS Service Connect, a two-container task definition plus a standalone second one, a CloudMap namespace, nested ALB/VPC modules. cold_deploy and migrate genuinely pass (43 of 62 stamped: 26 VERIFIED + 17 DRIFTED; 19 skipped - 16 untaggable by design, 3 blocked by #305). test_plan blocked by 4 sites: #305’s familiar default_* trio (3 sites) and a NEW one, filed as #308: the child-module for_each keyset prover (internal/live/identity/foreach_keyset.go) has no case for a for-comprehension (for k, v in var.container_definitions : k => v if …) and doesn’t chase a bare var.X for_each source across a module-call boundary to the literal object constructor at the caller, whose keys are actually static even though one unrelated attribute value inside the map is dynamic - resolve.go’s resource-level forEachOverComprehension already does the equivalent per-entry evaluation the module-call prover lacks. Two real floci gaps found and filed but not fixed, both sized as real modeling work rather than quick patches: lex00/floci#59 (CreateCluster silently drops settings/Container Insights, default_capacity_provider_strategy never serialized back) and lex00/floci#60 (CreateService/DescribeServices drop scheduling_strategy, enable_ecs_managed_tags, enable_execute_command, health_check_grace_period_seconds, deployment_controller, blue-green load_balancer.advanced_configuration, service_connect_configuration entirely - scheduling_strategy’s omission in particular forces the AWS provider to propose destroy-and-recreate on every plan after creation, a real non-idempotency bug independent of choudoufu). Neither floci gap blocks this crossing’s own outcome since test_plan already refuses earlier, upstream of any ECS-field diff. Follow-up pass 2026-08-18 (#313 cross-check, 0a94070b16/3ff22c5be6): the committed run.sh still asserted pre-#305 counts (43/62 eligible) and failed before ever reaching stage 3; updated to the real current numbers (46/62 eligible, #305’s default_* trio now fully resolved here) and re-verified against real floci twice plus a BREAK=1 negative control, all read from the script’s own printed lines. test_plan’s sole remaining blocker is confirmed #308 alone - #313’s diagnostic does not appear anywhere in the output. Mechanism: this estate’s vpc submodule expands subnets via count over a statically-known length, never a for_each keyed on the AZ name values, so #313 structurally can’t reach it. Commented on #313 and #308. #308 fixed and merged 2026-08-18 (a9ac6d06e7/b2bb59585d, generic: a *hclsyntax.ForExpr case plus a cross-module-call var/local chase in internal/live/identity/foreach_keyset.go, reaching every module-call for_each proof, not just this estate) - re-run confirms 0 occurrences of #308’s diagnostic (was 1), but test_plan is still blocked: #308 firing first had been masking two more causes in the same live-plan output all along. Follow-up pass 2026-08-18 (e74c7d5869/07c7317ab6) re-staled run.sh’s stage-3 assertions and header to the real current picture, verified across three separate real live-plan runs (stable counts each time) plus a BREAK=1 negative control: 236 total diagnostics, three distinct root causes, not one. Root cause A (#313’s canonical shape, 48 sites): data.aws_availability_zones.available feeding local.azs into module “vpc”’s azs argument. Root cause B (also #313’s family via a module output, 1 site): module.ecs_cluster.arn passed into module “ecs_service” as cluster_arn, “Module output not supported in static context”. Both A and B are #313’s already-ruled maintainer-level architecture question (live-plan never calls a provider during plan), not fixed here. Root cause C (NOT #313 - a distinct, newly-found, likely-fixable gap #308’s own fix exposed, 4 sites): each.value.enable_cloudwatch_logging/create_cloudwatch_log_group inside module.container_definition, both literal booleans in the caller’s own object literal but refused because each.value is treated as one opaque blob instead of projected to the referenced field - filed as #315, not attempted. These cascade to 177 “Unable to compute static value” and 6 “Unresolvable identity” sites (traced: aws_appautoscaling_policy reads aws_appautoscaling_target’s resource_id, itself one of C’s failures). Re-verified 2026-08-19 (be6b1096ba/b7bbff04a6) against everything landed overnight (#313’s data-source half, #315, #321/#324, #323, #325): root cause A (0 sites, confirmed fixed) and root cause C (0 sites, #315’s each.value projection fix confirmed) are both gone. Only root cause B remains (module.ecs_cluster.arn, a Computed attribute crossing a module-output boundary), now cascading to 1 “Unable to compute static value” plus 6 “Unresolvable identity” sites (aws_appautoscaling_target/aws_appautoscaling_policy chain) - down from 236 diagnostics to 8. This is the same already-acknowledged Computed-attribute/module-output architecture question corpus-rds-complete-postgres and corpus-security-group-complete are also blocked on; no action taken here, no new issue filed.

Reproduce it#

go run ./tools/gauntlet run corpus-ecs-fargate

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-ecs-fargate/run.sh; BREAK=1 corrupts its assertions to show they are load-bearing.