Terraform Resource Meta-Arguments
Introduction
When working with Terraform, resources are the most important building blocks. They represent infrastructure objects like virtual machines, networks, or database instances. While each resource type has its own specific configuration attributes, Terraform provides a set of special arguments called Meta-Arguments that work across all resource types.
Meta-arguments allow you to change how Terraform handles resources without altering their core functionality. They provide powerful ways to customize resource behavior, create multiple similar resources, establish dependencies, and control resource lifecycle events.
In this guide, we'll explore the five main meta-arguments that can be used with any resource type:
depends_on
- Specify explicit dependenciescount
- Create multiple resource instancesfor_each
- Create multiple instances from a map or setprovider
- Select a specific provider configurationlifecycle
- Configure how Terraform handles resource lifecycle events
Let's dive into each of these meta-arguments and see how they can supercharge your Terraform configurations!
The depends_on Meta-Argument
Understanding Resource Dependencies
In Terraform, resources often depend on other resources. For example, you might need to create a virtual network before launching virtual machines within it. Terraform automatically detects many dependencies based on attribute references (resource_type.name.attribute
), but sometimes you need to declare dependencies explicitly.
This is where the depends_on
meta-argument comes in. It allows you to specify that a resource must be created after another resource, even when there's no direct reference between them.
Syntax and Usage
resource "aws_instance" "app_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
# Explicit dependency
depends_on = [
aws_s3_bucket.example,
aws_dynamodb_table.example
]
}
Real-world Example
Consider a scenario where you're setting up a web application that requires:
- A database server
- A backend application server
- A load balancer
Even if there's no direct attribute reference, you might want to ensure the database is created before the application server:
resource "aws_db_instance" "database" {
engine = "mysql"
allocated_storage = 10
instance_class = "db.t3.micro"
name = "mydb"
username = "admin"
password = var.db_password
skip_final_snapshot = true
}
resource "aws_instance" "backend" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = <<-EOF
#!/bin/bash
echo "DB_HOST=${aws_db_instance.database.address}" >> /etc/environment
EOF
# Even though we reference the database above, we might want to be explicit
depends_on = [aws_db_instance.database]
}
resource "aws_lb" "frontend" {
name = "app-lb"
internal = false
load_balancer_type = "application"
# The load balancer should only be created after the backend server is ready
depends_on = [aws_instance.backend]
}
Best Practices
- Only use
depends_on
when necessary - Terraform's automatic dependency detection is often sufficient - Don't overuse
depends_on
as it can slow down your deployments - Use
depends_on
for hidden dependencies like initialization scripts that aren't captured through attribute references
The count Meta-Argument
Creating Multiple Resource Instances
The count
meta-argument allows you to create multiple instances of a resource without having to write the same resource block multiple times. It creates a numbered collection of resources starting with index 0.
Syntax and Usage
resource "aws_instance" "server" {
count = 3
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
tags = {
Name = "Server-${count.index}"
}
}
In this example, Terraform will create three EC2 instances named "Server-0", "Server-1", and "Server-2".
Accessing Individual Resources
When using count
, each resource instance becomes part of an array. You can reference these instances individually using square bracket notation:
output "first_server_ip" {
value = aws_instance.server[0].private_ip
}
output "all_server_ips" {
value = aws_instance.server[*].private_ip
}
The special [*]
symbol is used to get an attribute from all instances.
Conditional Resources with Count
You can use count
to conditionally create resources by setting it to either 0 or 1:
resource "aws_instance" "dev_server" {
count = var.environment == "development" ? 1 : 0
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
Limitations of Count
While count
is useful, it has some limitations:
- It's based on indices, which can cause issues when removing items from the middle of the list
- It can only create identical resources with minor variations
- It works best with simple, ordered lists
For more complex scenarios, consider using for_each
instead.
The for_each Meta-Argument
Creating Multiple Resources from Maps or Sets
The for_each
meta-argument is more powerful than count
for creating multiple resources. Instead of creating resources based on a number, it creates one resource per item in a map or set.
Syntax and Usage
With a set:
resource "aws_iam_user" "example" {
for_each = toset(["john", "mary", "peter"])
name = each.value
}
With a map:
resource "aws_iam_user" "example" {
for_each = {
john = "admin"
mary = "developer"
peter = "analyst"
}
name = each.key
tags = {
role = each.value
}
}
Accessing Individual Resources
When using for_each
, you access resources using the key instead of a numeric index:
output "john_arn" {
value = aws_iam_user.example["john"].arn
}
Real-world Example: Multiple EC2 Instances with Different Configurations
locals {
instances = {
"web-server" = {
instance_type = "t2.micro"
ami = "ami-0c55b159cbfafe1f0"
subnet_id = "subnet-12345"
},
"app-server" = {
instance_type = "t2.small"
ami = "ami-0c55b159cbfafe1f0"
subnet_id = "subnet-67890"
},
"db-server" = {
instance_type = "t2.medium"
ami = "ami-0c55b159cbfafe1f0"
subnet_id = "subnet-abcde"
}
}
}
resource "aws_instance" "servers" {
for_each = local.instances
instance_type = each.value.instance_type
ami = each.value.ami
subnet_id = each.value.subnet_id
tags = {
Name = each.key
}
}
Benefits of for_each over count
- Resources are identified by meaningful keys instead of numeric indices
- Adding or removing items in the middle doesn't affect other resources
- It handles more complex data structures
- It's easier to manage resources with varied attributes
The provider Meta-Argument
Using Multiple Provider Configurations
In Terraform, a provider is a plugin that interacts with APIs to create, update, and delete resources. Sometimes you need to use multiple configurations of the same provider, such as deploying to different AWS regions or different cloud accounts.
The provider
meta-argument allows you to specify which provider configuration a resource should use.
Syntax and Usage
First, define multiple provider configurations:
# Default provider configuration
provider "aws" {
region = "us-west-1"
}
# Additional provider configuration with alias
provider "aws" {
alias = "east"
region = "us-east-1"
}
Then specify which provider configuration to use for a resource:
resource "aws_instance" "example" {
provider = aws.east
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}
Real-world Example: Multi-region Deployment
# Primary region provider
provider "aws" {
region = "us-west-2"
}
# Disaster recovery region provider
provider "aws" {
alias = "dr"
region = "us-east-1"
}
# S3 bucket in primary region
resource "aws_s3_bucket" "primary" {
bucket = "my-app-data-primary"
}
# S3 bucket in DR region
resource "aws_s3_bucket" "backup" {
provider = aws.dr
bucket = "my-app-data-backup"
}
# Replication configuration
resource "aws_s3_bucket_replication_configuration" "replication" {
depends_on = [aws_s3_bucket.primary, aws_s3_bucket.backup]
role = aws_iam_role.replication.arn
bucket = aws_s3_bucket.primary.id
rule {
id = "backup-rule"
status = "Enabled"
destination {
bucket = aws_s3_bucket.backup.arn
storage_class = "STANDARD"
}
}
}
The lifecycle Meta-Argument
Customizing Resource Lifecycle Behavior
The lifecycle
meta-argument gives you control over how Terraform manages resources throughout their lifecycle, including creation, update, and deletion behaviors.
Lifecycle Sub-Arguments
The lifecycle
block supports several sub-arguments:
create_before_destroy
: Create replacement resources before destroying the originalprevent_destroy
: Prevents Terraform from destroying the resourceignore_changes
: Ignore changes to specific attributesreplace_triggered_by
: Forces replacement when specified resources change
Syntax and Usage
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [
tags,
user_data
]
}
}
create_before_destroy Example
The create_before_destroy
setting is useful when you need to ensure zero downtime during updates:
resource "aws_instance" "web" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
user_data = file("init_script.sh")
lifecycle {
create_before_destroy = true
}
}
With this configuration, when you change an attribute that requires replacement (like the AMI), Terraform will:
- Create the new instance first
- Wait for it to be fully provisioned
- Only then destroy the old instance
prevent_destroy Example
The prevent_destroy
setting is ideal for protecting critical resources like databases:
resource "aws_db_instance" "production" {
engine = "postgres"
instance_class = "db.t3.medium"
allocated_storage = 100
name = "production"
lifecycle {
prevent_destroy = true
}
}
If you try to destroy this resource or change an attribute that would force replacement, Terraform will show an error and stop the operation.
ignore_changes Example
The ignore_changes
setting is useful for attributes that might be modified outside of Terraform:
resource "aws_autoscaling_group" "example" {
name = "my-asg"
max_size = 10
min_size = 2
desired_capacity = 2
lifecycle {
# Auto Scaling might adjust the desired_capacity, and we want to ignore those changes
ignore_changes = [desired_capacity]
}
}
This tells Terraform to ignore changes to the desired_capacity
attribute, which might be modified by the auto scaling service itself.
replace_triggered_by Example
The replace_triggered_by
setting (available in Terraform 1.2+) forces replacement of a resource when specified dependencies change:
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
lifecycle {
replace_triggered_by = [
# Replace this instance when the configuration script changes
aws_s3_object.config_script.etag
]
}
}
Combining Meta-Arguments
Meta-arguments can be combined to create powerful and flexible configurations. Here's an example that uses multiple meta-arguments together:
locals {
regions = {
"us-west-2" = "ami-0c55b159cbfafe1f0"
"us-east-1" = "ami-0b5eea76982371e91"
}
}
# Provider configurations
provider "aws" {
region = "us-west-2"
}
provider "aws" {
alias = "east"
region = "us-east-1"
}
# Create a load balancer first
resource "aws_lb" "example" {
name = "multi-region-lb"
internal = false
load_balancer_type = "application"
}
# Create instances in multiple regions
resource "aws_instance" "app_servers" {
for_each = local.regions
# Use the appropriate provider based on the region
provider = each.key == "us-east-1" ? aws.east : aws
ami = each.value
instance_type = "t2.micro"
# Depend on the load balancer
depends_on = [aws_lb.example]
lifecycle {
create_before_destroy = true
ignore_changes = [tags]
}
tags = {
Name = "AppServer-${each.key}"
Region = each.key
}
}
In this example, we've combined:
- Multiple provider configurations
for_each
to create instances across regionsdepends_on
to ensure load balancer is created firstlifecycle
settings to manage instance replacement
Visualizing Meta-Arguments
Let's visualize how these meta-arguments affect resource creation with a diagram:
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)