- What Are Dynamic Blocks?
- Breaking Down the Dynamic Block
-
Example 1: Simplifying AWS Security Group Rules with Terraform Dynamic Blocks
- Without Using Dynamic Blocks
- Overview
- Refactoring with Terraform Dynamic Blocks
- Using Terraform Dynamic Blocks
- Variables for Ingress and Egress Settings
- Implementing Dynamic Blocks in Resource Block
- Explanation of the ingress Dynamic Block
- 1. dynamic ingress Block
- 2. for_each = var.ingress_settings
- 3. The Absence of an Explicit iterator Definition
- 4. content Block
- Explanation of the egress Dynamic Block
- 1. dynamic egress Block
- 2. for_each = var.egress_settings
- 3. iterator = main_sg_egress
- 4. content Block
- Example 2: Using map(object) with Dynamic Blocks for Simplified Ingress and Egress Rules
- Example 3: Dynamic Blocks in Terraform for AMI Filtering
- Example 4: Helm Release Using Dynamic Blocks
- Example 5: Nested Dynamic Blocks (EC2 Instance with Dynamic EBS Volumes and Tags)
- Conclusion
Imagine scrolling through a lengthy Terraform configuration file, filled with repetitive code that feels never-ending. It’s not only tedious but also makes the whole setup hard to understand and maintain, especially when changes are needed. This is where dynamic blocks come to the rescue!
Dynamic blocks in Terraform are designed to simplify and clean up your code by reducing repetition, making it more efficient and easier to manage. They help you follow the "Don't Repeat Yourself" (DRY) principle, turning complex and cluttered configurations into concise, reusable, and elegant blocks.
In this guide, we’ll break down how dynamic blocks work, why they’re game-changers, and how you can start using them to take your Terraform scripts to the next level!
What Are Dynamic Blocks?
In Terraform, dynamic blocks provide a way to dynamically generate repeated nested blocks within resource, data, provider, and provisioner blocks. They're commonly used in resource blocks to make your configurations more flexible and follow the "Don't Repeat Yourself" (DRY) principle.
Here's the basic structure of a dynamic block in Terraform:
resource "resource_type" "resource_name" {
# Resource block configuration
dynamic "label" {
for_each = collection_to_iterate
iterator = item
content {
# Content of the dynamically generated block
}
}
}
Breaking Down the Dynamic Block
Let’s walk through the key elements that make up a dynamic block:
label: This represents the type of block you want to generate dynamically (e.g.,subnet,security_group_rule). It essentially defines what nested block you’re constructing within the parent block.for_each: This argument takes a list or map, representing the collection of values you want to iterate over to generate multiple blocks. It allows you to loop over a set of data and create multiple instances of the nested block.iterator(optional): By default, this takes the name of thelabelblock, but you can customize it to any name you prefer. It acts as a placeholder for each element in your collection as you iterate over it. This helps in accessing individual elements within thecontentblock.content: This is the heart of your dynamic block. It defines the actual content that gets generated for each item in the iteration. Here, you use the iterator to reference properties or values of the current item in the loop.
Why Use Dynamic Blocks?
- Eliminate Repetition: Write once, use multiple times. Dynamic blocks reduce code duplication, making your Terraform scripts more concise.
- Increase Maintainability: Changes are easier to manage when code is less cluttered and more focused.
- Enhance Readability: With dynamic blocks, you can structure your configuration in a way that is both efficient and easier to understand.
Example 1: Simplifying AWS Security Group Rules with Terraform Dynamic Blocks
Without Using Dynamic Blocks
Let's start by looking at a typical Terraform configuration without using dynamic blocks. This configuration sets up an AWS VPC, a public subnet, and an AWS Security Group.
resource "aws_vpc" "main_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.main_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
resource "aws_security_group" "main_sg" {
vpc_id = aws_vpc.main_vpc.id
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
tags = {
Name = "main-sg"
}
}
Overview
-
VPC: Defines a Virtual Private Cloud with a CIDR block
10.0.0.0/16. - Subnet: Creates a public subnet within the VPC.
-
Security Group: The security group (
aws_security_group.main_sg) allows inbound traffic on ports 22 (SSH), 80 (HTTP), and 443 (HTTPS) and all outbound traffic.
However, the ingress and egress rules are repetitive, which makes the configuration lengthy and difficult to maintain, especially as the rules grow. This is a prime candidate for refactoring using Terraform dynamic blocks.
Refactoring with Terraform Dynamic Blocks
Let's make our code more efficient and adhere to the DRY principle using dynamic blocks.
Using Terraform Dynamic Blocks
Variables for Ingress and Egress Settings
First, we create variables to store our ingress and egress rules, making them reusable and easy to maintain:
variable "ingress_settings" {
type = list(object({
description = string
port = number
protocol = string
}))
default = [
{
description = "Allows SSH access"
port = 22
protocol = "tcp"
},
{
description = "Allows HTTP traffic"
port = 80
protocol = "tcp"
},
{
description = "Allows HTTPS traffic"
port = 443
protocol = "tcp"
}
]
}
variable "egress_settings" {
type = list(object({
description = string
port = number
protocol = string
}))
default = [
{
description = "Allows any protocol with any port access"
port = 0
protocol = "-1"
}
]
}
-
ingress_settings: Contains a list of ingress rules withdescription,port, andprotocol. -
egress_settings: Contains egress rules with similar attributes.
By storing these rules in variables, you make them flexible and easier to modify or expand.
Implementing Dynamic Blocks in Resource Block
resource "aws_vpc" "main_vpc" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "public_subnet" {
vpc_id = aws_vpc.main_vpc.id
cidr_block = "10.0.1.0/24"
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "public-subnet"
}
}
resource "aws_security_group" "main_sg" {
vpc_id = aws_vpc.main_vpc.id
dynamic "ingress" {
for_each = var.ingress_settings
content {
description = ingress.value["description"]
from_port = ingress.value["port"]
to_port = ingress.value["port"]
protocol = ingress.value["protocol"]
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
dynamic "egress" {
for_each = var.egress_settings
iterator = main_sg_egress
content {
description = main_sg_egress.value["description"]
from_port = main_sg_egress.value["port"]
to_port = main_sg_egress.value["port"]
protocol = main_sg_egress.value["protocol"]
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
tags = {
Name = "main-sg"
}
}
Let us Break Down the Dynamic Block Implementation
Explanation of the ingress Dynamic Block
Here's the ingress block from our Terraform configuration:
dynamic "ingress" {
for_each = var.ingress_settings
content {
description = ingress.value["description"]
from_port = ingress.value["port"]
to_port = ingress.value["port"]
protocol = ingress.value["protocol"]
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
Let’s break down each part to understand what's happening:
1. dynamic ingress Block
- Just like with the
egressblock, thedynamickeyword allows us to create multipleingressblocks based on the values invar.ingress_settings. - This means we can generate as many ingress rules as needed without having to hard-code each one.
2. for_each = var.ingress_settings
-
for_eachtells Terraform to iterate over the list defined in theingress_settingsvariable. - Each item in this list represents an individual ingress rule with attributes like
description,port, andprotocol.
3. The Absence of an Explicit iterator Definition
- Unlike the
egressblock, we have not defined aniteratorfor theingressblock. - When you omit the
iteratorattribute, Terraform defaults to using the block label (in this case,ingress) as the iteration variable. - This means that within the
contentblock,ingress.valuerefers to the current item in thefor_eachloop.
Why Is This Important?
- Using
ingress.valueis the default behavior, making the code simpler and cleaner when you don't need a custom iterator name. - This approach is ideal when you don’t have multiple nested dynamic blocks or when there is no risk of confusion.
4. content Block
- The
contentblock defines the structure of each dynamically generatedingressrule:-
description = ingress.value["description"]: Accesses thedescriptionfield of the current rule. -
from_portandto_port: Both set toingress.value["port"], indicating the port range for this rule. -
protocol = ingress.value["protocol"]: Sets the communication protocol (e.g.,tcp,udp). -
cidr_blocks = [aws_vpc.main_vpc.cidr_block]: Restricts access to the CIDR block defined by our main VPC.
-
Explanation of the egress Dynamic Block
Here's the relevant part of the code that uses a dynamic block for the egress rules:
dynamic "egress" {
for_each = var.egress_settings
iterator = main_sg_egress
content {
description = main_sg_egress.value["description"]
from_port = main_sg_egress.value["port"]
to_port = main_sg_egress.value["port"]
protocol = main_sg_egress.value["protocol"]
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
Let's break this down thoroughly:
1. dynamic egress Block
- The
dynamickeyword allows us to generate multiple instances of theegressblock based on an iterable set of data (var.egress_settingsin this case). - This approach provides a way to avoid manually duplicating the
egressblock for each rule, making our Terraform code much more maintainable and scalable.
2. for_each = var.egress_settings
- This line tells Terraform to loop through each item in the
var.egress_settingslist and create anegressblock for each element. -
var.egress_settingsis a variable of typelist(object({ ... })), which contains all the rules we want to define for our egress traffic.
3. iterator = main_sg_egress
- By default, Terraform uses the keyword
egressas the iteration variable to refer to the current item in thefor_eachloop. However, using theiteratorkeyword, we override this default behavior and provide our own custom name,main_sg_egress. - This means that instead of accessing
egress.valueto get the current item's properties, we now accessmain_sg_egress.value.
Why Use
iterator?
- The
iteratorattribute is particularly useful when you want to enhance the readability of your code or avoid conflicts when nesting multiple dynamic blocks that might otherwise share the same default name. In this case, ideally we do not need any iterator but to understand this concept we are using it.
4. content Block
- The
contentblock defines what theegressrules should look like for each item. - We access each value of the current item using
main_sg_egress.value. For example:-
main_sg_egress.value["description"]provides the description of the current egress rule. -
main_sg_egress.value["port"]specifies the port number forfrom_portandto_port. -
main_sg_egress.value["protocol"]gives the protocol type (e.g.,tcp,udp, or-1for all).
-
Example 2: Using map(object) with Dynamic Blocks for Simplified Ingress and Egress Rules
In this example, we'll take a more efficient approach by combining ingress and egress rules into a single variable using a map(object) type. This design allows us to handle security group rules with greater flexibility and simplicity, reducing redundancy and making it easier to manage changes.
Step 1: Define a map(object) Variable for Security Group Rules
We start by defining a variable named sg_settings, which holds all our security group rules (both ingress and egress) in a single structure. This variable is a map containing object entries that define each rule's details.
variable "sg_settings" {
type = map(object({
type = string
description = string
port = number
protocol = string
}))
default = {
ssh_ingress = {
type = "ingress"
description = "Allows SSH access"
port = 22
protocol = "tcp"
},
http_ingress = {
type = "ingress"
description = "Allows HTTP traffic"
port = 80
protocol = "tcp"
},
https_ingress = {
type = "ingress"
description = "Allows HTTPS traffic"
port = 443
protocol = "tcp"
},
all_egress = {
type = "egress"
description = "Allows all outbound traffic"
port = 0
protocol = "-1"
}
}
}
Explanation:
- The
sg_settingsvariable is defined as amap(object), meaning it's a collection of objects that you can look up by a key (likessh_ingressorall_egress). - Each entry (
ssh_ingress,http_ingress,https_ingress,all_egress) contains:-
type: Determines if the rule isingress(incoming) oregress(outgoing). -
description: Provides a human-readable explanation of the rule. -
port: Specifies which port to allow traffic through. -
protocol: Indicates the protocol type (e.g.,tcp,udp, or-1for all protocols).
-
By combining both ingress and egress rules into this single variable, we make the structure cleaner and avoid repetitive definitions.
Step 2: Use Dynamic Blocks to Define Ingress and Egress Rules in the Security Group Resource
Next, we create our aws_security_group resource, where we'll use Terraform's dynamic blocks to handle both ingress and egress rules based on the type specified in sg_settings.
resource "aws_security_group" "main_sg" {
vpc_id = aws_vpc.main_vpc.id
dynamic "ingress" {
for_each = { for key, rule in var.sg_settings : key => rule if rule.type == "ingress" }
content {
description = ingress.value.description
from_port = ingress.value.port
to_port = ingress.value.port
protocol = ingress.value.protocol
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
dynamic "egress" {
for_each = { for key, rule in var.sg_settings : key => rule if rule.type == "egress" }
content {
description = egress.value.description
from_port = egress.value.port
to_port = egress.value.port
protocol = egress.value.protocol
cidr_blocks = [aws_vpc.main_vpc.cidr_block]
}
}
tags = {
Name = "main-sg"
}
}
Explanation:
-
resource "aws_security_group" "main_sg"creates an AWS security group within the specified VPC (aws_vpc.main_vpc.id). - The
dynamicblock generates multipleingressoregressblocks based on the contents of oursg_settingsvariable.
Detailed Breakdown of the dynamic Blocks:
-
Dynamic
ingressBlock:-
for_eachiterates over each entry insg_settingswheretypeis"ingress". - Inside
content, the actual security group rule is created using values fromsg_settings. - This dynamic block allows us to generate multiple
ingressrules based on the entries in our variable.
-
-
Dynamic
egressBlock:- Similarly,
for_eachiterates over rules insg_settingswheretypeis"egress". - The content block sets the values for each egress rule based on the data in the variable.
- This ensures all outbound traffic rules are handled efficiently.
- Similarly,
Why This Approach Is Better
-
Unified Configuration: All rules are defined in a single, easy-to-manage variable (
sg_settings), reducing complexity and improving readability. -
Reduced Redundancy: The use of dynamic blocks means we avoid manually writing multiple
ingressandegressblocks, making the code cleaner. -
Flexibility: Adding or removing rules is as simple as modifying the
sg_settingsvariable, without needing to change the resource definition.
Example 3: Dynamic Blocks in Terraform for AMI Filtering
In this example, we leverage Terraform's dynamic blocks to filter Amazon Machine Images (AMIs) based on specific tags, enabling streamlined resource creation. This approach enhances configuration ability and allows for efficient management of EC2 instances to specific environments.
Variables and AMI Filtering
variable "ami_tag_filters" {
description = "A list of tag filters to locate specific Amazon Machine Images (AMIs)"
default = [
{
name = "tag:Purpose"
values = ["WebServer"]
},
{
name = "tag:Environment"
values = ["Production"]
}
]
}
The ami_tag_filters variable is defined as a list of filters that contain key-value pairs of tags. The variable is used to dynamically search for AMIs based on tags like Purpose and Environment. Here, the AMI is tagged with Purpose = WebServer and Environment = Production.
Data Source for AMI Selection
data "aws_ami" "filtered_ami" {
most_recent = true
owners = ["self"]
dynamic "filter" {
for_each = var.ami_tag_filters
content {
name = filter.value.name
values = filter.value.values
}
}
}
The data "aws_ami" "filtered_ami" block is used to dynamically filter AMIs based on the specified tags. The most_recent = true parameter ensures that the most recent AMI that matches the filters is selected. The owners attribute is set to "self", meaning only AMIs owned by the current AWS account will be considered.
Dynamic blocks are used in this section to iterate over the ami_tag_filters list. The for_each parameter takes each filter in the list and applies it to the AMI search. Inside the content block, the tag name and values are applied to each filter. This flexibility allows you to add or remove filters without modifying the data block itself.
EC2 Instance Creation
resource "aws_instance" "web_server_instance" {
count = 2
ami = data.aws_ami.filtered_ami.id
instance_type = "t2.micro"
tags = {
Name = "WebServer-${count.index}"
}
}
The aws_instance resource provisions two EC2 instances using the t2.micro instance type. The AMI is dynamically retrieved from the filtered AMI data source. The count parameter creates two instances, and the instance's name tag is dynamically generated using the ${count.index} syntax, which appends an index to the name (WebServer-0, WebServer-1).
Example 4: Helm Release Using Dynamic Blocks
Before Dynamic Block
resource "helm_release" "metric_server" {
name = "metrics-server"
chart = "metrics-server"
repository = "https://kubernetes-sigs.github.io/metrics-server/"
version = "3.12.0"
set {
name = "replicas"
value = "2"
}
set {
name = "metrics.enabled"
value = "true"
}
set {
name = "serviceMonitor.enabled"
value = "true"
}
}
This block of code works perfectly well, but it becomes difficult to scale as the number of settings increases. If you need to add more options or tweak existing ones, you'd have to repeat the same set block multiple times, which leads to redundancy and reduced maintainability. Now, let's optimize it using dynamic blocks to make it more modular, reusable, and maintainable.
After Dynamic Block
# Define variables
variable "metrics_server_settings" {
type = map(string)
default = {
"replicas" = "2"
"metrics.enabled" = "true"
"serviceMonitor.enabled" = "true"
}
}
resource "helm_release" "metric_server" {
name = "metrics-server_release"
chart = "metrics-server"
repository = "https://kubernetes-sigs.github.io/metrics-server/"
version = "3.12.0"
dynamic "set" {
for_each = var.metrics_server_settings
content {
name = set.key
value = set.value
}
}
}
Breakdown of the Dynamic Configuration
-
Variable Definition:
- The
metrics_server_settingsvariable is defined as amap(string). This type allows us to store multiple key-value pairs for our Helm release configuration. - The default values in this example include
replicas,metrics.enabled, andserviceMonitor.enabled. These correspond to the settings we need for themetrics-serverHelm chart.
- The
-
Dynamic Block:
- The dynamic block leverages the
for_eachargument to loop over the key-value pairs invar.metrics_server_settings. - For every key in the map, Terraform generates a
setblock, where the key becomes the Helm chart setting (e.g.,replicasormetrics.enabled), and the value is passed to theset.value.
- The dynamic block leverages the
Why the Dynamic Block is Better
The use of dynamic blocks transforms a static, repetitive setup into a flexible and concise solution. Instead of writing multiple set blocks, you can define all your settings in a map variable and then iterate over them, automatically generating the necessary set blocks. This approach not only reduces code duplication but also allows for easy updates and future scalability.
For example, if you want to add another setting, such as configuring resource limits, you would simply modify the variable:
variable "metrics_server_settings" {
type = map(string)
default = {
"replicas" = "2"
"metrics.enabled" = "true"
"serviceMonitor.enabled" = "true"
"resources.limits.cpu" = "100m"
}
}
With this, the dynamic block automatically picks up the additional setting without needing any changes to the core Helm release resource code.
Example 5: Nested Dynamic Blocks (EC2 Instance with Dynamic EBS Volumes and Tags)
When dealing with cloud infrastructure, flexibility is key—especially when managing resources like EBS volumes attached to EC2 instances. In this example, we'll use nested dynamic blocks to dynamically attach multiple EBS volumes to an EC2 instance, each with its own configuration and tags.
This scenario reflects a more realistic infrastructure setup where you may need to attach additional storage to your EC2 instance based on different workload needs, such as adding extra volumes for data storage, backup, or application-specific requirements. We'll also apply unique tags to these volumes, helping with organization, cost tracking, or environment differentiation.
Let’s dive in to see how dynamic blocks can help make your Terraform configuration flexible and scalable.
Variables Definition Using map(object)
We’ll define the EBS volumes, their sizes, types, and specific tags using a map(object) variable.
variable "ebs_volumes" {
type = map(object({
size = number
type = string
tags = map(string)
}))
default = {
"data_volume" = {
size = 50
type = "gp3"
tags = {
Name = "Data-Volume"
Env = "Prod"
Usage = "Database"
}
},
"backup_volume" = {
size = 100
type = "gp3"
tags = {
Name = "Backup-Volume"
Env = "Prod"
Usage = "Backup"
}
}
}
}
In this example:
- The
ebs_volumesvariable is a map where:-
Keys (
data_volume,backup_volume) represent unique identifiers for the volumes. - Each volume is defined as an object containing:
- size: The volume size in GB.
-
type: The type of the EBS volume (e.g.,
gp3). - tags: A map of key-value pairs to tag each volume, such as its purpose, environment, and other metadata.
-
Keys (
Terraform Configuration with Nested Dynamic Blocks
Using the dynamic block setup, we’ll create an EC2 instance that dynamically attaches the specified volumes and applies unique tags to each volume.
resource "aws_instance" "example" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
dynamic "ebs_block_device" {
for_each = var.ebs_volumes
iterator = ebs_volume
content {
device_name = "/dev/sd${ebs_volume.key}"
volume_size = ebs_volume.value.size
volume_type = ebs_volume.value.type
dynamic "tags" {
for_each = ebs_volume.value.tags
iterator = volume_tag
content {
key = volume_tag.key
value = volume_tag.value
}
}
}
}
tags = {
Name = "App-EC2-Instance"
Env = "Prod"
}
}
Explanation:
-
Dynamic Block for
ebs_block_device:- The dynamic block uses the
ebs_volumeiterator to loop over thevar.ebs_volumesmap. Each key in this map represents an individual volume, and the block dynamically configures thedevice_name,volume_size, andvolume_typebased on the attributes of eachebs_volume. - The
device_nameis generated dynamically using theebs_volume.keyto ensure each attached volume has a unique identifier. Thevolume_sizeandvolume_typeare set according to the values provided in theebs_volumesvariable.
- The dynamic block uses the
-
Nested Dynamic Block for
tags:- Inside each
ebs_volumeblock, the nested dynamic block iterates over the volume's tags using thevolume_tagiterator. This enables Terraform to apply a set of key-value tags to each specific volume. - The
volume_tag.keyandvolume_tag.valueensure that each volume receives the exact tags as defined in theebs_volumesmap.
- Inside each
By using meaningful iterators like ebs_volume and volume_tag, the structure of the Terraform configuration becomes clearer, enhancing readability and maintainability. The use of nested dynamic blocks ensures flexibility and future-proofing, allowing for efficient management of multiple EBS volumes and consistent application of tags. This approach keeps the configuration clean, scalable, and easy to adapt as infrastructure needs grow.
Conclusion
Get hooked on Terraform dynamic blocks! These powerful features streamline your infrastructure as code configurations, reducing redundancy, enhancing readability, and improving maintainability. By leveraging dynamic blocks, you can create more flexible and scalable solutions, simplifying complex setups and managing resources with ease. So, dive into the world of Terraform dynamic blocks and experience the transformation power they bring to your infrastructure management.
Top comments (0)