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
Last updated: August 2026

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]. count uses a numeric index; for_each uses a key. self exists only in provisioner and connection blocks. -replace=aws_instance.web forces replacement. A moved block 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.

AddressWhat it selects
aws_instance.webThe 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.al2023The 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.webNested 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.web is then a list (count) or a map (for_each), so you still need [0], ["app"], a splat, or values(...) 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.

ContextLegalIllegal or wrong idea
Parent HCLmodule.vpc.private_subnet_ids[1]module.vpc.aws_subnet.private[1].id as if internals were public
CLI / statemodule.vpc.aws_subnet.private[1]var.vpc.aws_subnet.private[1]
Inside module vpcaws_subnet.private[1].idmodule.vpc.aws_subnet.private[1] (you are already inside)
Input variablevar.ami_idvar.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.ato = 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] is count. aws_instance.web["app"] is for_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.al2023 is the data-source address; dropping data. looks for a managed resource.
  • self is not valid in ordinary resource arguments. It is the parent object in provisioner, connection, and custom-condition blocks.
  • -replace is not moved, and it is not destroy.
Test Your Knowledge

A resource is declared as resource "aws_instance" "web" { for_each = toset(["app", "job"]) }. Which address selects the app instance?

A
B
C
D
Test Your Knowledge

What does terraform apply -replace=aws_instance.web do on Terraform 1.12?

A
B
C
D
Test Your Knowledge

Inside a provisioner or connection block, how do you read the parent aws_instance's public_ip?

A
B
C
D