Terraform meta-arguments: the key to powerful infrastructure as code
Discover how Terraform meta-arguments—count, for_each, lifecycle & more—improve IaC reuse and control for faster, scalable deployments.
Terraform meta-arguments provide powerful controls over resource behavior beyond their type-specific configurations. They transform static infrastructure definitions into dynamic, flexible, and resilient deployments that adapt to changing requirements. These special arguments fundamentally change how Terraform processes resources, enabling more sophisticated infrastructure patterns with less code.
Comprehensive guide to depends_on
The depends_on
meta-argument explicitly declares dependencies between resources that Terraform cannot automatically infer through reference expressions. While Terraform typically handles dependencies implicitly, depends_on
manages "hidden" dependencies not directly visible in your configuration.
Purpose and functionality
depends_on
ensures resources are created, updated, and destroyed in the correct order. When specified, Terraform completes all actions on the dependency object before performing any actions on the object declaring the dependency. This is essential for resources that depend on each other's behavior rather than just their data.
Syntax and usage
The depends_on
meta-argument accepts a list of references to other resources or child modules:
resource "aws_instance" "example" {
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
depends_on = [
aws_security_group.example,
aws_subnet.example
]
}
Starting with Terraform 0.13, depends_on
can also be used with module blocks:
module "web_servers" {
source = "./modules/web_servers"
# module configuration...
depends_on = [module.vpc]
}
Best practices for depends_on
- Use as a last resort - Rely on implicit dependencies through reference expressions whenever possible
- Only for hidden dependencies - Use only when a resource relies on another resource's behavior but doesn't directly reference its data
- Always include comments - Document why the explicit dependency is necessary
Common use cases include resource configuration ordering, bootstrapping dependencies, infrastructure timing issues, and working around API limitations.
Interactions with other meta-arguments
With count
or for_each
, the dependency applies to all instances created. When used alongside lifecycle
blocks, dependencies declared with depends_on
are still respected, and Terraform may propagate the create_before_destroy
behavior to dependency resources.
Limitations and gotchas
Key limitations include:
- Cannot use expressions in
depends_on
values - Creates more conservative plans that might replace more resources than necessary
- May treat more values as "unknown" during planning
- Increases deployment time due to strict sequencing
- Using with data sources forces them to be processed at apply time rather than plan time
Common pitfalls include overuse, improper use with data sources, and creating cyclical dependencies.
Maximizing count for resource multiplication
The count
meta-argument creates multiple instances of a resource or module from a single configuration block, reducing duplication and enabling dynamic resource creation.
Purpose and functionality
count
efficiently manages similar infrastructure resources without duplicating code, improving maintainability and enabling conditional resource creation. Each instance is managed as a distinct infrastructure object with its own lifecycle.
Syntax and examples
count
accepts whole numbers, numeric expressions, or conditional expressions:
resource "aws_instance" "server" {
count = 4 # Creates four EC2 instances
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
tags = {
Name = "Server ${count.index}" # Using count.index for differentiation
}
}
The special count.index
value contains the distinct index number (starting with 0) for each instance, enabling customization:
resource "aws_subnet" "private" {
count = 3
vpc_id = aws_vpc.main.id
cidr_block = "10.0.${count.index + 1}.0/24"
availability_zone = var.availability_zones[count.index]
}
Conditional resource creation is a common pattern:
resource "aws_s3_bucket" "logging" {
count = var.enable_logging ? 1 : 0
bucket = "my-app-logs"
acl = "private"
}
Best practices and use cases
- Keep
count
at the top of resource blocks for readability - Use variables for
count
values instead of hardcoding them - Be cautious with resource references using proper indexing
- Consider using
for_each
when resources need distinct values not easily derived from an index
Common use cases include creating multiple similar resources, conditional resource creation, and resource replication across regions.
Interactions with other meta-arguments
depends_on
can be used alongside count
to establish dependencies, while count
and for_each
cannot be used in the same resource block. Both provider
and lifecycle
meta-arguments work with resources using count
.
Limitations and gotchas
The "index shifting problem" is the most significant limitation of count
. If you remove an element from the middle of a list that count
iterates over, all elements after it will shift down, causing Terraform to destroy and recreate resources unnecessarily.
Other limitations include:
count
must be known before apply- Splat expressions can become complex
- Module support was only added in Terraform 0.13
Unlocking flexibility with for_each
The for_each
meta-argument enables creation of multiple resource instances based on maps or sets of data, offering greater flexibility than count
for managing collections of resources with unique identifiers.
Purpose and functionality
for_each
allows you to:
- Create multiple similar resources from a single resource block
- Dynamically generate resources based on maps or sets
- Handle variations between resources more gracefully than
count
- Create more maintainable infrastructure code with meaningful keys
Syntax and examples
for_each
accepts either a map (where both keys and values are used) or a set of strings:
# Using with a map
resource "azurerm_resource_group" "rg" {
for_each = {
a_group = "eastus"
another_group = "westus2"
}
name = each.key
location = each.value
}
# Using with a set of strings
resource "aws_iam_user" "users" {
for_each = toset(["todd", "james", "alice", "dottie"])
name = each.key # For sets, each.key and each.value are identical
}
Since for_each
doesn't directly support lists, you need to convert them to sets:
locals {
subnet_ids = toset([
"subnet-abcdef",
"subnet-012345",
])
}
resource "aws_instance" "server" {
for_each = local.subnet_ids
ami = "ami-a1b2c3d4"
instance_type = "t2.micro"
subnet_id = each.key
}
When to use for_each vs. count
- Use
for_each
when:- Working with non-sequential resources or resources with natural identifiers
- Working with maps or sets of resources with varying attributes
- Creating resources that may be added or removed from the middle of a collection
- Need to reference specific resources by a meaningful key rather than an index
- Use
count
when:- Creating a simple, fixed number of identical resources
- Order matters and you need sequential numbering
- The number of resources is determined by a simple calculation
Interactions with other meta-arguments
for_each
cannot be used with count
in the same resource block, but works well with other meta-arguments like depends_on
, lifecycle
, and provider
. All instances created by for_each
will depend on resources specified in depends_on
.
Limitations and gotchas
- Values must be known before Terraform performs any resource actions
- Cannot use sensitive values in
for_each
arguments - Cannot use results of impure functions like
uuid
,bcrypt
, ortimestamp
- Lists must be explicitly converted to sets using
toset()
- Changing the structure of a
for_each
map or set can cause resources to be recreated
Controlling provider selection with the provider meta-argument
The provider
meta-argument specifies which provider configuration to use for a resource, overriding Terraform's default behavior of selecting providers based on resource type prefixes.
Purpose and functionality
By default, Terraform automatically associates resources with providers based on the prefix of the resource type (e.g., aws_instance
uses the aws
provider). The provider
meta-argument allows you to explicitly specify which provider configuration should be used, which is valuable for:
- Managing resources across multiple regions or environments
- Having multiple configurations of the same provider
- Using providers with non-standard naming conventions
- Fine-grained control over which provider configuration applies to which resources
Syntax and examples
resource "TYPE" "NAME" {
provider = PROVIDER.ALIAS
# ... other resource configuration ...
}
Multiple AWS Regions Example:
# Default AWS provider for us-east-1
provider "aws" {
region = "us-east-1"
}
# Additional provider for us-west-2 region
provider "aws" {
alias = "west"
region = "us-west-2"
}
# EC2 instance in us-west-2
resource "aws_instance" "west_instance" {
provider = aws.west
ami = "ami-0987654321fedcba0"
instance_type = "t2.micro"
}
Best practices and common use cases
- Explicit Provider References: Always use explicit provider references in large infrastructures
- Consistent Naming Conventions: Use clear naming for provider aliases
- Provider Organization: Define all provider configurations in a central location
- Keep Credentials Secure: Use environment variables or other secure methods for authentication
Common use cases include multi-region deployments, multi-account management, cross-provider resource management, and specialized provider configurations.
Interactions with other meta-arguments
The provider
meta-argument works alongside all other meta-arguments. When used with count
or for_each
, all instances use the same provider configuration. With depends_on
and lifecycle
, provider specifics don't affect how these meta-arguments function.
Limitations and gotchas
- Expressions Not Allowed: The
provider
meta-argument must be a direct reference - Provider Configurations in Root Module Only: Child modules receive provider configurations from the root module
- Module Compatibility: A module that defines its own provider configurations is not compatible with
count
,for_each
, anddepends_on
- Provider Removal Constraints: All resources using a provider configuration must be destroyed before removing that configuration
Mastering resource lifecycle management
The lifecycle
meta-argument provides fine-grained control over how Terraform manages resources throughout their creation, update, and destruction phases.
Purpose and sub-arguments
The lifecycle
block can contain the following sub-arguments:
create_before_destroy
: Creates replacement resources before destroying existing onesprevent_destroy
: Protects critical resources from accidental deletionignore_changes
: Controls which attributes Terraform ignores during updatesreplace_triggered_by
: Defines dependencies between resources for replacementprecondition
andpostcondition
: Establish validation for resource operations
Syntax and usage
resource "aws_instance" "example" {
# Resource configuration...
lifecycle {
create_before_destroy = true
prevent_destroy = false
ignore_changes = [tags]
replace_triggered_by = [aws_security_group.example.id]
precondition {
condition = var.environment == "production" ? var.instance_type == "t3.large" : true
error_message = "Production environment must use t3.large or larger instance types."
}
}
}
create_before_destroy
By default, when Terraform must replace a resource, it destroys the existing resource first and then creates a new one. With create_before_destroy = true
, Terraform reverses this order, creating the new resource first before destroying the old one to minimize downtime.
Best practices:
- Use for resources that provide critical services where downtime must be minimized
- Be aware that some resources may have naming or uniqueness requirements
- Note that destroy provisioners won't run if this is set to true
prevent_destroy
When set to true
, this causes Terraform to reject any plan that would destroy the resource:
resource "aws_db_instance" "database" {
# Database configuration...
lifecycle {
prevent_destroy = true
}
}
Best practices:
- Use sparingly for truly critical resources
- Be aware that this makes certain configuration changes impossible
- Document resources with
prevent_destroy
clearly
ignore_changes
This specifies resource attributes that Terraform should ignore when planning updates:
resource "aws_instance" "web" {
# Instance configuration...
tags = {
Name = "web-server"
}
lifecycle {
ignore_changes = [
tags,
user_data,
]
}
}
You can also use ignore_changes = all
to ignore all attributes.
Best practices:
- Use for attributes intentionally managed outside of Terraform
- Document why specific attributes are being ignored
- Be cautious with
ignore_changes = all
as it can lead to configuration drift
replace_triggered_by
Added in Terraform 1.2, this allows you to specify that a resource should be replaced when another resource changes:
resource "aws_instance" "example" {
# Configuration...
lifecycle {
replace_triggered_by = [
aws_security_group.example.id
]
}
}
Limitations:
- Only accepts resource addresses, not arbitrary expressions
- Cannot reference variables or data sources directly
- May cause cascading replacements in complex dependencies
Interactions with other meta-arguments
The lifecycle
block works with all other meta-arguments:
- With
count
orfor_each
, lifecycle settings apply to each instance - With
depends_on
, lifecycle settings (especiallycreate_before_destroy
) can propagate to dependencies - With
provider
, lifecycle settings are applied regardless of which provider configuration is used
How meta-arguments interact with each other
Understanding how Terraform meta-arguments interact is crucial for creating effective infrastructure code.
depends_on with count and for_each
When using depends_on
with resources that use count
or for_each
, the dependency applies to all instances created. Each instance will wait for the dependency to be satisfied:
resource "aws_security_group" "example" {
# Configuration...
}
resource "aws_instance" "example" {
count = 3
# Configuration...
# All three instances will wait for the security group
depends_on = [aws_security_group.example]
}
lifecycle with count and for_each
Lifecycle settings apply to all instances created through count
or for_each
:
resource "aws_instance" "example" {
for_each = toset(["web", "app", "db"])
# Configuration...
lifecycle {
create_before_destroy = true
ignore_changes = [tags]
}
}
provider with count and for_each
When using provider
with resources that use count
or for_each
, all instances use the same provider configuration:
provider "aws" {
alias = "west"
region = "us-west-2"
}
resource "aws_instance" "server" {
provider = aws.west
for_each = toset(["web", "app", "db"])
# Configuration...
}
Mutual exclusivity: count and for_each
count
and for_each
cannot be used in the same resource or module block:
resource "aws_instance" "server" {
count = 3 # Cannot use both count and for_each
for_each = var.instances # This would cause an error
# Configuration...
}
Inheritance and propagation
Some behaviors propagate through dependencies:
- If resource A has
create_before_destroy = true
and depends on resource B, Terraform enables the same behavior for resource B - This ensures proper ordering during complex replacements
Best practices for Terraform meta-arguments
General best practices
- Choose the right meta-argument for the job
- Use
count
for simple repetition - Use
for_each
for complex collections with meaningful keys - Use
depends_on
only when implicit dependencies aren't sufficient - Use
provider
for multi-region or multi-account deployments
- Use
- Document meta-argument usage
- Always comment why you're using explicit dependencies
- Document lifecycle customizations, especially
prevent_destroy
- Keep configurations DRY
- Use variables for values used in meta-arguments
- Extract complex transformations to local values
- Structure for readability
- Place meta-arguments at the top of resource blocks
- Group related resources that share meta-argument patterns
- Be cautious with sensitive data
- Never use sensitive values in
for_each
keys - Be aware of how meta-arguments affect state file content
- Never use sensitive values in
Meta-argument specific recommendations
For depends_on
:
- Use as a last resort when implicit dependencies aren't sufficient
- Document the reason for each explicit dependency
- Consider if the dependency is better expressed through data references
For count
:
- Be aware of the "index shifting problem" when resources might be removed
- Consider
for_each
for resources that might be added or removed from the middle of a collection - Use conditionals (
count = var.enabled ? 1 : 0
) for optional resources
For for_each
:
- Use meaningful keys that won't change frequently
- Extract complex transformations to locals for readability
- Remember that values must be known at plan time
For provider
:
- Use consistent naming conventions for provider aliases
- Centralize provider configurations in root modules
- Document the purpose of each provider configuration
For lifecycle
:
- Use
create_before_destroy
for zero-downtime deployments - Reserve
prevent_destroy
for truly critical resources - Use
ignore_changes
judiciously to prevent unintended drift - Document why specific attributes are being ignored
Recent updates in Terraform versions
Terraform 0.12 - 0.13
- depends_on: Terraform 0.13 added support for using
depends_on
with module blocks - count: Terraform 0.13 added support for using
count
with modules - for_each: Introduced in Terraform 0.12.6 for resources, with module support added in 0.13
- provider: Terraform 0.13 introduced the
required_providers
block and configuration_aliases
Terraform 1.0 and beyond
- lifecycle: Terraform 1.2 added the
replace_triggered_by
lifecycle meta-argument - lifecycle: Terraform 1.3 added
precondition
andpostcondition
blocks - All meta-arguments have seen improvements in error messaging, validation, and behavior consistency
Conclusion
Terraform meta-arguments transform static infrastructure definitions into dynamic, flexible, and resilient deployments. By understanding each meta-argument's purpose, syntax, limitations, and interactions, you can write more maintainable infrastructure code that handles complex requirements with ease.
Whether you're creating multiple similar resources, managing dependencies, working across regions or accounts, or controlling resource lifecycle behavior, meta-arguments provide the tools you need to implement sophisticated infrastructure patterns while keeping your code clean and DRY.
As you develop your Terraform skills, mastering these meta-arguments will significantly enhance your ability to create elegant, robust infrastructure as code solutions for even the most complex environments.