Skip to main content

Implementing Common Terraform Patterns

1. Using Conditional Resource Creation

Property Description Values Notes
count Number of instances to create 0 or 1 Set to 0 to disable resource creation
for_each Map or set of strings to iterate over map(string) or set(string) Creates one resource per key
condition Boolean expression to control resource creation true or false Used with count to conditionally create resources

Example: Conditional Resource Creation with count

resource "aws_instance" "web" {
  count = var.create_web_server ? 1 : 0

  ami           = "ami-12345678"
  instance_type = "t2.micro"
}

Example: Conditional Resource Creation with for_each

resource "aws_s3_bucket" "bucket" {
  for_each = var.create_buckets ? var.bucket_names : {}

  bucket = each.key
}
Note: When using count = 0, the resource is not created and any references to it will result in an error. Use count = 0 when you want to conditionally disable a resource.
Warning: Changing count from 1 to 0 or vice versa will cause Terraform to destroy and recreate the resource, which may lead to data loss. Ensure that any important data is backed up before making such changes.

2. Implementing Resource Factories

Example: Basic Resource Factory

resource "aws_instance" "web" {
  count = var.instance_count

  ami           = var.ami_id
  instance_type = var.instance_type

  tags = {
    Name = "web-${count.index}"
  }
}
Property Description Default
count Number of instances to create 1
ami Amazon Machine Image ID to use None
instance_type EC2 instance type t2.micro
Note: Use count.index to reference the index of each instance created.

Example: Using Variables with Resource Factories

variable "instance_count" {
  description = "Number of EC2 instances to create"
  type        = number
  default     = 2
}

variable "ami_id" {
  description = "AMI ID for EC2 instances"
  type        = string
  default     = "ami-12345678"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

Best Practices for Resource Factories

  • Use count to create multiple instances of the same resource
  • Use count.index to assign unique identifiers or tags
  • Define variables for scalable configuration
  • Avoid hardcoding values in resource blocks

3. Creating Wrapper Modules

Wrapper modules in Terraform are used to encapsulate and simplify the usage of more complex modules or resources. They provide a clean interface for consumers while hiding internal complexity.

Property Description Example
source Path to the module being wrapped ./child-module
input variables Variables passed to the wrapped module instance_type = "t3.micro"
output variables Variables exposed by the wrapper module public_ip = aws_instance.web.public_ip

Example: Basic Wrapper Module

module "wrapper" {
  source = "./child-module"
  instance_type = "t3.micro"
  ami_id = "ami-12345678"
}

output "public_ip" {
  value = module.wrapper.public_ip
}
Note: Wrapper modules should not contain any logic or resources themselves, but rather act as a pass-through to other modules or resources.

4. Using Dynamic Configuration

Dynamic blocks in Terraform allow you to conditionally generate nested blocks within resources or modules. They are particularly useful when the number of blocks or their content depends on runtime conditions or data.

Property Description Example
dynamic Used to define a dynamic block inside a resource or module. dynamic "block_name" { ... }

Example: Conditional Security Group Rules

resource "aws_security_group" "example" {
  name = "example-sg"

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}
Note: The for_each loop inside a dynamic block must iterate over a map or list. The content block defines the structure of each generated block.

Example: Dynamic Variables in Modules

module "aws_instance" {
  source = "./aws_instance_module"

  dynamic "tags" {
    for_each = var.tags_map
    content {
      key   = tags.key
      value = tags.value
    }
  }
}

Key Points

  • Dynamic blocks allow conditional generation of nested blocks.
  • Use for_each to iterate over a map or list.
  • The content block defines the structure of each generated block.
  • Dynamic blocks are evaluated at runtime, allowing flexibility in configuration.

5. Implementing Feature Flags

Flag Name Resource Type Description Notes
feature_flag_enabled local Boolean flag to enable or disable a feature Used in conditional logic within Terraform configurations
feature_flag_value local String or numeric value associated with the feature flag Can be used to pass values to modules or resources

Example: Using Feature Flags in Module

locals {
  feature_flag_enabled = true
  feature_flag_value   = "new_feature"
}

module "feature_module" {
  source = "./feature_module"
  enabled = local.feature_flag_enabled
  value   = local.feature_flag_value
}

Example: Conditional Resource Creation

resource "aws_s3_bucket" "feature_bucket" {
  count = local.feature_flag_enabled ? 1 : 0
  bucket = "feature-bucket"
}
Note: Feature flags in Terraform are typically implemented using local variables or input variables to control conditional logic. They do not directly interact with external feature flag services but can be used to simulate such behavior.

6. Using Null Labels Pattern

The Null Labels pattern is a Terraform best practice used to create resources that are conditionally managed or "null" based on configuration flags. This pattern is particularly useful when you want to enable or disable certain resources without removing them from the configuration, allowing for easier maintenance and conditional deployment.

Resource Description Usage
null_resource Resource that does nothing but can be used for triggers or lifecycle management. Used to conditionally manage resources based on input variables.

Example: Conditional Resource Creation with null_resource

resource "null_resource" "conditional_log" {
  count = var.enable_logging ? 1 : 0

  triggers = {
    timestamp = timestamp()
  }
}

resource "aws_cloudwatch_log_group" "app_logs" {
  count = var.enable_logging ? 1 : 0

  name = "/aws/lambda/${aws_lambda_function.app.name}"
}
Note: The null_resource does not create any actual infrastructure but can be used to manage dependencies or triggers in conditional logic.

7. Implementing Environment Patterns

Pattern Description Implementation Notes
Shared Infrastructure Common infrastructure shared across environments
module "vpc" {
  source = "./modules/vpc"
  environment = var.environment
}
Use module parameters to customize per environment
Environment-Specific Variables Variables that change per environment (e.g., region, instance count)
variable "environment" {
  description = "Environment name (dev, staging, prod)"
  type        = string
}

variable "instance_count" {
  description = "Number of EC2 instances"
  type        = number
  default     = 1
}
Use terraform.tfvars or CLI vars to override
Backend Configuration Isolated state storage per environment
terraform {
  backend "s3" {
    bucket = "my-terraform-state-dev"
    key    = "dev/terraform.tfstate"
    region = "us-west-2"
  }
}
Use different buckets or keys to isolate environments

Example: Environment-Specific Module Usage

module "web_app" {
  source = "./modules/web-app"
  environment = var.environment
  instance_count = var.instance_count
  instance_type = "t3.micro"
}

Best Practices for Environment Patterns

  • Use terraform.tfvars files for each environment (e.g., dev.tfvars, prod.tfvars)
  • Store state in isolated backends to prevent cross-environment drift
  • Use for_each or count to manage environment-specific resources
  • Implement validation rules for environment variables

8. Using Remote State Pattern

Property Description Example
backend Defines where Terraform state is stored backend "s3" { ... }
bucket Name of the S3 bucket to store state bucket = "my-terraform-state"
key Path to the state file in the bucket key = "prod/terraform.tfstate"
region AWS region for the S3 bucket region = "us-west-2"
encrypt Enable server-side encryption of state file encrypt = true

Example: S3 Backend Configuration

terraform {
  backend "s3" {
    bucket = "my-terraform-state"
    key    = "prod/terraform.tfstate"
    region = "us-west-2"
    encrypt = true
  }
}
Note: Remote state enables collaboration and prevents local state drift. Always use versioning and encryption for sensitive state files.
Warning: Never commit backend configuration with secrets to version control. Use variables or environment-specific files.

Benefits of Remote State

  • Enables team collaboration
  • Prevents local state conflicts
  • Supports state locking via DynamoDB
  • Allows secure storage of sensitive data

9. Implementing Module Composition

Example: Basic Module Composition

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "3.10.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.10.0/24", "10.0.11.0/24", "10.0.12.0/24"]
}

