6.2 Resource Attributes and Cross-Resource References

Key Takeaways

  • Reference a managed attribute as TYPE.NAME.ATTR and a data-source attribute as data.TYPE.NAME.ATTR
  • A reference creates an implicit dependency; Terraform will not apply the consumer until the producer exists
  • Configured arguments come from your HCL; computed attributes such as an AWS instance id are decided by the remote API and show as (known after apply) on first create
  • Use ${} interpolation only inside string literals; modern HCL prefers first-class expressions such as ami = data.aws_ami.al2023.id
  • Outputs and other resources consume attributes the same way; circular references fail the dependency graph
Last updated: August 2026

6.2 Resource Attributes and Cross-Resource References

Quick Answer: Read a managed resource with TYPE.NAME.ATTR (aws_instance.web.id). Read a data source with data.TYPE.NAME.ATTR (data.aws_ami.al2023.id). That reference is also an implicit dependency. Attributes you set in HCL are configured; attributes the API assigns — an EC2 instance id, a public IP — are computed and print as (known after apply) until apply. Use ${} only inside strings. Circular references fail.

Objective 4b on Terraform Associate (004) is: refer to resource attributes and create cross-resource references. HashiCorp maps this objective to References to Named Values and Resource Addressing. Section 6.3 covers addresses used on the CLI. This section is the configuration-language side: how one block reads another block's attributes.

The two reference shapes

Terraform does not invent a third prefix for resources. Any name that is not var., local., module., data., path., or terraform. is treated as a managed resource reference:

resource "aws_instance" "web" {
  ami           = data.aws_ami.al2023.id
  instance_type = "t3.micro"
}

resource "aws_eip" "web" {
  instance = aws_instance.web.id
}

output "web_public_ip" {
  value = aws_eip.web.public_ip
}
What you wantExpressionNotes
Managed resource attributeaws_instance.web.idTYPE.NAME.ATTR
Data source attributedata.aws_ami.al2023.idAlways starts with data.
Input variablevar.instance_typeNot a resource address
Local valuelocal.name_prefixSingular local
Child module outputmodule.vpc.private_subnet_ids[0]Parent sees outputs, not the child's internal resources

There is no resource.aws_instance.web.id. There is no var.aws_instance.web.id. Forgetting the data. prefix on a data source is the most common 004 miss: aws_ami.al2023.id looks for a managed resource "aws_ami" "al2023" and fails if you only declared a data block.

These names are not ordinary objects. Official language: you cannot use square-bracket notation to replace the TYPE.NAME path, and you cannot write for inst in aws_instance to iterate every AWS instance resource in the module. You must write the type and the label exactly.

Configured versus computed attributes

Provider documentation lists two families of attributes:

  • Configured (arguments) — values you write in the block. instance_type = "t3.micro" and ami = data.aws_ami.al2023.id are configured. After the first apply they are also stored in state, and you can read them back as aws_instance.web.instance_type.
  • Computed (exported) — values the remote system assigns. An AWS instance id such as i-0abc..., private_ip, arn, and many *_id attributes are computed. You cannot know them from HCL alone.

On a create, Terraform cannot predict a generated id. Official wording: unknown values appear in terraform plan output as (known after apply). Adding a known value to an unknown value produces another unknown value, so "web-${aws_instance.web.id}" is also (known after apply) until apply finishes.

After the object exists, later plans show the stored id unless the instance is being replaced. Replacement makes the id unknown again, because a new object gets a new id.

count cannot be unknown. If you write count = aws_instance.web.id == "" ? 1 : 1 in a way that needs a not-yet-created id, plan fails. Data sources whose arguments depend on computed values are deferred to apply, and their attributes become unknown too.

Implicit dependencies

Terraform walks every expression and draws a graph edge from the referenced object to the object that mentioned it. In the example above:

  • aws_instance.web depends on data.aws_ami.al2023 because of ami = data.aws_ami.al2023.id.
  • aws_eip.web depends on aws_instance.web because of instance = aws_instance.web.id.
  • The output depends on the EIP.

You do not need depends_on for that chain. depends_on is the escape hatch when there is a hidden ordering requirement that does not appear as an attribute reference — for example, an IAM policy that must exist before an instance profile is usable, even though no argument interpolates the policy's id. Prefer the implicit edge. Official docs treat references as the normal way to declare order.

Nested attributes and splats follow the same rule. aws_instance.web.ebs_block_device[*].device_name is a list of configured device names. aws_instance.web[0].id is the first instance when count is set. aws_instance.web["app"].id is the app key when for_each is set. A splat works on a list (count); for for_each you take values(aws_instance.web)[*].id because a splat does not walk a map.

Interpolation versus first-class expressions

Older Terraform required ${...} around every reference. Modern HCL — including Terraform 1.12 — treats references as first-class expressions. You write them bare wherever an expression is allowed:

resource "aws_instance" "web" {
  ami = data.aws_ami.al2023.id          # first-class — preferred
  # ami = "${data.aws_ami.al2023.id}"  # legal, but forces a string
}

resource "aws_lb_target_group_attachment" "web" {
  target_id = aws_instance.web.id
}

locals {
  banner = "Welcome to ${var.project} in ${var.environment}"  # ${} belongs here
}

${} is a string template interpolation. It is required only inside a quoted string or a heredoc, where you are building text. Official strings documentation: a ${ ... } sequence evaluates the expression, converts the result to a string if needed, and inserts it. Directives such as %{ if } and %{ for } are the other template form; they are still string features, not a second reference syntax.

Wrapping a lone reference in "${...}" is not an error, but it converts the value to a string. That can hide type mistakes (id is already a string; a number or bool should stay a number or bool). 004 answers that still wrap every reference in ${} are answering from 0.11 muscle memory.

Outputs, other resources, and cycles

Anything that accepts an expression can consume an attribute: another resource argument, a locals value, a module input, or an output value. Sensitive computed attributes stay sensitive when you derive from them; if you export one, the output itself must be marked sensitive.

Circular references fail. If aws_instance.web sets subnet_id = aws_subnet.app.id and aws_subnet.app sets a tag from aws_instance.web.id, Terraform cannot order the graph. The run errors with a cycle. Break the cycle by removing one edge — usually the tag should not wait on the instance — or by introducing a value that does not come from the other resource (a variable, a local, or a data source that already exists).

Referring to a resource inside its own resource block by name is the same class of cycle. That is why provisioner and connection blocks use self (section 6.3). Ordinary arguments in the same block can refer to other arguments only in the limited ways the provider schema allows; they cannot wait on the computed id of the object that does not exist yet except as (known after apply).

004 traps for objective 4b

  • aws_instance.web.id is the managed instance. data.aws_instance.web.id is a different object — a data source you must declare.
  • Plan printing (known after apply) for an instance id is success, not a broken interpolation.
  • ${} is for strings. Bare TYPE.NAME.ATTR is the modern reference.
  • A parent module cannot reach into module.vpc.aws_subnet.private[0].id as a configuration expression. It can only read module.vpc outputs.
  • Two resources that each reference the other will not apply "in parallel"; they fail the graph.
Loading diagram...
A reference is an implicit dependency edge
Test Your Knowledge

Which expression correctly reads the id of a managed aws_instance named web?

A
B
C
D
Test Your Knowledge

A brand-new aws_instance.web is being created. How does terraform plan display its id?

A
B
C
D
Test Your Knowledge

Why do you usually write ami = data.aws_ami.al2023.id instead of ami = "${data.aws_ami.al2023.id}" on Terraform 1.12?

A
B
C
D