WintelGuy.com

Exploring Terraform dynamic Blocks with GCP Examples

Contents

Introduction

When working with Terraform to provision cloud infrastructure, you may encounter situations where you need to specify multiple environment variables, add multiple disks or IAM users, each requiring its own nested block structure. Writing each of these blocks separately can lead to repetitions and decreased code maintainability.

That's where dynamic blocks come in. Terraform's dynamic block feature allows you to programmatically generate nested blocks based on variable input. This can significantly reduce code duplication and make your infrastructure more modular and scalable.

In this tutorial, we'll illustrate how to use dynamic blocks in Terraform with a couple of examples. By the end of this tutorial, you'll understand how and when to use dynamic blocks to improve your own Terraform infrastructure-as-code projects.

Prerequisites

If you want to follow along, ensure you have the following:

  • A Google Cloud Platform account with billing enabled
  • A GCP project with necessary APIs (e.g., Compute, Cloud Run) 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 with sufficient permissions and downloading its key for use with Terraform
  • Basic knowledge of HCL (HashiCorp Configuration Language), including providers, variables, and resource definitions

With that setup out of the way, let's dive into what Terraform dynamic blocks are and how you can use them to write cleaner and flexible infrastructure code.

Back to Top

What is a Terraform dynamic Block?

In Terraform, many resources include nested blocks that you define explicitly, for example, multiple env variables inside a google_cloud_run_service or multiple attached_disk blocks in a compute instance resource. These sub-blocks usually have the same structure and differ only in their values.

Here's what multiple env variable blocks might look like in a static configuration without a dynamic block:

resource "google_cloud_run_service" "default" { name = "cloudrun-hello" location = "us-east1" template { spec { containers { image = "us-docker.pkg.dev/cloudrun/container/hello" env { name = "APP" value = "web" } env { name = "ENV" value = "prod" } } } } }

This approach works, but it becomes unwieldy if you want to add, remove, or modify the list of environment variables dynamically, especially if you want to drive these values from variables or data sources.

A dynamic block lets you programmatically generate multiple nested blocks of the same type, using input data structured as a set or map. Instead of coding the same structure multiple times, you define it once with dynamic block which instructs Terraform to generate a nested block for every item included in the input variable.

Here's the dynamic equivalent of the previous static example:

variable "env_vars" { type = map(string) default = { APP = "web" ENV = "prod" } } resource "google_cloud_run_service" "default" { name = "cloudrun-hello" location = "us-east1" template { spec { containers { image = "us-docker.pkg.dev/cloudrun/container/hello" dynamic "env" { for_each = var.env_vars content { name = env.key value = env.value } } } } } }

The dynamic "env" block replaces two env blocks. This version is cleaner, scalable, driven entirely by input variable, and more suitable for automation and reuse.

Anatomy of a dynamic Block

The dynamic block has a specific structure and behaves differently from regular Terraform configuration blocks. Here's a breakdown of its components:

