What is a Resource? #
In the Concept section, you were introduced to resources as the basic unit of Terraform configuration. In this section, we go deeper — how resources interact with each other, how Terraform manages their lifecycle, and how to write robust resources for production infrastructure. This article builds the foundation for the deeper topics in this section: lifecycle, dependency, and operation.
Resource vs Data Source #
One of the most common confusions in Terraform is the difference between resource and data. Both are configuration blocks, but their roles are very different.
flowchart LR
subgraph "resource (Manage)"
R1["aws_vpc.main"] --> R2["Terraform CREATES\nthis resource"]
R2 --> R3["Terraform UPDATES\nif the config changes"]
R3 --> R4["Terraform DELETES\nif destroyed"]
end
subgraph "data (Read)"
D1["aws_ami.ubuntu"] --> D2["Terraform READS\nsomething that exists"]
D2 --> D3["Cannot be\ncreated/deleted"]
D3 --> D4["Only a reference\nfor other resources"]
end
style R1 fill:#e3f2fd,stroke:#1565c0
style D1 fill:#e8f5e9,stroke:#2e7d32# resource — Terraform CREATES and MANAGES this
# Terraform is responsible for its lifecycle: create, update, destroy
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
# data — Terraform READS this, doesn't create it
# The resource already exists outside Terraform, you just want to reference it
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-*-22.04-amd64-server-*"]
}
}
# Using both together
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id # AMI from the data source
instance_type = "t3.micro"
vpc_id = aws_vpc.main.id # VPC from the resource
}
| Aspect | resource | data |
|---|---|---|
| Purpose | Create and manage new resources | Read existing resources |
| Lifecycle | Create, update, destroy | None |
| In state? | ✅ Yes, stored and tracked | ✅ Yes, but only as a cache |
| Created by Terraform? | ✅ Yes | ❌ No, already exists outside |
| Examples | Create VPC, EC2, S3 | Read an AMI, read an existing VPC |
Complete Resource Block Anatomy #
flowchart TD
A["Resource Block"] --> B["Required Arguments\n(ami, instance_type)"]
A --> C["Optional Arguments\n(subnet_id, tags)"]
A --> D["Nested Blocks\n(tags, root_block_device)"]
A --> E["Meta-Arguments\n(depends_on, count,\nfor_each, provider,\nlifecycle)"]
B --> B1["Mandatory\nProvided by the provider"]
C --> C1["Optional\nProvided by the provider"]
D --> D1["Nested structure\nProvided by the provider"]
E --> E1["Provided by Terraform\nApplies to ALL\nresources"]
style A fill:#e3f2fd,stroke:#1565c0
style E fill:#fff3e0,stroke:#e65100
style E1 fill:#fff3e0,stroke:#e65100# Complete resource block structure with all its parts
resource "<PROVIDER>_<TYPE>" "<NAME>" {
# ─────────────────────────────────────────
# ARGUMENTS — configuration specific to this resource
# ─────────────────────────────────────────
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
# Nested blocks
tags = {
Name = "web-server"
Environment = var.environment
}
# ─────────────────────────────────────────
# META-ARGUMENTS — apply to all resources
# ─────────────────────────────────────────
# Explicit dependency
depends_on = [aws_iam_role_policy_attachment.node]
# Create multiple instances
count = 3
# or: for_each = var.instance_map
# Use a non-default provider
provider = aws.us_east
# Customize the lifecycle
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [tags["LastModified"]]
}
}
Meta-arguments (depends_on, count, for_each, provider, lifecycle) are special arguments recognized by Terraform itself — not provider arguments. They apply to all resources, regardless of type or provider.
How Resources Are Represented in State #
Every resource created by Terraform is stored in state with its unique identity. Understanding this structure helps when you need to interact with state directly.
flowchart TD
A["State File"] --> B["Resource Type\naws_instance"]
B --> C["Resource Name\nweb"]
C --> D["Resource Address\naws_instance.web"]
D --> E["Attributes\nid, ami, ip, tags"]
F["Resource + count"] --> G[""aws_instance.workers[0""]
F --> H[""aws_instance.workers[1""]
F --> I[""aws_instance.workers[2""]
J["Resource + for_each"] --> K["aws_subnet.public["a"]"]
J --> L["aws_subnet.public["b"]"]
J --> M["aws_subnet.public["c"]"]
style D fill:#e3f2fd,stroke:#1565c0
style G fill:#e8f5e9,stroke:#2e7d32
style K fill:#e8f5e9,stroke:#2e7d32# View all resources in state
terraform state list
# Output:
# aws_instance.web
# aws_instance.workers[0]
# aws_instance.workers[1]
# aws_instance.workers[2]
# module.vpc.aws_vpc.main
# module.vpc.aws_subnet.public["ap-southeast-1a"]
# View details of one resource in state
terraform state show aws_instance.web
# Output:
# # aws_instance.web:
# resource "aws_instance" "web" {
# ami = "ami-0abcdef1234567890"
# id = "i-0abcdef1234567890"
# instance_type = "t3.micro"
# private_ip = "10.0.1.42"
# public_ip = "54.123.45.67"
# ... (all attributes, including the ones AWS generated)
# }
Resource addresses in state follow the format: <type>.<name> for regular resources, <type>.<name>[index] for resources with count, and <type>.<name>["key"] for resources with for_each.
| Resource Pattern | Address Format | Example |
|---|---|---|
| Regular resource | <type>.<name> | aws_instance.web |
With count | <type>.<name>[index] | aws_instance.workers[0] |
With for_each | <type>.<name>["key"] | aws_subnet.public["a"] |
| Inside a module | module.<name>.<type>.<name> | module.vpc.aws_vpc.main |
Resource Addresses and How to Reference Them #
Every resource has an address that can be referenced from elsewhere in the configuration.
flowchart LR
A["Cross-resource\nreferences"] --> B["Format:\ntype.name.attribute"]
B --> C["aws_vpc.main.id\n(id of the VPC)"]
B --> D["aws_subnet.public.id\n(id of the subnet)"]
B --> E["aws_instance.web.public_ip\n(IP of the instance)"]
F["Splat expressions"] --> G[""aws_subnet.private[*"].id\nAll subnet IDs at once"]
style B fill:#e3f2fd,stroke:#1565c0# Reference format: <TYPE>.<NAME>.<ATTRIBUTE>
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id # Reference to the "id" attribute of aws_vpc.main
}
# For resources with count:
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 10}.0/24"
}
resource "aws_route_table_association" "private" {
count = 3
subnet_id = aws_subnet.private[count.index].id # Access per index
route_table_id = aws_route_table.private.id
}
# All subnets at once using a splat expression:
output "private_subnet_ids" {
value = aws_subnet.private[*].id # ["subnet-001", "subnet-002", "subnet-003"]
}
Consistent Writing Patterns #
Consistency in how you write resources makes configurations easier to read and review.
# RECOMMENDED PATTERNS:
resource "aws_instance" "web" {
# 1. Required arguments (mandatory) at the top
ami = var.ami_id
instance_type = var.instance_type
# 2. Important optional arguments
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.web.name
# 3. Nested blocks
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
# 4. Tags always at the end
tags = merge(
var.common_tags,
{
Name = "web-server"
Role = "web"
}
)
# 5. Lifecycle at the very end (if any)
lifecycle {
create_before_destroy = true
}
}
Resource Addressing #
Resource addressing enables specific references to particular resources.
# Format: module.module_name.resource_type.resource_name[index]
# Root level resource
aws_instance.web
# Resource in a module
module.networking.aws_vpc.main
# Resource in a count
aws_instance.web[0]
aws_instance.web[1]
# Resource in a for_each
aws_instance.web["app-server"]
aws_instance.web["db-server"]
# Nested module
module.networking.module.subnets.aws_subnet.private[0]
# Resource reference for dependencies
resource "aws_instance" "app" {
subnet_id = module.networking.private_subnet_ids[0]
# Implicit dependency on module.networking
}
Resource Metadata #
# Every resource has metadata you can access
resource "aws_instance" "web" {
ami = "ami-12345"
instance_type = "t3.micro"
tags = {
Name = "web-server"
}
}
# Available metadata:
# aws_instance.web.id → Instance ID
# aws_instance.web.arn → ARN
# aws_instance.web.private_ip → Private IP
# aws_instance.web.public_ip → Public IP (if any)
# aws_instance.web.availability_zone → AZ
# aws_instance.web.instance_state → State (running, stopped)
# View all attributes of a resource
terraform state show aws_instance.web
# Shows ALL attributes stored in state
Resource Lifecycle States #
RESOURCE STATES:
1. pending_create: Will be created on apply
2. creating: Currently being created
3. created: Already created (active)
4. pending_update: Will be updated on apply
5. updating: Currently being updated
6. pending_destroy: Will be deleted on apply
7. destroying: Currently being deleted
8. destroyed: Already deleted
TERRAFORM PLAN ICONS:
+ Create (new resource)
~ Update (changed resource)
- Destroy (deleted resource)
-/+ Replace (destroy + create)
<= Read (data source)
# View a resource's state
terraform state show aws_instance.web
# List all resources in state
terraform state list
# List with a filter
terraform state list | grep aws_instance
Summary #
resourcecreates,datareads — resources are managed by Terraform, data sources are only read from what already exists.- Meta-arguments (
depends_on,count,for_each,provider,lifecycle) apply to all resources, not provider arguments.- Resource addresses in state follow the
<type>.<name>format — important to know forterraform stateoperations and cross-resource references.- References use the
<type>.<name>.<attribute>format — this is what builds Terraform’s automatic dependency graph.- Splat expressions (
[*]) access all instances of acount-based resource at once.- Writing consistency — required args first, then optional, nested blocks, tags, lifecycle — makes reviews easier.