Skip to main content

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:

  1. depends_on - Specify explicit dependencies
  2. count - Create multiple resource instances
  3. for_each - Create multiple instances from a map or set
  4. provider - Select a specific provider configuration
  5. lifecycle - 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

hcl
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:

  1. A database server
  2. A backend application server
  3. A load balancer

Even if there's no direct attribute reference, you might want to ensure the database is created before the application server:

hcl
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

hcl
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:

hcl
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:

hcl
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:

  1. It's based on indices, which can cause issues when removing items from the middle of the list
  2. It can only create identical resources with minor variations
  3. 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:

hcl
resource "aws_iam_user" "example" {
for_each = toset(["john", "mary", "peter"])
name = each.value
}

With a map:

hcl
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:

hcl
output "john_arn" {
value = aws_iam_user.example["john"].arn
}

Real-world Example: Multiple EC2 Instances with Different Configurations

hcl
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

  1. Resources are identified by meaningful keys instead of numeric indices
  2. Adding or removing items in the middle doesn't affect other resources
  3. It handles more complex data structures
  4. 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:

hcl
# 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:

hcl
resource "aws_instance" "example" {
provider = aws.east
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t2.micro"
}

Real-world Example: Multi-region Deployment

hcl
# 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:

  1. create_before_destroy: Create replacement resources before destroying the original
  2. prevent_destroy: Prevents Terraform from destroying the resource
  3. ignore_changes: Ignore changes to specific attributes
  4. replace_triggered_by: Forces replacement when specified resources change

Syntax and Usage

hcl
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:

hcl
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:

  1. Create the new instance first
  2. Wait for it to be fully provisioned
  3. Only then destroy the old instance

prevent_destroy Example

The prevent_destroy setting is ideal for protecting critical resources like databases:

hcl
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:

hcl
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:

hcl
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:

hcl
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 regions
  • depends_on to ensure load balancer is created first
  • lifecycle 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! :)