Skip to content

fix(terraform): create edges to outputs of modules expanded by count or for_each - #7663

Open
AlexKantor87 wants to merge 2 commits into
bridgecrewio:mainfrom
AlexKantor87:fix-module-instance-output-edges
Open

fix(terraform): create edges to outputs of modules expanded by count or for_each#7663
AlexKantor87 wants to merge 2 commits into
bridgecrewio:mainfrom
AlexKantor87:fix-module-instance-output-edges

Conversation

@AlexKantor87

@AlexKantor87 AlexKantor87 commented Aug 26, 2026

Copy link
Copy Markdown

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Description

A module block expanded by count or for_each gets vertices named name[idx] / name["key"], but references to its outputs never match those vertices, so the referencing resource gets no edge to the module output and every graph check involving the module fails regardless of the configuration. For example, a security group created in a module called with count = var.create_sg ? 1 : 0 and attached via module.sg[0].security_group_id fails CKV2_AWS_5 even though the attachment is right there in the same graph. Plain module calls, including nested ones, are unaffected, which I suspect is why this has gone unnoticed: it only bites codebases that use count as their conditional-module idiom, and for those it bites on every graph check at once.

Three parts to the fix:

  1. The lookup. _get_possible_vertices misses expanded module vertices because references are index-stripped before lookup while the vertices keep their suffix. A fallback now matches on the suffix-stripped vertex name, covering numeric and string keys both.
  2. for_each instance binding. A string key survives tokenisation (sg["alpha"] arrives intact), but _connect_module took the first admissible output vertex and stopped, so which instance a reference bound to depended on iteration order and flipped between runs. Expanded instances now connect to their own output, matched on TFModule.foreach_idx.
  3. count instance binding. A numeric index does not survive tokenisation: module.sg[1].security_group_id reaches edge building as ['sg', 'security_group_id'], so per-instance binding is impossible there, and best-match silently orphaned every instance it did not pick. References that resolve by name alone now connect to every expanded instance of that module block. The instances are expansions of a single configuration block, so this keeps the graph a faithful superset instead of dropping edges.

Verified shapes

shape before after
plain module call, incl. nested two deep ok ok (unchanged)
count module, indexed reference no edge connected
count = 2, one consumer per instance one instance orphaned every instance connected
for_each, string-key references nondeterministic binding exact instance, deterministic
reference wrapped in try() ok ok (unchanged)
nested-block attribute (e.g. network_configuration) ok ok (unchanged)

Known limitations, deliberately out of scope

These sit in reference tokenisation (get_referenced_vertices_in_value), a layer above edge building, and I did not want to widen a targeted fix into that:

  • splat: module.sg[*].attr loses both the marker and the attribute, arriving as ['sg']
  • dynamic index: module.sg[var.i].attr, same
  • a for_each key containing a dot: module.sg["a.b"].attr is split inside the quotes into ['sg["a', 'b"]', ...]

Happy to raise these as separate issues with reproductions if useful.

Related in spirit to #6145, which fixed the same class of problem for the terraform_plan framework. Found on 3.3.6, still present on 3.3.15 and main.

Checklist:

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation (none needed; behaviour now matches what the documentation already describes)
  • I have added tests that prove my feature, policy, or fix is effective and works

Three tests: the first fails on main with expected to find edge from [resource aws_lb.alb] to [output security_group_id] and passes with the fix; a count = 2 fixture asserts every instance's output gains an incoming edge; a for_each fixture asserts each reference binds to exactly its own instance. The full tests/terraform/graph tree passes either way, apart from two pre-existing rustworkx solver failures that are identical with and without this change.

…or for_each

A module block expanded by count or for_each gets vertices named "name[idx]"
or 'name["key"]', but references to its outputs never match those vertices, so
the referencing resource gets no edge to the module output and every graph
check involving the module fails regardless of the configuration. For example,
a security group created in a module called with count and attached via
module.sg[0].security_group_id fails CKV2_AWS_5 even though the attachment is
right there in the same graph. Plain module calls, including nested ones, are
unaffected, which is why this has been easy to miss: it only bites codebases
that use count as their conditional-module idiom, and for those it bites on
every graph check at once.

Three parts to the fix:

1. _get_possible_vertices misses expanded module vertices because references
   are index-stripped before lookup while the vertices keep their suffix. A
   fallback now matches on the suffix-stripped vertex name (numeric and string
   keys both).

2. A for_each string key survives tokenisation ('sg["alpha"]' arrives intact),
   but the output selection in _connect_module took the first admissible output
   vertex and stopped, so which instance a reference bound to depended on
   iteration order and flipped between runs. Expanded module instances now
   connect to their own output, matched on TFModule.foreach_idx.

3. A numeric count index does NOT survive tokenisation:
   module.sg[1].security_group_id reaches edge building as ['sg',
   'security_group_id'], so per-instance binding is impossible there and
   best-match silently orphaned every instance it did not pick. References that
   resolve by name alone now connect to every expanded instance of that module
   block. The instances are expansions of a single configuration block, so this
   keeps the graph a faithful superset instead of dropping edges.

Known limitations, deliberately out of scope because they sit in reference
tokenisation rather than edge building: splat (module.sg[*].attr loses both the
marker and the attribute, arriving as ['sg']), a dynamic index
(module.sg[var.i].attr, same), and a for_each key containing a dot
(module.sg["a.b"].attr is split inside the quotes into ['sg["a', 'b"]', ...]).

Three tests: the first fails on main with 'expected to find edge from [resource
aws_lb.alb] to [output security_group_id]' and passes with the fix; a count = 2
fixture asserts every instance's output gains an incoming edge; a for_each
fixture asserts each reference binds to exactly its own instance. The full
tests/terraform/graph tree passes either way, apart from two pre-existing
rustworkx solver failures that are identical with and without this change.

Found on 3.3.6, still present on 3.3.15 and main.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant