Keywords: Terraform | for_each | List of Objects | GCP Compute Instances | Infrastructure as Code
Abstract: This technical article provides an in-depth exploration of using for_each to iterate through lists of objects in Terraform 0.12. Through analysis of GCP compute instance deployment scenarios, it details the conversion of lists to maps for efficient iteration and compares different iteration patterns. The article also discusses state management differences between for_each and count, offering complete solutions for infrastructure-as-code loop processing.
Introduction
In infrastructure-as-code (IaC) practices, efficiently managing multiple similar resources is a common requirement. Terraform, as a mainstream IaC tool, introduced the for_each meta-argument in version 0.12, providing more flexible solutions for resource iteration. This article focuses on how to iterate through lists of objects in Terraform 0.12, particularly for GCP compute instance deployment scenarios.
Core Concept: List to Map Conversion
Terraform's for_each requires input to be either a map or a set of strings. When dealing with lists of objects, they must be converted to map format where keys serve as unique identifiers. This conversion not only meets syntactic requirements but also ensures stable identification of resource instances.
Consider the following GCP compute instance configuration example:
"gcp_zone": "us-central1-a",
"image_name": "centos-cloud/centos-7",
"vms": [
{
"hostname": "test1-srfe",
"cpu": 1,
"ram": 4,
"hdd": 15,
"log_drive": 300,
"template": "Template-New",
"service_types": [
"sql",
"db01",
"db02"
]
},
{
"hostname": "test1-second",
"cpu": 1,
"ram": 4,
"hdd": 15,
"template": "APPs-Template",
"service_types": [
"configs"
]
}
]Practical Solution
For the above configuration, the best practice is to use a for expression to convert the list to a map:
resource "google_compute_instance" "node" {
for_each = {for vm in var.vms: vm.hostname => vm}
name = "${each.value.hostname}"
machine_type = "custom-${each.value.cpu}-${each.value.ram*1024}"
zone = "${var.gcp_zone}"
boot_disk {
initialize_params {
image = "${var.image_name}"
size = "${each.value.hdd}"
}
}
network_interface {
network = "${var.network}"
}
metadata = {
env_id = "${var.env_id}"
service_types = "${join(",",each.value.service_types)}"
}
}The advantages of this approach include:
- Stability: Using
hostnameas the key ensures resource identification remains stable when list order changes - Precision: When removing middle instances, Terraform can accurately identify and remove target resources without unexpected impacts
- Readability: Clear configuration structure facilitates maintenance and understanding
Comparison with Other Iteration Patterns
While this article primarily focuses on iterating through lists of objects, understanding other iteration patterns provides comprehensive knowledge of Terraform's looping mechanisms:
String List Iteration: The simplest case, directly using the toset() function:
locals {
ip_addresses = ["10.0.0.1", "10.0.0.2"]
}
resource "example" "example" {
for_each = toset(local.ip_addresses)
ip_address = each.key
}Cartesian Product Generation: Combining multiple lists to create combinations:
locals {
domains = ["https://example.com", "https://stackoverflow.com"]
paths = ["/one", "/two", "/three"]
}
resource "example" "example" {
urls = flatten([
for domain in local.domains : [
for path in local.paths : {
domain = domain
path = path
}
]
]))
}Important State Management Considerations
The reference article "For_each on resources causing recreation when item removed from list" reveals critical differences in state management between for_each and count. When using count, resource instances are identified by numeric indices (e.g., [0], [1]), and removing middle instances causes subsequent instances to be renumbered, potentially triggering unexpected resource recreation.
In contrast, for_each uses string keys (e.g., ["test1-srfe"]), where each resource instance has a stable identifier. This design ensures:
- Removing specific instances doesn't affect other instances
- Resource identification in state files remains consistent
- Infrastructure changes are more predictable and secure
Modular Extension
For more complex deployment scenarios, consider a modular approach. Terraform 1.3 and later versions support using for_each in modules:
variable "hosts" {
type = map(object({
cpu = optional(number, 1)
ram = optional(number, 4)
hdd = optional(number, 15)
log_drive = optional(number, 300)
template = optional(string, "Template-New")
service_types = list(string)
}))
}This modular design enhances code reusability and maintainability.
Best Practices Summary
Based on the analysis and practical experience presented in this article, the following best practices are recommended:
- Prefer Unique Identifiers: In list-to-map conversion, choose fields with business significance as unique keys
- Avoid Using Indices: Do not use list indices as keys since they change with list order
- Use Dynamic Key Generation Cautiously: Avoid using functions like
uuid()that generate different values on each execution - Consider Modularization: For complex infrastructure, adopt modular design to improve code organization
- Test Deletion Scenarios: Test scenarios involving middle instance deletion before deployment to ensure expected behavior
Conclusion
Terraform 0.12's for_each functionality provides powerful tools for iterative infrastructure management. By converting lists of objects to maps and using meaningful unique identifiers as keys, stable and predictable resource management can be achieved. This approach not only solves GCP compute instance deployment issues but also provides reference templates for similar scenarios on other cloud platforms. As Terraform versions evolve, these core concepts will continue to provide a solid foundation for infrastructure-as-code practices.