Terraform Dynamic Blocks
Introduction
When working with Terraform, you'll often encounter situations where you need to create multiple similar nested blocks within a resource. This could be multiple security group rules, multiple IAM policies, or various other configurations that follow a similar pattern. Creating each of these blocks manually can lead to repetitive code, which is harder to maintain and more prone to errors.
This is where dynamic blocks come in. Dynamic blocks allow you to dynamically construct repeatable nested blocks within your Terraform resource configurations. They function as a loop construct, enabling you to iterate over a collection (like a list or a map) and generate multiple blocks based on that collection.
In this guide, we'll explore how dynamic blocks work, when to use them, and how they can dramatically improve your Terraform configuration's readability and maintainability.
Basic Syntax
The basic syntax of a dynamic block is as follows:
resource "aws_security_group" "example" {
name = "example"
dynamic "ingress" {
for_each = var.service_ports
content {
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
}
Let's break down this example:
- We're creating an AWS security group resource.
- Instead of defining multiple
ingress
blocks manually, we're using a dynamic block. - The
for_each
argument iterates overvar.service_ports
, which would be a list or map of port numbers. - For each value in
var.service_ports
, Terraform creates aningress
block using the configuration specified in thecontent
block.
When to Use Dynamic Blocks
Dynamic blocks are particularly useful in these scenarios:
- Repeating nested blocks: When you have multiple similar nested blocks within a resource.
- Configurable infrastructure: When you want to make your infrastructure more configurable by parameterizing the number of blocks.
- DRY (Don't Repeat Yourself) principle: To avoid repetitive code that's hard to maintain.
Comprehensive Example: AWS Security Group
Let's look at a more comprehensive example using an AWS security group:
variable "ingress_rules" {
description = "List of ingress rules for the security group"
type = list(object({
port = number
protocol = string
cidr_blocks = list(string)
description = string
}))
default = [
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
},
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
]
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
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
description = ingress.value.description
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
description = "Allow all outbound traffic"
}
tags = {
Name = "web-sg"
}
}
In this example:
- We define a variable
ingress_rules
that contains a list of objects, each representing an ingress rule. - We use a dynamic block to iterate over
var.ingress_rules
and create aningress
block for each element. - Each
ingress
block uses the values from the corresponding element invar.ingress_rules
.
Using Dynamic Blocks with Maps
Dynamic blocks also work well with maps. Here's an example:
variable "ingress_ports" {
description = "Map of ingress ports to their descriptions"
type = map(string)
default = {
"80" = "HTTP"
"443" = "HTTPS"
"22" = "SSH"
}
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "Security group for web servers"
dynamic "ingress" {
for_each = var.ingress_ports
content {
from_port = ingress.key
to_port = ingress.key
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = ingress.value
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
In this example:
- We define a map variable
ingress_ports
where the keys are port numbers and the values are descriptions. - In the dynamic block, we use
ingress.key
to access the key (port number) andingress.value
to access the value (description).
Conditional Creation of Resources
You can also use dynamic blocks with conditional logic to create resources only when certain conditions are met:
variable "create_nat_gateway" {
description = "Whether to create NAT Gateway"
type = bool
default = true
}
variable "public_subnets" {
description = "List of public subnets"
type = list(string)
default = ["subnet-12345678", "subnet-87654321"]
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
dynamic "nat_gateway" {
for_each = var.create_nat_gateway ? var.public_subnets : []
content {
subnet_id = nat_gateway.value
}
}
}
In this example:
- We use a conditional expression
var.create_nat_gateway ? var.public_subnets : []
to determine whether to create NAT gateways. - If
create_nat_gateway
is true, we create a NAT gateway for each public subnet. - If
create_nat_gateway
is false, we create no NAT gateways.
Nested Dynamic Blocks
You can also nest dynamic blocks within each other for more complex configurations:
variable "security_groups" {
description = "Security groups with their rules"
type = map(object({
name = string
description = string
ingress = list(object({
port = number
protocol = string
cidr_blocks = list(string)
}))
}))
default = {
"web" = {
name = "web-sg"
description = "Web security group"
ingress = [
{
port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
},
"db" = {
name = "db-sg"
description = "Database security group"
ingress = [
{
port = 3306
protocol = "tcp"
cidr_blocks = ["10.0.0.0/16"]
}
]
}
}
}
resource "aws_security_group" "this" {
for_each = var.security_groups
name = each.value.name
description = each.value.description
dynamic "ingress" {
for_each = each.value.ingress
content {
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = ingress.value.cidr_blocks
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
In this example:
- We define a complex variable
security_groups
that contains a map of security groups, each with its own set of ingress rules. - We use
for_each
at the resource level to create multiple security groups. - Within each security group, we use a dynamic block to create the ingress rules.
Best Practices
While dynamic blocks are powerful, here are some best practices to keep in mind:
-
Use them judiciously: Dynamic blocks can make your code harder to read if overused. Sometimes, explicit static blocks might be more readable.
-
Consider module abstractions: For very complex configurations, consider creating modules instead of using dynamic blocks.
-
Document your variables: When using dynamic blocks with complex variables, ensure your variables are well-documented.
-
Test thoroughly: Dynamic blocks can generate a large number of resources. Test your configurations thoroughly to avoid unexpected behavior.
Visualizing Dynamic Blocks
Here's a simple diagram to visualize how dynamic blocks work:
Summary
Dynamic blocks in Terraform provide a powerful way to create multiple similar nested blocks within resources. They allow you to:
- Iterate over lists or maps to create multiple blocks.
- Make your configurations more dynamic and configurable.
- Reduce repetition in your code.
When used correctly, dynamic blocks can significantly improve the readability, maintainability, and flexibility of your Terraform configurations.
Additional Resources
Exercises
- Create a dynamic block to generate multiple EBS volumes with different sizes.
- Use a dynamic block to create multiple Route53 records from a list of domain names.
- Create a security group with dynamic ingress and egress rules based on a map of service ports.
- Use dynamic blocks to create multiple IAM policy documents from a list of policy statements.
By mastering dynamic blocks, you'll be able to write more concise, maintainable, and flexible Terraform configurations.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)