dynamic "block_name" { for_each = <collection> iterator = <iterator_name> # optional, defaults to "block_name" content { # block content using iterator or block_name } }

block_name

  • This is the name of the block you want to generate dynamically (e.g., env, rule, etc.).
  • It must match a valid sub-block name expected by the parent resource.

for_each (required)

  • <collection> is the iterable data structure that defines sub-blocks' parameters. It can be a list, map, or set.
  • Terraform will generate one sub-block instance for each element in the collection.

iterator (optional)

  • Allows you to customize the name used to reference items in the collection (like a loop variable).
  • If omitted, Terraform defaults to block_name.
  • Useful when block_name conflicts with an attribute name, or for clarity in large blocks.

Example with iterator:

dynamic "env" { for_each = var.env_vars iterator = item content { name = item.key value = item.value } }

labels (optional)

  • It's only needed when you're generating nested blocks with labels (i.e., identifiers after the block type).
  • Specifies a list of labels, in order, to use for each generated sub-block.
  • You can use the temporary iterator variable in this value.

Now, let's review the google_cloud_run_service configuration example with dynamic block in more details.

Back to Top

Example 1: Cloud Run Configuration with env Blocks

This google_cloud_run_service example configuration:

  • uses dynamic block to configure environment variables for a Cloud Run service
  • reads env parameters from an input variable (env_vars)
  • gracefully handles the case when env_vars is empty (i.e., no environment variables will be added)
  • outputs resulting Cloud Run configuration parameters

Warning:

This configuration provisions Google Cloud resources that may result in billing charges:

  • Cloud Run service: Billed based on actual usage, including request count, request duration, and allocated CPU/memory.
  • Egress traffic: Outbound network traffic (especially to the public internet or between regions) is billed separately.

To avoid unexpected charges, remember to delete the service when no longer needed using terraform destroy.

Terraform Code:

# main.tf # Provider Configuration provider "google" { project = var.project_id } # Input Variables variable "project_id" { description = "Your GCP project ID" type = string } variable "env_vars" { description = "Map of environment variables to inject into the Cloud Run container" type = map(string) default = {} # Empty by default — no env vars } # Cloud Run Resource Definition resource "google_cloud_run_service" "default" { name = "cloudrun-hello" location = "us-east1" metadata { annotations = { # Allow access w/o authentication for testing "run.googleapis.com/invoker-iam-disabled" = "true" } } template { spec { containers { image = "us-docker.pkg.dev/cloudrun/container/hello" # Dynamically generate 'env' blocks from 'env_vars' input dynamic "env" { for_each = var.env_vars # Use 'item' as the loop iterator iterator = item content { name = item.key # Use the key from the map as the variable name value = item.value # Use the corresponding value } } } } } } # Outputs output "cloud_run_url" { description = "Cloud Run URL Info" value = { url = google_cloud_run_service.default.status[0].url } } output "cloud_run_env_vars_map" { description = "Environment variables as key-value map (if any)" value = { for env in try(google_cloud_run_service.default.template[0].spec[0].containers[0].env, []) : env.name => env.value } }

Also create a file, for example env_vars.tfvars, with the following content:

# env_vars.tfvars env_vars = { APP = "web" ENV = "dev" }

How This Works

The dynamic "env" block tells Terraform to create multiple env { ... } blocks (one for each entry in var.env_vars) within the container's specification.

for_each = var.env_vars iterates over the items in the env_vars variable (e.g., { APP = "web", ENV = "prod" }). For each key-value pair, a new env block will be created. If var.env_vars is empty ({}), the dynamic block doesn't produce any env blocks.

iterator = item assigns the alias item to the loop variable. Without this, the default loop variable name would be env, which could be confusing since we're creating env blocks. Using a dedicated iterator name (item) clearly indicates that we're referring to the current key-value pair in the iteration.

content { ... } defines the content of each generated env block.

name = item.key - inside the content block, item.key accesses the key of the current key-value pair from the env_vars map. This key will be used as the name of the environment variable within the container (e.g., "APP", "ENV").

value = item.value - similarly, item.value accesses the value associated with the current key in the env_vars map. This value will be assigned as the value of the environment variable within the container (e.g., "web", "prod").

The output block displays the URL and environment variables of the created Cloud Run service, while safely handling the case when no environment variables are defined:

  • google_cloud_run_service.default.status[0].url extracts the URL of the deployed Cloud Run service.
  • google_cloud_run_service.default.template[0].spec[0].containers[0].env is the path to the env list for the first (and only) container in our Cloud Run resource.
  • The try( ... ) function prevents errors by safely falling back to [] if no env blocks are created (when env_vars is empty).

How to Apply This Configuration:

To create a Cloud Run instance without any environment variables simply run terraform apply. Since env_vars is empty ({}) by default, the dynamic block will not generate any env blocks.

If env blocks are required, provide env_vars values on the command line, for example:

terraform apply -var='env_vars={APP="web",ENV="prod"}'

This overrides the default env_vars value using the CLI input and Terraform will generate env blocks for each key-value pair.

Alternatively, supply env_vars values in a .tfvars file:

terraform apply -var-file="env_vars.tfvars"

The custom variable file is loaded using the -var-file parameter. The default value from main.tf is overridden by the contents of env_vars.tfvars and Terraform will create env blocks for each key-value pair.

The dynamic block handles all these scenarios cleanly with no need for conditional logic.

Cleaning Up:

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

Summary:

As shown in this example, Terraform's dynamic block combined with for_each provides a flexible way to define environment variables for a Google Cloud Run service. It allows your configuration to adapt automatically based on variable input, without hardcoding individual env blocks.

We also demonstrated how to use try( ... ) to safely access the parameters of the generated resource through the structured indexing path (e.g., .template[0].spec[0].containers[0].env).

Such code patterns are ideal for writing reusable, scalable infrastructure configurations that behave predictably regardless of input complexity.

Back to Top

Example 2: GCE VM with Dynamic Disk Attachment

Let's review a Terraform configuration example that provisions a Google Compute Engine (GCE) instance with two attached persistent disks using a dynamic block.

Warning:

This configuration creates Google Cloud resources that may incur charges:

  • Compute Engine VM instance: Billed hourly based on uptime, regardless of usage.
  • Persistent disks: Billed by provisioned capacity, even if the disks are not actively used.
  • Network egress: If the VM sends traffic to the internet or other regions, standard GCP data egress charges apply.

Be sure to delete the resources (terraform destroy) when they are no longer needed to avoid ongoing charges.

Terraform Code:

# main.tf # Provider Configuration provider "google" { project = var.project_id } # Variables variable "project_id" { description = "Your GCP project ID" type = string } variable "zone" { description = "GCP zone" type = string default = "us-central1-a" } variable "instance_name" { description = "Name of the compute instance" type = string default = "example-instance" } variable "attached_disks" { description = "List of additional persistent disks to attach" type = list(object({ name = string size = number type = string })) default = [ { name = "data-disk-1" size = 10 type = "pd-standard" }, { name = "data-disk-2" size = 5 type = "pd-balanced" } ] } # Provision Additional Disks resource "google_compute_disk" "extra_disks" { for_each = { for disk in var.attached_disks : disk.name => disk } name = "${var.instance_name}-${each.value.name}" size = each.value.size type = each.value.type zone = var.zone } # Provision Compute Instance with Attached Disks resource "google_compute_instance" "vm" { name = var.instance_name machine_type = "f1-micro" zone = var.zone boot_disk { initialize_params { image = "debian-cloud/debian-12" } } # Using "default" VPC network_interface { network = "default" access_config {} # Provision Ephemeral Public IP } # Attach Additional Disks dynamic "attached_disk" { for_each = google_compute_disk.extra_disks iterator = disk content { source = disk.value.self_link } } } # Output: Instance and Disk Names output "instance_and_attached_disks" { description = "The VM instance name and the names of attached disks (excluding the boot disk)." value = { instance_name = google_compute_instance.vm.name attached_disks = [ for d in google_compute_instance.vm.attached_disk : d.device_name ] } }

How This Works

The attached_disks variable defines a list of disk resources (excluding the boot disk), each with the following parameters:

  • name: the unique name of the disk
  • size: the size in GB
  • type: the disk type (e.g., pd-standard, pd-balanced)

You can modify this list to add or remove disks without changing the rest of the code.

The google_compute_disk block uses for_each to loop over the list of disks and create corresponding named resources.

for disk in var.attached_disks : disk.name => disk converts a list of disk objects (from the attached_disks variable) into a map where:

  • key = disk.name (disk names must be unique)
  • value = disk - the corresponding full disk object (including name, size, and type) from the list.

name = "${var.instance_name}-${each.value.name}" forms the target disk names by adding the instance name as a prefix.

The dynamic "attached_disk" block uses for_each to iterate over google_compute_disk.extra_disks and attach all the disks created earlier.

output "instance_and_attached_disks" displays the instance name and the attached disk device names (excluding the boot disk).

How to Apply This Configuration:

Initialize Terraform:

terraform init

Apply the configuration, providing your GCP project ID:

terraform apply -var="project_id=YOUR_PROJECT_ID"

Expected Result:

  • One Compute Engine VM is created with a boot disk.
  • Two additional persistent disks are provisioned and attached to the VM.
  • Terraform displays:
Outputs: instance_and_attached_disks = { "attached_disks" = [ "persistent-disk-1", "persistent-disk-2", ] "instance_name" = "example-instance" }

Cleaning Up:

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

Summary:

This example shows how to dynamically attach multiple disks to a Google Compute Engine instance using Terraform's dynamic block and for_each. The configuration allows you to change the number or type of attached disks by simply modifying the input variable.

This approach simplifies the code and improves its maintainability, particularly when disk configuration is environment-specific or frequently updated.

Back to Top

Conclusion

In this tutorial, we explored two practical examples of using Terraform's dynamic blocks to programmatically generate nested resource blocks:

  • A Cloud Run service configuration that dynamically injects environment variables based on a map input (env_vars), without hardcoding each variable.
  • A Compute Engine VM code that dynamically attaches one or more persistent disks using a list of disk parameters.

These examples demonstrate how dynamic blocks can help write cleaner, more modular, and DRY (Don't Repeat Yourself) Terraform code by iterating over variable input and rendering nested blocks accordingly.

By using dynamic blocks, you can significantly improve the maintainability and adaptability of your Terraform codebase, especially when dealing with variable infrastructure patterns across environments or deployments.

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