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
}
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.
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 |
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
countto create multiple instances of the same resource - Use
count.indexto 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
}
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
}
}
}
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_eachto iterate over a map or list. - The
contentblock 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"
}
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}"
}
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 | |
Use module parameters to customize per environment |
| Environment-Specific Variables | Variables that change per environment (e.g., region, instance count) | |
Use terraform.tfvars or CLI vars to override |
| Backend Configuration | Isolated state storage per environment | |
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.tfvarsfiles for each environment (e.g.,dev.tfvars,prod.tfvars) - Store state in isolated backends to prevent cross-environment drift
- Use
for_eachorcountto 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
}
}
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 |
Best Practices for Module Composition
- Use versioned modules from public registries when possible
- Keep modules focused on a single responsibility
- Use
countorfor_eachfor 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 Aliasing with Modules | Use provider aliases inside modules to isolate configurations and avoid conflicts. | |
| Shared Configuration via Variables | Centralize provider settings using variables to reduce duplication across environments. | |
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"
}