skip to content

Beyond One Resource

Judging one resource usually means finding another, and a plan records that link as a reference rather than a value. A subject managed in another root module never appears at all.

on this pageshow

questions

3

In a Terraform plan JSON, which section shows that one resource references another?

level: juniorimportance: must knowfreq 58%

answer

  1. values and links live apart
  2. expressions, not attributes
  3. look in the configuration section
  4. references carry addresses, ids do not
  5. then join by address

basics

~10 s

The configuration section. Each resource there carries an expressions object whose references list the addresses it points at. The planned_values section holds only each resource's resolved attribute values and records no links between resources.

solid answer

~40 s

Relationships live in `configuration`, not in `planned_values`. Under `configuration.root_module.resources`, every resource has an `expressions` object with one entry per argument, and an argument written as a reference appears as `{"references": ["aws_security_group.web.id", "aws_security_group.web"]}` — resource addresses, not values. `planned_values` gives you each resource's post-apply attributes keyed by address, and nothing else; the instance's `vpc_security_group_ids` there would be provider ids like `sg-0ab…`, which are opaque strings you cannot look up in the document, and for a group this run is creating there is no id printed at all. So any rule whose subject is "the security group this instance attaches to" is a two-step job: read the reference out of `configuration`, then join by address back into `planned_values` to read the target's attributes.

go deeper

for a junior

Be ready to name the two sections and say what each is for: configuration holds expressions and references, planned_values holds resolved attribute values per resource address. Knowing that links and values live apart is the whole point here.

for a middle

Explain why a provider id in planned_values is useless as a join key, and show the three-step shape of a cross-resource rule: find the subject, read the reference, join by address. Mention that references may point at variables or module outputs, not only resources.

for a senior

Demonstrate that you know what a green result means when the reference is a literal id or the target lives outside this configuration. An interviewer wants to hear you distinguish 'no violation found' from 'the rule could not evaluate its subject'.

for a principal

Own the consequence for a control catalogue: which controls are expressible against a single plan at all, and how you keep the gate's stated coverage honest when some of them are not. The framing to defend is that the artifact's shape, not your intent, sets the limit.

## The plan is several views, not one picture A Terraform plan rendered as JSON is not a single model of your infrastructure. It is a set of parallel views of the same run, and each answers a different question. Two of them matter for cross-resource rules: - **`planned_values`** — what each resource will look like after apply. Under `planned_values.root_module.resources` you get one entry per resource instance, each with an `address` (`aws_instance.web[0]`), a `type`, a `name`, and a `values` object holding the resolved attributes. - **`configuration`** — your code, normalised into JSON. Under `configuration.root_module.resources` you get one entry per resource *block* (not per instance), each with an unindexed `address`, a `mode` (`managed` or `data`), and an `expressions` object with one entry per argument. The distinction that decides what rules you can write: **`planned_values` records values; `configuration` records links.** ## Why the values view cannot answer a relationship question Take a real control: *an instance tagged `tier = data` must not attach a security group that allows egress to `0.0.0.0/0`.* Two resources, and the violation exists only in the pair. From `planned_values` you can read the instance's `tags`, and you can separately read each security group's `egress` blocks. What you cannot read is **which group this instance attaches to**. The instance's `vpc_security_group_ids` attribute holds provider identifiers — opaque strings like `sg-0ab12…` — and an identifier is not an address. There is no index in the document that maps `sg-0ab12…` back to `aws_security_group.web`, so you have nothing to look up. And when the same run is creating that group, the identifier does not exist yet at all, so the attribute carries no usable value either. Written only against `planned_values`, the rule collapses into one of two useless shapes: flag *every* group with open egress (which fires on the NAT and load-balancer groups nobody objects to), or flag *every* data-tier instance (which tells you nothing about its network exposure). ## The link is an expression, and expressions are in `configuration` In the configuration view the same argument looks like this: ``` "expressions": { "vpc_security_group_ids": { "references": ["aws_security_group.web.id", "aws_security_group.web"] } } ``` An expression entry is one of two things. Either a **`constant_value`**, when the argument was written as a literal, or a **`references`** array, when it was written as a reference to something else. The array typically carries both the full traversal (`aws_security_group.web.id`) and the bare resource address (`aws_security_group.web`), because both are legitimate ways to name what was traversed. The bare address is the one you want: it is exactly the key you will match on in `planned_values`. References are not always to resources. `var.subnet_id`, `local.sg`, `module.network.security_group_id` and `data.aws_ami.base.id` all appear in the same array shape, so a rule that assumes every reference names a managed resource in this module will trip over the first indirection. ## The shape of every cross-resource rule Once you know where the two halves live, the pattern is always the same three steps: 1. Find the subject in `configuration` — the resource whose argument creates the relationship. 2. Pull the target's address out of that argument's `references`. 3. Join by address into `planned_values` and read the target's attributes. Then assert over the pair. The join key is a string address, and getting that string right — indices, module prefixes — is where the work actually is. ## The case with no reference at all If someone wrote `vpc_security_group_ids = ["sg-0ab12…"]`, the expression is a `constant_value` and there is no `references` entry to follow. The group is a real thing in the account, it is doing real damage, and it is simply not in this document — not as a managed resource, not as anything. A rule that starts by reading `references` finds nothing, produces no violation, and reports green. That is the more general limit of a plan gate: the document describes **one root configuration's run**, so a subject that lives in another configuration, or was created by hand, is not there to be judged.

  • What exactly does a references array contain for one argument?
    Usually both the full traversal that was written and the bare resource address it started from — for example `aws_security_group.web.id` alongside `aws_security_group.web`. Strip to the bare address before joining, since that is the form `planned_values` uses. References can also name non-resources: `var.sg_id`, `local.groups`, `module.network.sg_id`, or a `data.` address, so check what kind of thing you landed on before assuming you have a resource.
  • What if the attribute was written as a hard-coded id string instead of a reference?
    Then the configuration entry is a `constant_value` and there is no `references` array at all. The target resource is not declared in this configuration, so there is nothing in the document to join to and nothing to evaluate. A rule that only walks references will silently produce no violation — which looks identical to a pass. Detect that case explicitly and report it as unevaluable rather than letting it fall through.
  • Does the configuration section also tell you the attribute's final value?
    Only when it was a literal, as a `constant_value`. Anything computed, defaulted by the provider, or derived from a variable or another resource is resolved into `planned_values`, not into `configuration`. So the two sections are complementary rather than redundant: read links from one and values from the other.

planned_values is a phone book: every entry's details, no calls. configuration is the call log: who dialled whom, no details. A question about a conversation needs both, joined by name.

saying these in an interview costs you the question

  • Assumes planned_values records links between resources
  • Treats a provider id as a joinable resource address
  • Expects final attribute values in the configuration section
  • Believes the plan contains every resource in the account
  • Writes the rule against one resource and calls the pair covered

context

open as a page

In Terraform plan JSON, how do you join a configuration reference back to its planned resource?

level: middleimportance: should knowfreq 50%

basics

~20 s

Strip the reference down to its bare resource address, then look that address up among the planned resources. The two sides do not match literally: configuration addresses carry no count or for_each index, and module resources are nested differently in each section.

open as a page

A log-retention rule's subject lives in another root module's plan — how do you gate it?

level: seniorimportance: nice to knowfreq 30%

basics

~20 s

Usually you do not gate it here. When the control's subject is missing from this plan, either constrain the module input so it becomes visible, or re-home the control where the subject exists. Approximating it is worse than not enforcing it.

open as a page