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

  1. Use as a last resort - Rely on implicit dependencies through reference expressions whenever possible
  2. Only for hidden dependencies - Use only when a resource relies on another resource's behavior but doesn't directly reference its data
  3. 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

  1. Keep count at the top of resource blocks for readability
  2. Use variables for count values instead of hardcoding them
  3. Be cautious with resource references using proper indexing
  4. 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, or timestamp
  • 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

  1. Explicit Provider References: Always use explicit provider references in large infrastructures
  2. Consistent Naming Conventions: Use clear naming for provider aliases
  3. Provider Organization: Define all provider configurations in a central location
  4. 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

  1. Expressions Not Allowed: The provider meta-argument must be a direct reference
  2. Provider Configurations in Root Module Only: Child modules receive provider configurations from the root module
  3. Module Compatibility: A module that defines its own provider configurations is not compatible with count, for_each, and depends_on
  4. 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 ones
  • prevent_destroy: Protects critical resources from accidental deletion
  • ignore_changes: Controls which attributes Terraform ignores during updates
  • replace_triggered_by: Defines dependencies between resources for replacement
  • precondition and postcondition: 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 or for_each, lifecycle settings apply to each instance
  • With depends_on, lifecycle settings (especially create_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

  1. 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
  2. Document meta-argument usage
    • Always comment why you're using explicit dependencies
    • Document lifecycle customizations, especially prevent_destroy
  3. Keep configurations DRY
    • Use variables for values used in meta-arguments
    • Extract complex transformations to local values
  4. Structure for readability
    • Place meta-arguments at the top of resource blocks
    • Group related resources that share meta-argument patterns
  5. Be cautious with sensitive data
    • Never use sensitive values in for_each keys
    • Be aware of how meta-arguments affect state file content

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 and postcondition 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.