6.3 Resource Addressing and Interpolation Patterns
Key Takeaways
- A resource address identifies instances in configuration and state: aws_instance.web, aws_instance.web[0], aws_instance.web["app"], module.vpc.aws_subnet.private[1], data.aws_ami.al2023
- count uses a 0-based numeric index; for_each uses a quoted key — do not mix aws_instance.web[0] with a for_each resource
- Inside provisioner and connection blocks, self is the parent resource; referring to the resource by name would create a cycle. Provisioners are a last resort and not a 004 focus
- terraform apply -replace=aws_instance.web forces replacement of that address; a moved block records a rename so Terraform does not destroy
- Do not confuse var.name with a resource address, and do not treat module.vpc.aws_subnet.private[1] as a parent-module expression — that shape is a CLI address, while configuration uses module outputs
6.3 Resource Addressing and Interpolation Patterns
Quick Answer: An address is
[module path][resource spec]. Root examples:aws_instance.web,aws_instance.web[0],aws_instance.web["app"],data.aws_ami.al2023. Child-module CLI form:module.vpc.aws_subnet.private[1].countuses a numeric index;for_eachuses a key.selfexists only in provisioner and connection blocks.-replace=aws_instance.webforces replacement. Amovedblock renames without destroying.
This section stays on objective 4b — refer to resource attributes and create cross-resource references — and adds the address vocabulary HashiCorp lists next to that objective. You use the same strings in terraform state show, -target, and -replace. Terraform 1.12 still documents this syntax on Resource Addressing.
The address grammar
Official syntax is two optional parts:
[module path][resource spec]
Omit the module path and you are in the root module. A resource spec is resource_type.resource_name[instance index]. Data sources use the same spec with a data. prefix.
| Address | What it selects |
|---|---|
aws_instance.web | The whole managed resource in the root module. If count or for_each is set, this is all instances |
aws_instance.web[0] | The first instance of a count resource (indexes are 0-based) |
aws_instance.web["app"] | The for_each instance whose key is app |
data.aws_ami.al2023 | The root data source named al2023 |
module.vpc.aws_subnet.private[1] | Inside child module vpc, the count instance [1] of aws_subnet.private |
module.foo[0].module.bar["a"].aws_instance.web | Nested modules, each with its own index or key |
Index versus key is a pure 004 trap:
count = 4→ instances[0],[1],[2],[3].aws_instance.web[0]is legal.aws_instance.web["0"]is a different (usually invalid) key.for_each = toset(["app", "job"])or a map → instances["app"],["job"].aws_instance.web["app"]is legal.aws_instance.web[0]is not.- Omitting the index on a multi-instance resource means all instances in CLI contexts such as
-target. In an HCL expression,aws_instance.webis then a list (count) or a map (for_each), so you still need[0],["app"], a splat, orvalues(...)to reach.id.
CLI address versus configuration expression
This distinction is easy to miss and the exam likes it.
From a parent module's HCL, module.vpc is an object of outputs. You write module.vpc.private_subnet_ids[1]. You cannot reach through the child and write module.vpc.aws_subnet.private[1].id unless the child literally exported an output with that structure — and it should not.
From the CLI, module.vpc.aws_subnet.private[1] is the resource address in state. That is what you pass to -replace, -target, terraform state show, and terraform state mv.
Inside the child module, the resource is just aws_subnet.private[1]. The module.vpc. prefix appears only when you stand outside that module.
| Context | Legal | Illegal or wrong idea |
|---|---|---|
| Parent HCL | module.vpc.private_subnet_ids[1] | module.vpc.aws_subnet.private[1].id as if internals were public |
| CLI / state | module.vpc.aws_subnet.private[1] | var.vpc.aws_subnet.private[1] |
Inside module vpc | aws_subnet.private[1].id | module.vpc.aws_subnet.private[1] (you are already inside) |
| Input variable | var.ami_id | var.aws_instance.web |
var. is never a resource address. var.web is an input variable named web. If the question shows var.aws_instance.web.id, it is wrong.
Interpolation patterns next to addresses
Section 6.2 covered first-class expressions. Addresses show up inside interpolations when you are building a string, not when you are selecting an instance:
resource "aws_instance" "web" {
count = 2
ami = data.aws_ami.al2023.id
tags = {
Name = "web-${count.index}" # interpolation inside a string
}
}
output "first_id" {
value = aws_instance.web[0].id # expression, no ${}
}
output "banner" {
value = "first instance is ${aws_instance.web[0].id}"
}
count.index and each.key / each.value are block-local names. They are not resource addresses. "web-${count.index}" is a string template. aws_instance.web[count.index] (from another resource that also has count) is an indexed reference, still a first-class expression.
Escape a literal ${ in a string with $${. That is a template rule, not an addressing rule.
self, provisioners, and why 004 barely cares
Expressions in provisioner and connection blocks cannot refer to their parent resource by name. That reference would be a dependency cycle: the resource would depend on itself. HashiCorp's provisioner docs say to use the special self object, which is the parent resource. self.public_ip, self.private_ip, and self.id are the usual examples.
resource "aws_instance" "web" {
ami = data.aws_ami.al2023.id
instance_type = "t3.micro"
provisioner "local-exec" {
command = "echo ${self.private_ip}"
}
}
HashiCorp also warns, repeatedly, that provisioners are a last resort. Terraform cannot model them in the plan the way it models create and destroy, and most of them need network access plus credentials onto the box. 004 is not a provisioner exam. Remember self for provisioner and connection blocks (custom precondition / postcondition checks also use self — that is objective 4g). Prefer cloud-init, machine images, or a real configuration-management tool over provisioners.
-replace uses an address
terraform apply -replace=aws_instance.web (or terraform plan -replace=...) instructs Terraform to replace that instance even if configuration would otherwise update it in place or do nothing. Official plan documentation: this is for a degraded remote object you want to recreate with the same configuration. Repeat the flag to replace several addresses. You cannot combine -replace with -destroy. On Terraform 1.12 this is the current workflow; terraform taint is the older path.
If the resource uses count or for_each, pass a specific instance when you mean one object: -replace=aws_instance.web[0] or -replace='aws_instance.web["app"]'. Quotes around a key often need shell quoting.
-target also takes a resource address, but HashiCorp documents it as an exceptional recovery tool, not routine workflow. Do not treat -target as the way you "apply one resource" day to day.
moved on Terraform 1.12, at exam depth
A moved block is current on 1.12 (it shipped in 1.1). 004 lists it under state-management objective 6d; you still need a light 4b reading because the block is written in address syntax:
resource "aws_instance" "web" {
# formerly labeled "old"
}
moved {
from = aws_instance.old
to = aws_instance.web
}
Before planning aws_instance.web, Terraform looks in state for aws_instance.old and renames that object. The next plan does not destroy the instance. You can move a whole resource, a single instance (from = aws_instance.web[0] → to = aws_instance.web["app"]), or a module call (from = module.a → to = module.b). You cannot use moved to turn a managed resource into a data source.
moved is not -replace. Replace recreates the remote object. moved only changes the address in state.
004 traps for addresses and interpolation
aws_instance.web[0]iscount.aws_instance.web["app"]isfor_each. Mixing them is a graph error.module.vpc.aws_subnet.private[1]is a CLI / state address. Parent HCL uses module outputs.var.never selects a resource.data.aws_ami.al2023is the data-source address; droppingdata.looks for a managed resource.selfis not valid in ordinary resource arguments. It is the parent object in provisioner, connection, and custom-condition blocks.-replaceis notmoved, and it is not destroy.
A resource is declared as resource "aws_instance" "web" { for_each = toset(["app", "job"]) }. Which address selects the app instance?
What does terraform apply -replace=aws_instance.web do on Terraform 1.12?
Inside a provisioner or connection block, how do you read the parent aws_instance's public_ip?