module "ec2_instances" {
  source = "./modules/ec2-instance"
  count  = 3

  instance_type = "t3.micro"
  vpc_id        = module.vpc.vpc_id
  subnet_id     = module.vpc.private_subnets[0]
}
Property Description Default Required
source Path to the module source (local, remote, or registry) N/A Yes
version Version constraint for registry modules "" No
count Number of instances to create 1 No
for_each Map or set of values to iterate over "" No
depends_on List of resources or modules this module depends on [] No
Note: Module composition allows for reusability and abstraction of infrastructure components.

Best Practices for Module Composition

  • Use versioned modules from public registries when possible
  • Keep modules focused on a single responsibility
  • Use count or for_each for scalable resource creation
  • Pass outputs from one module as inputs to another

10. Using Provider Configuration Patterns

Pattern Description Example
Named Provider Configurations Define multiple provider instances with distinct names for different regions or environments.
provider "aws" {
  alias  = "us_west"
  region = "us-west-2"
}

provider "aws" {
  alias  = "eu_west"
  region = "eu-west-1"
}
Provider Aliasing with Modules Use provider aliases inside modules to isolate configurations and avoid conflicts.
module "vpc" {
  source = "./modules/vpc"
  providers = {
    aws = aws.us_west
  }
}
Shared Configuration via Variables Centralize provider settings using variables to reduce duplication across environments.
variable "aws_region" {
  default = "us-east-1"
}

provider "aws" {
  region = var.aws_region
}

Example: Using Provider Aliases for Multi-Region Deployment

provider "aws" {
  alias  = "primary"
  region = "us-west-2"
}

provider "aws" {
  alias  = "secondary"
  region = "eu-central-1"
}

resource "aws_instance" "web" {
  provider = aws.primary
  ami           = "ami-12345678"
  instance_type = "t2.micro"
}

resource "aws_instance" "backup" {
  provider = aws.secondary
  ami           = "ami-87654321"
  instance_type = "t2.micro"
}

Was this article helpful?