Infrastructure as Code (IaC) often involves managing resources with configurations that are not fixed at compile time. Whether you are provisioning AWS security groups, configuring Kubernetes deployments, or setting up Azure network interfaces, the number of nested blocks or list items frequently varies. Hardcoding these configurations leads to repetitive code and maintenance nightmares. This is where Terraform dynamic blocks and the for_each meta-argument shine.
For intermediate to advanced developers, mastering these two features is essential for writing clean, scalable, and maintainable HCL (HashiCorp Configuration Language) code. This post explores advanced patterns for combining these tools to handle complex, variable-length collections.
Understanding the Building Blocks
Before diving into advanced patterns, let’s briefly clarify the distinction. for_each is a meta-argument that can be used on resources or modules to create multiple instances of a resource based on a map or set of strings. It is ideal for top-level resource creation.
On the other hand, dynamic blocks allow you to generate nested blocks (like ingress in a security group) inside a resource dynamically. They iterate over a list or map and inject the corresponding block configuration for each element.
Pattern 1: The Nested Dynamic Block
The most common use case for dynamic blocks is generating nested configurations. Consider an AWS aws_security_group. You want to define ingress rules based on a variable, but the number of rules is unknown. Using a dynamic block, you can iterate over a list of maps.
variable "ingress_rules" {
type = list(object({
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = []
}
resource "aws_security_group" "example" {
name = "example-sg"
dynamic "ingress" {
for_each = var.ingress_rules
content {
from_port = ingress.value.from_port
to_port = ingress.value.to_port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
}
# Usage in main.tf
resource "aws_security_group" "web_sg" {
ingress_rules = [
{
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["10.0.0.0/8"]
}
]
}
This pattern keeps your variable definitions clean and allows your infrastructure to adapt instantly when new rules are added to the input list.
Pattern 2: Combining for_each and Dynamic Blocks
Advanced scenarios often require creating multiple resources, each with its own set of dynamic nested blocks. For example, you might be deploying multiple application services, each with specific environment variables or container ports.
By combining for_each on the resource and dynamic blocks inside the resource, you achieve a high degree of flexibility.
variable "services" {
type = map(object({
name = string
ports = list(object({
containerPort = number
protocol = string
}))
}))
}
resource "aws_lb_target_group" "this" {
for_each = var.services
name = each.key
port = 80
protocol = "HTTP"
# Note: Dynamic blocks inside target groups are limited,
# but this pattern applies to resources like aws_instance or aws_lb_listener
tags = {
Name = each.key
}
}
While the above example is simplified, a more complex implementation involves using for_each to spin up distinct modules or resources, where each instance receives a specific configuration map that drives its internal dynamic blocks.
Best Practices and Performance Considerations
When managing variable-length collections, keep the following in mind:
- Immutability of Keys: When using
for_each, ensure your keys are stable. Changing a key in a map will cause Terraform to destroy and recreate the resource rather than updating it. - Simplify Variables: Use structured variable types (maps of objects or lists of objects) to validate inputs early. This prevents runtime errors when dynamic blocks try to access undefined attributes.
- Readability: While powerful, excessive nesting of dynamic blocks can make code hard to read. Extract complex configurations into local values or separate modules if the logic becomes too convoluted.
Conclusion
Dynamic blocks and for_each are not just convenience features; they are fundamental tools for building robust, cloud-native infrastructure with Terraform. By leveraging these advanced patterns, you can eliminate code duplication, reduce the risk of configuration drift, and create infrastructure that scales as gracefully as your applications do. Start refactoring your hard-coded blocks today to unlock the full potential of your IaC strategy.