WintelGuy.com

Terraform for_each Tutorial with Practical Examples

Contents

Introduction

Terraform is a popular infrastructure-as-code tool (IaC) that allows you to define, provision, and manage cloud infrastructure using declarative configuration. As your infrastructure grows, so does the need to efficiently manage multiple similar resources, whether you're creating storage buckets, firewall rules, or IAM accounts.

One of Terraform's most powerful constructs for handling such scenarios is for_each. This meta-argument lets you dynamically create resources using maps, sets, or objects. Compared to more basic count, for_each provides enhanced control and flexibility, especially when working with named resources with some configuration variations.

In this tutorial, you'll learn how to use for_each through a series of practical, gradually more advanced examples. We'll focus on Google Cloud Platform (GCP) resources that are either free or very low-cost, so you can follow along without incurring unnecessary charges.

You'll learn:

  • How for_each works and when to use it
  • The difference between for_each and count
  • How to loop over maps and sets to create multiple resources
  • How to use for_each with more complex input structures

By the end of this tutorial, you should be able to use for_each in your own Terraform projects to reduce code repetition and improve maintainability.

Prerequisites

Before you begin, ensure you have the following:

  • A Google Cloud Platform account with billing enabled
  • A GCP project with necessary APIs (e.g., IAM, Storage) enabled
  • Terraform installed
  • Google Cloud CLI (gcloud) installed and authenticated
  • Terraform authenticated with GCP, either by:
    • Running gcloud auth application-default login, or
    • Setting up a GCP service account and downloading its key for use with Terraform
  • Basic knowledge of HCL (HashiCorp Configuration Language)

Note: Most examples in this tutorial use GCP's free tier services. Still, keep an eye on your GCP billing and always delete unnecessary resources to avoid unexpected charges.

Back to Top

Understanding for_each in Terraform

When managing infrastructure with Terraform, you often need to create multiple resources of the same type, for example, several GCP storage buckets or service accounts. While you could use separate resource blocks for each instance, such approach quickly becomes inefficient and error-prone.

Terraform's for_each meta-argument solves this problem by letting you define a single code block (or a module) that can provision multiple resources, each with its own unique configuration defined by an input collection. This helps you write cleaner, more scalable infrastructure code.

for_each Syntax Overview

When for_each is added to a resource block, Terraform loops over the corresponding collection, which could be either a set, map, or map of objects, and creates one resource instance for each element in the collection.

resource "resource_type" "name" { for_each = <collection> # Access the current key and value name = each.key property = each.value ... }

How it Works:

  • for_each = <collection> accepts a set, map, or map(object) as input.
  • As Terraform loops over the input collection:
    • each.key assumes the current key from the map or the element itself if using a set.
    • each.value holds the value for the current element. If a set was provided, this is the same as each.key.
  • Terraform generates one resource instance per element in the collection. Each instance can be uniquely identified and referenced using its key: resource_type.name["key"].

Examples of input collections that can be used with for_each:

a simple set of strings - set(string):

variable "file_names" { type = set(string) default = ["alpha", "beta", "gamma"] }

a key-value mapping - map(string):

variable "user_roles" { type = map(string) default = { "alice" = "admin" "bob" = "viewer" "carol" = "editor" } }

a more complex structure for advanced use cases - map(object):

variable "buckets" { type = map(object({ location = string storage_class = string })) default = { "data" = { location = "US", storage_class = "STANDARD" } "logs" = { location = "US", storage_class = "NEARLINE" } } }

Note: If you have a list, you must convert it to a set with toset(var.my_list).

for_each vs count

Terraform provides two ways to create multiple resources: count and for_each. While they can produce similar results, count and for_each have important differences:

Feature count for_each
Input Type list or integer map or set
Item Reference count.index each.key, each.value
Resource Addressing Index-based (e.g., resource[0]) Named keys (e.g., resource["web-server"])
Ordering Yes (based on list index) No (map/set keys are independent)

Key for_each Concepts to Remember

  • Use for_each:
    • When your data is a map or a set
    • When each resource needs a unique identifier or tracking individual resources by name is important
  • The keys in the input collection must be unique
  • When using for_each, each resource instance is addressed with its key - resource_type.name["key"]
  • You cannot use count and for_each in the same resource block.

Back to Top

Example 1: Creating Local Files (with set(string))

Before diving into cloud-specific resources, let's begin with a very simple example that illustrates the behavior of for_each using Terraform's local_file resource. This resource block writes a file to your local machine and does not require any cloud credentials, making it perfect for learning and experimentation.

Goal: Create multiple local .txt files from a list of names using for_each.

Terraform Code:

# Input Variable variable "file_names" { type = set(string) default = ["report", "summary", "notes"] } # Resource Definition Using for_each resource "local_file" "example" { for_each = var.file_names filename = "${path.module}/${each.key}.txt" content = "Key: ${each.key}, Value: ${each.value}." }

Explanation:

  • The file_names variable is a set of strings, which ensures that each file name is unique.
  • for_each = var.file_names tells Terraform to create one local_file resource per each item in file_names.
  • path.module is a Terraform built-in variable that refers to the directory where the current .tf file (module) is located.
  • each.key retrieves the current file name from the set.
  • each.value contains the same value as each.key.
  • Files are written to the current directory with names: report.txt, summary.txt, and notes.txt.

Result:

After running

  • terraform init
  • terraform apply

Terraform will create the following files in your working directory:

  • report.txt
  • summary.txt
  • notes.txt

Cleaning Up:

To delete the files, simply run terraform destroy. Confirm when prompted, and Terraform will delete all the created resources.

Summary:

  • This example shows how for_each loops over a set and creates multiple local_file resources.
  • We demonstrated how each resource instance is mapped to the corresponding set value with each.key.
  • The example runs completely locally and does not require access to GCP or any cloud provider. It provides a convenient way to experiment with the mechanics of for_each before applying it to more complex cloud resources.

Back to Top

Example 2: Batch Create IAM Service Accounts (with map(string))

In this example, we'll use for_each with a map(string) to create multiple IAM service accounts in Google Cloud. This approach allows you to assign each account a unique ID and a descriptive display name.

Using a map instead of a set gives us a key-value pair structure, enabling easy reference to individual resources by their logical names, and making the configuration more readable.

Goal: Create multiple GCP IAM service accounts, each with:

  • A unique account ID
  • A human-friendly display name

Terraform Code:

# Provider Configuration provider "google" { project = var.project_id } # Input Variables variable "project_id" { description = "Your GCP project ID" type = string } variable "service_accounts" { description = "Map of service account IDs and display names" type = map(string) default = { app-engine-sa = "App Engine Service Account" billing-sa = "Billing Manager Account" monitoring-sa = "Monitoring Service Account" } } # Resource Definition Using for_each resource "google_service_account" "sa" { for_each = var.service_accounts account_id = each.key display_name = each.value } # Outputs output "service_account_info" { description = "Combined info for each service account" value = { for k, sa in google_service_account.sa : k => { email = sa.email name = sa.name } } }

Explanation:

  • Input variable service_accounts is a map(string) where:
    • key = service account ID ("app-engine-sa", and so on)
    • value = display name ("App Engine Service Account", and so on)
  • for_each = var.service_accounts tells Terraform to create one service account for each map entry.
  • each.key provides access to the keys from the input map.
  • each.value references the corresponding service account display name from the input map.
  • The output block returns configuration details for the created accounts.
  • The for expression iterates over the resource list google_service_account.sa to retrieve account details.

Result:

After running

  • terraform init
  • terraform apply

Terraform will perform the following actions:

  • create three service account resources, uniquely addressable as
  • google_service_account.sa["app-engine-sa"]
  • google_service_account.sa["billing-sa"]
  • google_service_account.sa["monitoring-sa"]
  • output account parameters:
service_account_info = { "app-engine-sa" = { "email" = "app-engine-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" "name" = "projects/YOUR_PROJECT_ID/serviceAccounts/app-engine-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" } "billing-sa" = { "email" = "billing-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" "name" = "projects/YOUR_PROJECT_ID/serviceAccounts/billing-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" } "monitoring-sa" = { "email" = "monitoring-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" "name" = "projects/YOUR_PROJECT_ID/serviceAccounts/monitoring-sa@YOUR_PROJECT_ID.iam.gserviceaccount.com" } }

Cleaning Up:

To delete the accounts, simply run terraform destroy. Confirm when prompted, and Terraform will delete all the created resources.

Summary:

In this example we demonstrated how to use for_each with a map(string) to create uniquely named resources with distinct configurations and display the resource parameters with the help of a for loop.

Back to Top

Example 3: Creating GCS Buckets from a List (with map(object))

In this example, we'll use for_each with a map(object) to dynamically create multiple Google Cloud Storage (GCS) buckets. Each bucket will have a unique name and configurable attributes, such as location, storage class, and versioning, all defined in a single input variable.

This pattern is ideal for batch provisioning cloud resources of the same type with individualized configurations.

Goal: Create multiple GCS buckets in different regions, with configurable storage classes and versioning options, using a single input variable.

Terraform Code:

# Provider Configuration provider "google" { project = var.project_id } # Input Variables variable "project_id" { description = "Your GCP project ID" type = string } variable "gcs_buckets" { description = "Map of bucket names to their config (location, storage class, versioning)" type = map(object({ location = string storage_class = string versioning = bool })) default = { "my-logs-bucket" = { location = "US-EAST1" storage_class = "STANDARD" versioning = true } "my-archive-bucket" = { location = "US-WEST1" storage_class = "NEARLINE" versioning = false } } } # Resource Definition Using for_each resource "google_storage_bucket" "buckets" { for_each = var.gcs_buckets name = "${each.key}-${var.project_id}" location = each.value.location storage_class = each.value.storage_class versioning { enabled = each.value.versioning } # delete bucket and content on destroy. force_destroy = true } # Output output "bucket_urls" { description = "URLs of the created buckets" value = { for k, b in google_storage_bucket.buckets : k => b.url } }

Explanation:

  • Input variable gcs_buckets is a map of objects, where each:
    • key = bucket name,
    • value = object with bucket configuration, including location, class, and versioning.
  • for_each = var.gcs_buckets loops over the map and creates one bucket per entry.
  • each.key refers in turn to each bucket name (key) from the map.
  • each.value.location, each.value.storage_class, and each.value.versioning refer the corresponding bucket configuration properties.
  • ${...}-${var.project_id} appends the project ID to ensure globally unique bucket names.
  • force_destroy = true allows you to delete a bucket that contains objects (to simplify cleanup process).
  • The output block returns configuration details for the created buckets.
  • The for expression iterates over the resource list google_storage_bucket.buckets to retrieve bucket details.

Result:

After running

  • terraform init
  • terraform apply

Terraform will perform the following actions:

  • create two buckets
  • my-logs-bucket-YOUR_PROJECT_ID in US-EAST1 with STANDARD storage class and versioning enabled
  • my-archive-bucket-YOUR_PROJECT_ID in US-WEST1 with NEARLINE storage class and versioning disabled
  • output bucket URLs:
bucket_urls = { "my-archive-bucket" = "gs://my-archive-bucket-YOUR_PROJECT_ID" "my-logs-bucket" = "gs://my-logs-bucket-YOUR_PROJECT_ID" }

Cleaning Up:

To delete the buckets, simply run terraform destroy. Confirm when prompted, and Terraform will delete all the created resources.

Summary:

In this example we demonstrated how to use for_each with a map(object) to create uniquely named resources with distinct configurations and output the resource parameters with the help of a for loop.

Back to Top

Limitations and Caveats

Cannot use for_each and count together

  • Terraform does not allow both count and for_each on the same resource.

Changing the for_each key map can cause resource recreation

  • If you change the keys in a for_each map (e.g., rename a key), Terraform will destroy and recreate the resource, even if the underlying configuration is the same.

Avoid using set() keys in resource references

  • When you use a set(string) with for_each, Terraform automatically uses each set item as the key for the corresponding resource instance. But because sets may not be stable, these auto-generated keys aren't ideal for referencing specific resource instances later in your code.

Back to Top

Best Practices & Tips

for_each in Terraform is very useful, but it's important to follow some best practices and understand its limitations to ensure your configurations are maintainable, predictable, and scalable.

Prefer for_each over count for complex or keyed data

  • for_each gives better control and more readable plans when working with maps or sets of strings.
  • Each resource gets a named key, for example: google_service_account.sa["monitoring"], rather than an index google_service_account.sa[0] (with count), which helps avoid issues if items are reordered or changed.

Use Maps for Stable Resource Identifiers

  • Maps ensure each resource has a consistent key.
  • Ideal for outputs, referencing specific resources, and maintaining state when items are added/removed.

Combine outputs using a for expression

  • Instead of multiple output blocks, structure outputs into a single map of objects for better organization and downstream use.

Leverage complex objects for multi-property resources

  • Define input variables as map(object({ ... })) when each resource needs distinct parameters. This allows granular control and clean data modeling.

Use Modules with for_each

  • If you're managing multiple related resources per item (e.g., bucket + IAM + logging), consider using a module and applying for_each at the module level.

Back to Top

Conclusion

The for_each meta-argument in Terraform is a powerful feature that allows you to manage multiple resources efficiently and predictably. By leveraging maps and sets as input, for_each gives you control over resource creation, naming, and referencing, making your infrastructure code cleaner, more scalable, and easier to maintain.

In this tutorial, we explored:

  • The core syntax and behavior of for_each meta-argument.
  • The differences between using maps, sets, and lists with for_each.
  • Practical examples ranging from local file creation to provisioning GCP IAM service accounts and GCS buckets.

Understanding how to structure input data properly is key to using for_each effectively. As your infrastructure grows in complexity, these patterns become essential for writing modular, dynamic, and robust Terraform code.

See Also:
Handling Terraform State in Multi-Environment Deployments
Understanding Terraform Variable Precedence
Terraform Value Types Tutorial
Terraform count Explained with Practical Examples
Exploring Terraform dynamic Blocks with GCP Examples
Working with External Data in Terraform
Handling Sensitive and Ephemeral Data in Terraform
Terraform Modules FAQ