terraform-aws-modules/terraform-aws-security-group examples/complete (tag v6.0.0), its flagship example

Source: https://github.com/terraform-aws-modules/terraform-aws-security-group.git at v6.0.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 deploypass22s67 resources (DELTA 2, lex00/floci#57)
Migratepass1m16s58 of 67 stamped, 67 identities recorded (#364 unit A2)
Replan from nothingpass4sthe plan is genuinely empty: every choudoufu wall (#305, #307, #313 A and B, #321, #332) and both confirmed floci gaps (#102, #104) are fixed or absent this run; default route table identities asserted by value against the AWS CLI in step 3a
No-op applypass4sno-op apply (0 added, 0 changed, 0 destroyed); tofu-estate-tagged object count unchanged at 58 objects, read through resourcegroupstaggingapi
Drift and reconvergepass7sone object tampered (DriftProbe tag on the main security group), exactly module.security_group.aws_security_group.this[0] proposed, apply changed 1 and the tag is gone, confirmed via the AWS CLI
Renamepass14smoved block: module.postgresql renamed to module.postgresql_renamed with zero churn (0 add, 4 change, 0 destroy) - the rule-children case, its own SG plus ingress/egress rules and rules_exclusive all moving under one moved block; live-mv: aws_security_group.app 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 blockpass11schoudoufu: deleting module.postgresql_renamed’s block proposed exactly 5 destroys (0 add, 0 change, 5 destroy: SG + 2 ingress + 1 egress + 1 untaggable rules_exclusive), applied cleanly (0 added, 0 changed, 5 destroyed), the security group is genuinely gone from the live account (0 matches on describe-security-groups for the old id, read via the AWS CLI, not choudoufu’s own report), and the next plan proposes nothing; stock oracle on cold_deploy’s own state (D-ORACLE remove) also proposes exactly 5 destroys for the same 5 objects; classifyOrphans did not withhold the untaggable rules_exclusive destroy even though module.security_group’s and module.consul’s own rules_exclusive instances share its block key, because both surviving instances are bound, not unclaimed
Change countpass28schoudoufu: scaling aws_security_group.count_test from 2 to 1 destroyed exactly count_test[1] (0 add, 0 change, 1 destroy), leaving count_test[0]’s live GroupId (sg-388e5aceb94bd0d5d) and its tofu-address=aws_security_group.count_test:0 marker unchanged, both read through the AWS CLI; scaling back from 1 to 2 created exactly count_test[1] (1 add, 0 change, 0 destroy) under a NEW server-minted GroupId (sg-ccc8aef2f836b8abc, was sg-ceabc2389e1136146 - a security group id is never reused, so the destroy is directly observable rather than inferred) carrying tofu-address=aws_security_group.count_test:1, while count_test[0] kept both its id and its marker throughout; the local record store tracked the same facts by value at every step (index 0’s record never moved, index 1’s stopped naming the destroyed object and then named the new one); the next plan is empty. G-ORACLE is the stock leg for the identical shape, applied for real with plain terraform in the idle greenfield account: 3 created, then 0 add/0 change/1 destroy hitting count_test[1] only, then 1 add/0 change/0 destroy bringing it back under a new id, count_test[0]’s id unchanged both times - identical to choudoufu’s. SYNTHETIC BLOCK, and why: this estate declares no scalable knob of its own - every count in terraform-aws-security-group v6.0.0 is the boolean ’local.create ? 1 : 0’ toggle, and its real for_each maps (var.ingress_rules/var.egress_rules) cannot be scaled in isolation because main.tf line 96 feeds every rule id into aws_vpc_security_group_rules_exclusive.this[0], making the plan 0 add/1 change/1 destroy instead of the exact shape this stage asserts; aws_security_group.count_test is self-contained, named by nothing else here, and of a type this estate already exercises six times over (reference-ec2-vpc Part F and corpus-iam-policy Part G are the precedent). BREAK_COUNT=1 runs this stage’s Break control (expect count_test[0] to be the destroyed one) and correctly fails to hold.
Replace with create_before_destroypass11schoudoufu: changing module.security_group’s ForceNew name argument proposed exactly one SG replace at the same declared address, cascading into its 7 ingress rules, 1 egress rule and 1 rules_exclusive enforcer (all replaced) - 10 to add, 10 to destroy, matching F-ORACLE’s own plan shape; applied cleanly; the old SG (sg-7d47321ffe396ce67) is confirmed gone and the new SG (sg-1aeeb0bfa4607e022) carries the marker, both via the AWS CLI; the local record store’s record at the same address now names the new SG, not the destroyed one; the next plan proposes no resource action; BREAK=replace confirms a manufactured marker collision is reported loudly (“Indistinguishable instances without per-instance markers”, naming both live security groups) rather than silently proposed as nothing - internal/live/discovery/supersededclaimant.go (#849) tombstones only what an apply actually destroyed, so a live duplicate with no tombstone is never pruned away. 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 destroynot run
Teardownnot run
Plan, review, applypass18sone argument edited (aws_security_group.app’s tags merge in Reviewed=yes; the standalone app SG has no rule children, so one argument is one row), “plan -out=approved.tfplan” wrote a 87593-byte stock-format plan file whose whole change set is one update on aws_security_group.app (sg-a7a28533562efa9a1); the world then moved out of band (a DriftProbe tag on sg-7d47321ffe396ce67 through the AWS CLI, never through choudoufu - STAGE 5’s own proven mutation, on a DIFFERENT instance and a DIFFERENT live security group 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.security_group.aws_security_group.this[0] and the live sg-7d47321ffe396ce67 it was computed against, with “Exit status 3” spelled out for a pipeline; nothing was applied - describe-tags on sg-a7a28533562efa9a1 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 DriftProbe tag deleted and nothing else changed, the IDENTICAL file applied - 0 added, 1 changed, 0 destroyed - and sg-a7a28533562efa9a1 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, the app SG confirmed to be the same GroupId it started as, 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 applypass27s67 resources from nothing, all markers verified via the AWS CLI, 67 records in the local record store (#364 A2), replan empty, 6 tagged security groups (4 named + 2 default adopters) and every named one’s rule shape matches $PLAIN_EST’s own stage-1 apply object by object, tags stripped
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 3m42.9s. Oracle: stock terraform 1.16.1, stock tofu 1.12.6 (matches the current pin).

Landed c876435875 (2026-08-18), 67 real resources. The brief expected this crossing to hit #304 (a static lookup()-keyed count-index) directly, since a prior crossing found #304 through this same module as a dependency - checked, not assumed, and refuted: v6.0.0 rewrote the module from the classic single-aws_security_group-with-dynamic-blocks shape #304 lives in to a for_each-over-a-map shape emitting aws_vpc_security_group_ingress_rule/egress_rule per rule key, so that whole pattern is gone from this version’s own example. migrate genuinely passes (52 of 67 eligible and stamped: 35 VERIFIED + 17 DRIFTED; 15 skipped - 6 untaggable by design, 9 unadmitted). test_plan blocked by two real, distinct, filed gaps: #305 (6 sites, the familiar default_* adopter trio, doubled since this estate nests two terraform-aws-vpc calls) and a NEW one, filed as #307: aws_vpc_security_group_rules_exclusive is unadmitted (3 sites) - no CFN counterpart and the pinned provider ships no identity schema for it, but its own import docs are unambiguous that security_group_id (required, ForceNew, always a direct parent reference) is its whole identity, the same shape aws_vpc_security_group_vpc_association’s already-admitted row has. Also found and filed a real floci gap (lex00/floci#57: EC2 AssociateSecurityGroupVpc has no handler at all), worked around with a documented delta removing the estate’s one vpc_associations block (67 of 68 resources still stand up for real). One open, honestly-unresolved observation: 17 of the DRIFTED resources show referenced_security_group_id read back as “000000000000/sg-xxx” from floci where config computes a bare “sg-xxx” - doesn’t block stamping, but live-plan never got past #305/#307’s hard refusals to reveal whether the real provider’s diff-suppression absorbs this cleanly or would surface as an 18th change. Not filed separately; the next crossing attempt (once #305/#307 land) will show it for real. Follow-up pass 2026-08-19 (#313’s data-source fix, c636ab20f7/0284d8c408): re-verified for real against the current pin (67 cold-deployed, 58 stamped, state deleted). test_plan diagnostics dropped from 239 to 19 - #313’s canonical data.aws_availability_zones cause (50 sites) is fully gone, the module-output/resource-attribute variant (2 sites) still correctly refuses (out of scope by the maintainer’s own ruling), and the 187-site cascade collapsed to 5. The remaining 12 sites are a NEW, newly-reached class (previously masked behind #313’s hard refusal): element([].id, count.index) in the vpc module’s aws_route_table_association.private - both operands are tagged resources, the obstacle is the splat-through-function-call shape, not an admission gap. Filed as #321, not attempted (scouting slot). test_plan therefore stays fail, but the estate’s real remaining blocker is now #321 (a derivable, no-design-call-needed gap) rather than #313 (an architecture question) - #321 is the clear next step toward this estate’s five-of-five and the core set’s last gap. Follow-up pass 2026-08-19 (#321 fixed and merged, c33a47288a/626ca84739): element([].attr, idx) over a splat of tagged resources now resolves generically - it names the same live object a direct indexed traversal already resolves, via element()’s own modulo wraparound. Re-verified for real: test_plan diagnostics 19 -> 7. The 12 splat-through-element sites are confirmed cleared by their absence from real live-plan output; the remaining 7 are #313’s own deliberately-out-of-scope resource-attribute root cause (root cause B), a maintainer scope boundary, not a bug. test_plan stays fail - the core set does NOT reach five-of-five from this fix alone. Generalizes beyond this estate: refusal-probe over terraform-aws-modules/vpc’s own examples, 21 -> 8 sites, zero regressions - three configs (ipam, ipv6-only, outpost) fully cleared. A related but distinct lint-side wall (RuleCountIndex, 32 sites/6 configs, a genuine unresolved composite-vs-per-argument injectivity design question) stays independently blocking and was left open, documented rather than attempted. Follow-up pass 2026-08-19 (#191 fixed and merged, 312acbbb61/75ef0a6a78): internal/live/identity/partialargs.go’s tolerant rebuild now composes across more than one module call and evaluates a call the caller wrote (merge(), not a bare constructor) through an evaluator whose own var.* closure is already tolerant, one module up. module.consul’s ingress_referenced_security_group_id map no longer poisons the 22 ingress rules it seeds two module calls down - the map’s KEYS (eleven preset names crossed with one caller key) were always written down; only the VALUE under one key was ever unknowable, and it still is. Re-verified for real against floci, script exit 0, BREAK=1 negative control correctly fails: test_plan diagnostics 7 -> 4, and every analysis-layer refusal this estate has ever hit is now 0 and asserted by absence (#305, #307, #313 root causes A and B both, #321). What newly reached PROJECTION and blocks the estate now is #332 (not #313 - the old ‘Unable to use aws_security_group.app in static context’ framing is confirmed gone): aws_default_route_table imports by the VPC’s id, not its own, and the ratified row says otherwise - 2 ‘Cannot import for projection’ + 2 ’empty result’, one pair per nested vpc module call, both traced to the same type. #332 is filed, not fixed here; it is now the sole remaining blocker on this estate and on the core set’s last five-of-five gap. #332 fixed 2026-08-19 (859c1ad747/ff1f6bcdea/c1197befc7): the ratified row claimed aws_default_route_table imports by the route table’s own rtb-… id; the real provider imports it by the VPC’s id, read off the vpc_id ATTRIBUTE (not argument) the discovered object already carries - settled by running stock terraform 1.15.8 + hashicorp/aws 6.59.0 (Error: empty result for rtb-…, Import successful! for vpc-…). Reach stated honestly: one type today (defaultAdopterSiblings/sameRatifiedIdentity in internal/live/discovery/discovery.go split “same live object” from “same import identity” generically, off each type’s own ratified IdentityAttrs/ImportSyntax, no type name in the control flow - #302’s aws_iam_service_linked_role/aws_iam_role pair already exercises the same recomposition path; aws_default_route_table is simply the only aws_default_* row that currently diverges from its plain sibling at aws 6.59.0). Re-verified independently 2026-08-20 with a fresh real crossing against floci (ghcr.io/lex00/floci@sha256:120b6783c7fb48d3d78245056251492b7d9246cdc3b397c98db2659cdc78d94a, the currently pinned image): STAGE 1 PASS, STAGE 2 PASS, STAGE 3 BLOCKED at exactly 1 site (was 239, then 19, then 7, then 4) - every choudoufu-layer refusal this estate has ever hit (#305, #307, #313 root causes A and B, #321, #332) confirmed absent, and step 3a re-derives each nested VPC’s default route table import identity from AWS directly (module.vpc: vpc-9eceebf0 -> rtb-7a9e0d017620e163c; module.vpc_secondary: vpc-91d40754 -> rtb-b66d5dc82e63b6e38) and asserts it BY VALUE, not by absence. The 1 remaining site is the AWS provider answering “Provider produced invalid plan” on its own requires-replacement path for module.security_group.aws_vpc_security_group_ingress_rule.this[“dns-from-prefix-list”] (cty.Path{cty.GetAttrStep{Name:""}}), explicitly a provider bug per the error’s own text - filed upstream as #335, genuinely outside this fork’s code (the diagnostic names no choudoufu path). test_plan stays “fail” in this table’s pass/fail/not_run vocabulary since the plan is not clean, but the estate’s own blocker has moved entirely off this fork: #332 was the core set’s last derivable gap, and #335 (an AWS-provider defect) is what now stands between this estate and five-of-five.

Reproduce it#

go run ./tools/gauntlet run corpus-security-group-complete

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