Study guide
6.2 Resource Attributes and Cross-Resource References
HashiCorp Certified: Terraform Associate 004
Track your progress as you read
Finished sections get a check mark.
1 Introduction & Exam Overview
1.1 Current Exam Facts 1.2 Format, Delivery, Retakes, and Recertification 1.3 004 Changes, Terraform 1.12, and Study Plan
2 Infrastructure as Code with Terraform
2.1 What Infrastructure as Code Is 2.2 Advantages of IaC Patterns 2.3 Multi-Cloud, Hybrid, and Service-Agnostic Workflows
3 Terraform Fundamentals: Providers & State
3.1 Install and Version Terraform Providers 3.2 How Terraform Uses Providers 3.3 Configuration with Multiple Providers 3.4 How Terraform Uses and Manages State
4 Core Terraform Workflow: Init & Validate
4.1 The Terraform Write–Plan–Apply Workflow 4.2 Initialize a Working Directory 4.3 Validate a Configuration
5 Plan, Apply, Destroy, and Formatting
5.1 Generate and Review an Execution Plan 5.2 Apply Changes to Infrastructure 5.3 Destroy Terraform-Managed Infrastructure 5.4 Formatting and Style Adjustments
6 Resources, Data Sources, and References
6.1 Resource Blocks vs Data Blocks 6.2 Resource Attributes and Cross-Resource References 6.3 Resource Addressing and Interpolation Patterns
7 Variables, Outputs, and Complex Types
7.1 Input Variables, tfvars, and Precedence 7.2 Output Values 7.3 Complex Types: list, map, object, tuple, set
8 Dynamic Configuration and Dependencies
8.1 Expressions, count, for_each, and dynamic Blocks 8.2 Built-in Functions 8.3 Dependencies, depends_on, and Lifecycle Rules
9 Conditions, Secrets, and Sensitive Data
9.1 Custom Conditions: validation, precondition, postcondition, check 9.2 Sensitive Data Handling 9.3 Vault, Ephemeral Values, and Write-Only Arguments
10 Terraform Modules
10.1 How Terraform Sources Modules 10.2 Variable Scope within Modules 10.3 Using Modules in Configuration 10.4 Managing Module Versions
11 Terraform State Management
11.1 The Local Backend 11.2 State Locking 11.3 Remote State and the backend Block 11.4 Resource Drift and State Operations
12 Maintain Infrastructure with Terraform
12.1 Import Existing Infrastructure 12.2 Inspect State with the CLI 12.3 Verbose Logging and Diagnostics
13 HCP Terraform
13.1 Create Infrastructure with HCP Terraform 13.2 Collaboration and Governance Features 13.3 Workspaces and Projects 13.4 HCP Terraform Integrations
6.2 Resource Attributes and Cross-Resource References
Key Takeaways Explain This Topic AI
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 want Expression Notes
- Managed resource attribute aws_instance.web.id TYPE.NAME.ATTR
Data source attribute data.aws_ami.al2023.id Always starts with data.
- Input variable var.instance_type Not a resource address
- Local value local.name_prefix Singular local
- Child module output module.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.