Resources
Resources are the most important element in Terraform. Each resource block describes one or more infrastructure objects, such as virtual machines, storage buckets, or DNS records.
Resource Syntax
A resource block declares a resource of a specific type with a specific local name:
resource "<RESOURCE_TYPE>" "<NAME>" {
# Configuration arguments
}
Example
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
}
The resource type (aws_instance) and name (web) together form a unique identifier: aws_instance.web.
Resource Types
Each provider defines resource types. The type name format is:
Examples:
aws_instance - AWS provider, instance resource
azurerm_virtual_machine - Azure provider, virtual machine resource
google_compute_instance - Google Cloud provider, compute instance resource
Resource Arguments
Provider-Specific Arguments
Each resource type has its own set of required and optional arguments:
resource "aws_instance" "web" {
# Required
ami = "ami-12345678"
instance_type = "t2.micro"
# Optional
subnet_id = var . subnet_id
vpc_security_group_ids = [ aws_security_group . web . id ]
tags = {
Name = "WebServer"
Environment = "production"
}
}
Nested Blocks
Many resources support nested configuration blocks:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
network_interface {
device_index = 0
description = "Main network interface"
}
}
Meta-arguments change resource behavior and work with any resource type.
count
Create multiple instances using an integer:
resource "aws_instance" "web" {
count = 3
ami = "ami-12345678"
instance_type = "t2.micro"
tags = {
Name = "web- ${ count . index } "
}
}
Reference instances:
# All instances
all_ips = aws_instance . web [ * ] . private_ip
# Specific instance
first_ip = aws_instance . web [ 0 ] . private_ip
for_each
Create instances from a map or set:
resource "aws_instance" "web" {
for_each = {
web1 = "t2.micro"
web2 = "t2.small"
}
ami = "ami-12345678"
instance_type = each . value
tags = {
Name = each.key
}
}
Reference instances:
web1_ip = aws_instance . web [ "web1" ] . private_ip
A resource cannot use both count and for_each - they are mutually exclusive.
depends_on
Explicitly declare dependencies:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
depends_on = [
aws_security_group . firewall ,
]
}
Terraform automatically determines most dependencies. Use depends_on only when necessary.
provider
Select a non-default provider configuration:
resource "aws_instance" "web" {
provider = aws . west
ami = "ami-12345678"
instance_type = "t2.micro"
}
lifecycle
Customize resource lifecycle behavior:
resource "aws_security_group" "firewall" {
lifecycle {
create_before_destroy = true
prevent_destroy = true
ignore_changes = [
description ,
]
}
}
Lifecycle Arguments
Create replacement before destroying the original. Useful for zero-downtime updates.
Prevent accidental resource destruction. Terraform will error if you try to destroy this resource.
Ignore changes to specified attributes. Use all to ignore all changes. ignore_changes = [ ami , instance_type ]
# or
ignore_changes = all
Replace this resource when specified resources change: lifecycle {
replace_triggered_by = [
aws_instance . web [ 1 ],
aws_security_group . firewall . id
]
}
Provisioners
Provisioners run scripts or commands during resource creation or destruction:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
provisioner "local-exec" {
command = "echo ${ self . private_ip } >> private_ips.txt"
}
provisioner "remote-exec" {
inline = [
"sudo apt-get update" ,
"sudo apt-get install -y nginx" ,
]
}
}
Provisioners are a last resort. Use them only when no other option exists. Consider cloud-init, user data, or configuration management tools instead.
Provisioner Types
local-exec - Run commands on the machine running Terraform
remote-exec - Run commands on the remote resource
file - Copy files to the remote resource
Destroy-Time Provisioners
provisioner "local-exec" {
when = destroy
command = "./cleanup.sh ${ self . id } "
}
Connection Blocks
Configure how provisioners connect to resources:
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro"
connection {
type = "ssh"
host = self . public_ip
user = "ubuntu"
private_key = file ( "~/.ssh/id_rsa" )
}
provisioner "remote-exec" {
inline = [ "echo hello" ]
}
}
Preconditions and Postconditions
Validate assumptions about resources:
resource "aws_instance" "web" {
ami = var . ami_id
instance_type = var . instance_type
lifecycle {
precondition {
condition = var . instance_type != "t2.nano"
error_message = "Instance type cannot be t2.nano."
}
postcondition {
condition = self . private_ip != ""
error_message = "Instance must have a private IP."
}
}
}
Resource Addresses
Reference resources in your configuration:
# Simple resource
aws_instance . web
# Resource with count
aws_instance . web [ 0 ]
aws_instance . web [ * ] # All instances
# Resource with for_each
aws_instance . web [ "primary" ]
# Resource attributes
aws_instance . web . id
aws_instance . web . private_ip
Resource Behavior
Create
Terraform creates resources that don’t exist:
Update
Terraform updates resources when arguments change:
resource "aws_instance" "web" {
instance_type = "t2.small" # Changed from t2.micro
}
Replace
Some changes require replacement (destroying and recreating):
resource "aws_instance" "web" {
ami = "ami-87654321" # AMI change requires replacement
}
Destroy
Remove resources from configuration:
Data Sources
Read information about existing infrastructure:
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = [ "ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*" ]
}
filter {
name = "virtualization-type"
values = [ "hvm" ]
}
owners = [ "099720109477" ]
}
resource "aws_instance" "web" {
ami = data . aws_ami . ubuntu . id
}
Ephemeral Resources
Short-lived resources that exist only during operations:
ephemeral "aws_secret" "auth" {
for_each = local . auths
input = each . value
depends_on = [ aws_instance . depends ]
}
Ephemeral resources are not stored in state and exist only during Terraform operations.
Best Practices
Use Descriptive Names Choose clear, descriptive names for resources that indicate their purpose.
Minimize Provisioners Avoid provisioners when possible. Use native resource features instead.
Explicit Dependencies Let Terraform infer dependencies. Use depends_on only when necessary.
Lifecycle Management Use lifecycle arguments to prevent accidental destruction of critical resources.
Next Steps
Providers Configure providers for resources
Variables Parameterize resource configurations