5.1 Terraform Dynamic Blocks and For-Each Loops: Smarter Infrastructure Definitions

Terraform Dynamic Blocks and For-Each Loops: Smarter Infrastructure Definitions
Hey everyone! Ever felt like you were copy-pasting blocks of code in your Terraform configurations, just changing minor details for slightly different resources? That's a sign you could be making your infrastructure definitions smarter and more maintainable. Today, we're diving into two powerful Terraform features that will help you do just that: Dynamic Blocks and For-Each Loops.
Think of your Terraform code as a recipe. If you want to bake five identical cakes, you wouldn't copy-paste the entire recipe five times, right? You'd just make one recipe and bake it five times! Dynamic Blocks and For-Each Loops let you do something similar for your infrastructure.
What are Dynamic Blocks?
Imagine you're ordering a pizza. Some people want pepperoni, some want mushrooms, some want both, and some want neither. A dynamic block in Terraform lets you define optional blocks of configuration based on conditions. Think of it as saying, "If the customer wants pepperoni, then add the pepperoni ingredients."
In essence, a dynamic block lets you conditionally generate repetitive configurations within a resource. It's great for situations where certain arguments are only needed based on specific conditions.
What are For-Each Loops?
For-Each loops are more straightforward. They allow you to create multiple resources or configurations based on a list or a map. Continuing the cake analogy, if you want to bake five identical cakes, you would use the for-each loop with the number 5.
Real-World Example: Kubernetes Network Policies
Let's say we're deploying a Kubernetes application and want to create Network Policies to control traffic flow. We want to create policies for each namespace, and each namespace might have different allowed ingress rules.
resource "kubernetes_network_policy" "example" {
for_each = var.namespaces
metadata {
name = "allow-from-${each.key}"
namespace = each.key
}
spec {
pod_selector {
match_labels = {
app = "my-app"
}
}
# Using Dynamic Blocks for ingress rules
dynamic "ingress" {
for_each = each.value.allowed_namespaces
content {
from {
namespace_selector {
match_labels = {
name = ingress.value
}
}
}
}
}
policy_types = ["Ingress"]
}
}
variable "namespaces" {
type = map(object({
allowed_namespaces = list(string)
}))
default = {
"namespace-a" = {
allowed_namespaces = ["namespace-b", "namespace-c"]
}
"namespace-b" = {
allowed_namespaces = ["namespace-a"]
}
"namespace-c" = {
allowed_namespaces = [] # No ingress allowed
}
}
}
Explanation:
for_each = var.namespaces: This creates a separatekubernetes_network_policyresource for each key-value pair in thenamespacesvariable. Theeach.keyrefers to the namespace name (e.g., "namespace-a"), andeach.valuerefers to the object containingallowed_namespaces.dynamic "ingress": This createsingressblocks only for the namespaces listed ineach.value.allowed_namespaces. If a namespace has an emptyallowed_namespaceslist, noingressblocks are created.ingress.value: Inside thedynamic "ingress"block,ingress.valuerefers to each individual element within theallowed_namespaceslist.
Without Dynamic Blocks and For-Each Loops:
Imagine writing this code without these features! You'd have to duplicate the entire kubernetes_network_policy block multiple times, hardcoding the namespace names and allowed namespaces. That would be a maintenance nightmare!
Challenge: Dealing with Complex Logic within Dynamic Blocks
Sometimes, you need more complex logic inside your dynamic blocks. For example, you might want to conditionally add different types of from blocks based on whether you're allowing traffic from a namespace or an IP address.
Solution: Using Ternary Operators and Local Variables
You can use ternary operators and local variables to simplify complex logic:
dynamic "ingress" {
for_each = each.value.allowed_sources
content {
dynamic "from" {
for_each = ingress.value.type == "namespace" ? [1] : []
content {
namespace_selector {
match_labels = {
name = ingress.value.name
}
}
}
}
dynamic "from" {
for_each = ingress.value.type == "ip_block" ? [1] : []
content {
ip_block {
cidr = ingress.value.cidr
}
}
}
}
}
variable "namespaces" {
type = map(object({
allowed_sources = list(object({
type = string
name = optional(string)
cidr = optional(string)
}))
}))
default = {
"namespace-a" = {
allowed_sources = [
{ type = "namespace", name = "namespace-b" },
{ type = "ip_block", cidr = "10.0.0.0/24" }
]
}
}
}
In this example, allowed_sources is now a list of objects that have a type attribute and either a name or a cidr depending on the type. We then use two dynamic blocks with ternary operators to conditionally create the correct from blocks.
Benefits of Using Dynamic Blocks and For-Each Loops:
Reduced Code Duplication: Write less code and avoid repetitive configurations.
Increased Readability: Make your Terraform configurations easier to understand.
Improved Maintainability: Simplify changes and updates to your infrastructure.
Dynamic Infrastructure: Easily adapt to changing requirements.
Conclusion:
Dynamic Blocks and For-Each Loops are essential tools in your Terraform toolkit. They empower you to create more flexible, maintainable, and efficient infrastructure definitions. Start experimenting with them today, and you'll quickly see the benefits! Happy Terraforming!




