Resources and Resource Addressing
A resource is the fundamental building block of Terraform: one block manages one real object — an EC2 instance, an S3 bucket, an IAM role. Every resource has a unique address, like aws_instance.web, that Terraform uses internally to track it and that you type by hand into the operational commands — state, import, taint, and -target. The day you need to move a resource in state or import an existing one, the address syntax is the thing standing between you and the fix.
Most newcomers never think about addresses until something breaks, then discover that count and for_each change the address shape in ways the error messages assume you already know. Learning the forms now saves an afternoon of confusion later.
Resource Block Anatomy
A resource block has three parts: the keyword resource, the type, and a local name. In resource "aws_instance" "web", the type aws_instance is fixed by the provider — you cannot invent it — and the local name web is yours to choose. The arguments inside the braces configure the object. The type plus the local name together form the address, which is why both must be unique in combination within a module.
resource "aws_instance" "web" { ami = "ami-0abc123" instance_type = "t3.micro" tags = { Name = "web" } }
Read that as: the type is aws_instance, the local name is web, and the address is therefore aws_instance.web. The arguments — ami, instance_type, tags — are inputs the provider documents on the Registry. There is no separate "ID" you assign; Terraform learns the real AWS instance ID after it creates the object and stores that mapping in state.
Resource Addresses
The base address is type.name — aws_instance.web. The moment a resource uses count, it becomes a list, and each instance is addressed by integer index: aws_instance.web[0], aws_instance.web[1]. With for_each it becomes a map, and each instance is addressed by string key: aws_instance.web["api"]. This is the single most common stumbling block — once a resource has count or for_each, the bare aws_instance.web is no longer a valid address, and Terraform will reject it.
# count produces integer-indexed instances aws_instance.web[0] aws_instance.web[1] # for_each produces string-keyed instances aws_instance.web["api"] aws_instance.web["worker"]
You will type these into commands like terraform state show 'aws_instance.web["api"]' and terraform import 'aws_instance.web[0]' i-0abc123. The quoting matters in a shell because the brackets and quotes are otherwise interpreted by the shell, not Terraform.
Attributes and References
Every resource exposes attributes that other resources can read: aws_instance.web.id, aws_instance.web.private_ip, aws_vpc.main.arn. Some attributes are set in your config and known immediately; others — an instance ID, an auto-assigned IP — are known after apply, meaning Terraform cannot fill them in at plan time because the cloud assigns them at creation. Referencing one of these in a place that needs a concrete value during planning produces an error, and it is a frequent source of confusion.
The other reason to reference an attribute rather than hardcode its value is dependency ordering. When the subnet below reads aws_vpc.main.id, Terraform infers that the VPC must exist before the subnet, and orders the graph accordingly. Hardcoding the VPC's ID as a string would break that inference and let Terraform try to build them in the wrong order.
resource "aws_vpc" "main" { cidr_block = "10.0.0.0/16" } resource "aws_subnet" "app" { vpc_id = aws_vpc.main.id # subnet now depends on the VPC cidr_block = "10.0.1.0/24" }
Meta-Arguments
A handful of arguments work on every resource type regardless of provider: count and for_each create multiple instances, provider selects a non-default provider configuration, depends_on declares an explicit dependency Terraform cannot infer, and lifecycle overrides default create/update/destroy behavior. These are not provided by the AWS provider — they are part of the Terraform language itself, which is why they behave identically on an aws_instance, a google_compute_instance, or any other resource. The next topics in this chapter cover dependencies and the lifecycle block in depth.
Module Addressing
When a resource lives inside a module, its address gains a module. prefix. A resource named this of type aws_vpc inside a module called network is addressed from the root as module.network.aws_vpc.this. Nesting compounds: a module inside a module produces module.platform.module.network.aws_vpc.this. You need these full paths whenever you run state mv, import, or -target against something inside a module — which, in any real codebase, is most things.
- Confusing the type with the local name and writing
aws_instance.idinstead ofaws_instance.web.id— the type alone is not an address. - Forgetting the index on a resource that uses
countorfor_each, soaws_instance.web.iderrors because the real address is nowaws_instance.web[0].id. - Referencing a "known after apply" attribute where a value is needed at plan time, hitting an error because the cloud has not assigned it yet.
- Hardcoding an ID that a sibling resource exposes as an attribute, which removes the implicit dependency and lets Terraform build things in the wrong order.
- Naming local resources after their type (
aws_instance "aws_instance") so the address reads as a stutter and tells you nothing about the role.
- Reference other resources' attributes rather than hardcoding IDs, so Terraform infers the dependency and orders the graph for you.
- Name local resources for their role —
web,db,app— never for their type, since the type is already in the address. - Learn the indexed address forms early; every
state,import, and-targetcommand on a counted resource needs them. - Use the Registry's "Attributes Reference" to know which attributes exist and which are only known after apply.
- Quote the full address — including module path and index — when passing it to a CLI command in a shell.
Knowledge Check
A resource aws_instance.web uses for_each over a map with keys api and worker. How do you address the api instance's ID?
aws_instance.web["api"].idaws_instance.web.idaws_instance.web[0].idaws_instance.api.id
Why reference aws_vpc.main.id in a subnet instead of pasting the VPC's literal ID string?
- The reference makes Terraform infer that the VPC must exist first and orders the graph accordingly
- Literal VPC ID strings are outright rejected by the AWS provider for the subnet's vpc_id argument
- Attribute references are encrypted in the state file while plain literal ID strings are stored in clear text
- It is purely a cosmetic style preference with no behavioral difference between the two approaches
What does "known after apply" mean for an attribute like an instance ID?
- The cloud assigns it at creation, so Terraform cannot supply it at plan time
- The attribute is only readable after you run a separate refresh command
- The value is hidden from state until the apply completes
- It can only be used inside the same resource block, never referenced elsewhere
How do you address a resource aws_vpc.this that lives inside a module named network?
module.network.aws_vpc.thisnetwork.aws_vpc.thisaws_vpc.this.module.networkmodule.network.this
You got correct