Resource #
Resources are the basic unit of a Terraform configuration. Everything you want Terraform to create and manage — a VM, a database, a DNS record, an IAM role — is declared as a resource block. Understanding how resources work, how they interact with each other, and how Terraform manages their lifecycle is the foundation of everything more complex in Terraform.
Resource Block Anatomy #
Every resource block has a consistent structure. The parts are simple, but each has a very specific role.
resource "<provider>_<type>" "<local_name>" {
argument = value
}
# Real example:
resource "aws_instance" "web_server" {
# │ │ │
# │ │ └── Local name (only for internal references)
# │ └────────────── Resource type (from the AWS provider)
# └───────────────────────── Provider prefix (required, automatically determines the provider)
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-server"
Environment = "production"
}
}
The local name (web_server above) only has meaning inside the Terraform configuration — it doesn’t appear in the AWS Console. What appears in AWS is the value of the tags.Name argument. The local name is used to reference this resource from elsewhere in the configuration.
The resource type (aws_instance) consists of two parts separated by an underscore: the provider prefix (aws) and the specific type (instance). Terraform uses this prefix to determine which provider is responsible for managing the resource.
# Examples of various resource types from different providers:
resource "aws_instance" "web" { } # AWS EC2
resource "aws_s3_bucket" "data" { } # AWS S3
resource "google_compute_instance" "app" { }# GCP Compute
resource "cloudflare_record" "dns" { } # Cloudflare DNS
resource "kubernetes_deployment" "api" { } # Kubernetes
resource "postgresql_role" "admin" { } # PostgreSQL
Cross-Resource References #
One resource can reference attributes of another resource. This is what forms the dependency graph and lets resources connect to each other dynamically. The reference format is always <type>.<local_name>.<attribute>.
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
}
resource "aws_subnet" "public" {
# Reference to another resource's attribute
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "ap-southeast-1a"
}
resource "aws_security_group" "web" {
name = "web-sg"
vpc_id = aws_vpc.main.id # Reference to the same VPC
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id # from the subnet
vpc_security_group_ids = [aws_security_group.web.id] # from the security group
}
These references aren’t just value insertion — they also declare a dependency. When you write aws_vpc.main.id, Terraform knows the subnet must be created after the VPC. The dependency graph is calculated automatically from these references.
flowchart TD
VPC["aws_vpc.main"] --> SUBNET["aws_subnet.public"]
VPC --> SG["aws_security_group.web"]
SUBNET --> EC2["aws_instance.web"]
SG --> EC2
style VPC fill:#e3f2fd,stroke:#1565c0
style SUBNET fill:#e8f5e9,stroke:#2e7d32
style SG fill:#e8f5e9,stroke:#2e7d32
style EC2 fill:#fff3e0,stroke:#e65100Generated Attributes #
Some attributes are only available after the resource is created — like IP addresses, ARNs, or IDs generated by the cloud provider. These attributes are stored in the state and can be referenced by other resources or exposed as outputs.
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
# These attributes are only available after apply
output "public_ip" {
value = aws_instance.web.public_ip # IP from AWS
}
output "arn" {
value = aws_instance.web.arn # ARN from AWS
}
# Reference to another resource
resource "aws_route53_record" "app" {
zone_id = var.dns_zone_id
name = "app.example.com"
type = "A"
ttl = 300
records = [aws_instance.web.public_ip] # IP of the instance above
}
The full list of attributes available for each resource type can be found in each provider’s documentation at registry.terraform.io. Attributes you can set are called arguments, while those you can only read after creation are called attributes.
Resource Lifecycle #
Every resource has a lifecycle: it’s created, possibly modified, and eventually deleted. Terraform manages this automatically, but you can customize its behavior through the lifecycle block.
create_before_destroy
#
By default, when Terraform needs to replace a resource (for example, because a force_new attribute changed), it deletes the old one first, then creates the new one. With create_before_destroy = true, the order is reversed — the new resource is created first, then the old one is deleted. This matters for resources that can’t tolerate downtime.
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
lifecycle {
create_before_destroy = true
# If the AMI changes → create the new instance first → then delete the old one
# Without this: the old instance is deleted first → a gap with no server
}
}
prevent_destroy
#
Prevents a resource from being accidentally deleted. If terraform plan produces a destroy operation for this resource, Terraform will error.
resource "aws_s3_bucket" "critical_data" {
bucket = "company-critical-data"
lifecycle {
prevent_destroy = true
# terraform destroy or removing the block from config → ERROR
# Protection for resources that must never be deleted
}
}
ignore_changes
#
Tells Terraform to ignore changes to specific attributes. Useful when an attribute is managed outside Terraform (for example, by auto-scaling, monitoring agents, or manual operations).
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
tags = {
Name = "web-server"
LastModified = timestamp()
}
lifecycle {
ignore_changes = [
tags["LastModified"], # This tag changes on every apply
ami, # AMI is managed by a separate pipeline
]
}
}
replace_triggered_by
#
Triggers a replacement of this resource when a specific resource or attribute changes. Useful for ensuring a resource is replaced together with its dependency.
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
lifecycle {
replace_triggered_by = [var.ami_id]
# If var.ami_id changes → this instance is replaced
# More explicit than the force_new determined by the provider
}
}
Meta-Arguments: count and for_each
#
Terraform provides meta-arguments for creating multiple resource instances at once. Meta-arguments aren’t resource attributes — they’re instructions to Terraform about how to create the resource.
count — for Identical Resources
#
resource "aws_subnet" "public" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index}.0/24"
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "public-subnet-${count.index + 1}"
}
}
# References:
# aws_subnet.public[0].id
# aws_subnet.public[1].id
# aws_subnet.public[2].id
for_each — for Resources with Distinct Identities
#
resource "aws_iam_user" "team" {
for_each = toset(["alice", "bob", "charlie"])
name = each.key
tags = {
Name = each.key
}
}
# References:
# aws_iam_user.team["alice"].arn
# aws_iam_user.team["bob"].arn
# aws_iam_user.team["charlie"].arn
# Can also come from a map:
resource "aws_s3_bucket" "buckets" {
for_each = {
logs = "app-logs-2024"
assets = "app-assets-2024"
backups = "app-backups-2024"
}
bucket = each.value
tags = {
Purpose = each.key
}
}
flowchart TD
A["Choose a meta-argument"] --> B{"Identical resources\nor distinct ones?"}
B -->|"Identical"| C["count"]
B -->|"Distinct identities"| D["for_each"]
C --> E[""Reference: resource.name[index""]
D --> F[""Reference: resource.name[key""]
E --> G["Problem: removing a middle element\n→ indices shift\n→ resources get recreated"]
F --> H["Safe: removing a middle element\n→ other keys unaffected"]
style C fill:#fff3e0,stroke:#e65100
style D fill:#e8f5e9,stroke:#2e7d32
style G fill:#ffebee,stroke:#c62828
style H fill:#e8f5e9,stroke:#2e7d32Be careful withcounton existing resources. If you remove an element from the middle of a list, all indices after it shift — Terraform will delete and recreate all resources after the removed element. Usefor_eachfor resources with unique identities to be safer.
Conditional Resources with Count #
count can also be used to create a resource conditionally — count = 0 means the resource isn’t created.
resource "aws_nat_gateway" "main" {
count = var.enable_nat ? 1 : 0
allocation_id = aws_eip.nat[0].id
subnet_id = aws_subnet.public[0].id
}
# References to conditional resources need care:
# aws_nat_gateway.main[0].id → errors if count = 0
# Solution: use try() or a conditional
locals {
nat_gateway_id = try(aws_nat_gateway.main[0].id, null)
}
The provider Meta-Argument
#
When using multi-region or multi-account setups, you can specify which provider manages a given resource.
resource "aws_s3_bucket" "replica" {
provider = aws.us_east # Use the aliased provider
bucket = "app-replica-us"
}
Resources Removed from the Configuration #
When you remove a resource block from a .tf file and run terraform apply, Terraform will delete that resource from the infrastructure. This is the default behavior you must understand — removing from the config means removing from the infrastructure.
# Previously in the config:
# resource "aws_instance" "old_server" {
# ami = "ami-0abcdef1234567890"
# instance_type = "t3.micro"
# }
# After removing from the config:
# $ terraform plan
# aws_instance.old_server will be destroyed
# Plan: 0 to add, 0 to change, 1 to destroy.
# If you don't want the resource deleted, there are several options:
# 1. prevent_destroy (before removing the block)
# 2. terraform state rm (remove from state without destroying)
# 3. moved block (move to a new resource)
moved Block for Renaming/Restructuring
#
When you want to change a resource’s local name or move it into a module without deleting and recreating it, use a moved block.
# Previously:
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
# Want to rename it to "app_server" without recreating:
resource "aws_instance" "app_server" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
moved {
from = aws_instance.web
to = aws_instance.app_server
# Terraform knows this is the same resource → no destroy/create
}
Summary #
- Resources are Terraform’s basic unit — every piece of infrastructure is declared as a
resource "<type>" "<local_name>"block.- Local names are only for internal references — they don’t affect the resource name in the cloud, only used for references within the configuration.
- Cross-resource references (
aws_vpc.main.id) build the dependency graph automatically — Terraform knows the execution order from these references.- The lifecycle block lets you customize behavior:
create_before_destroy(zero-downtime),prevent_destroy(protection),ignore_changes(ignore external attributes).- Use
for_eachinstead ofcountfor resources with distinct identities — safer when changes happen in the middle of a list because keys don’t shift.countfor conditionals —count = 0means the resource isn’t created, but be careful when referencing conditional resources.- The
movedblock enables renaming/restructuring without destroy/create — very useful when refactoring configurations.