Advanced HCL: Dynamic Blocks and Lifecycle Preconditions
Scenario
You are writing a complex `.tf` file to rapidly spin up testing Compute Instances with specific open firewall ports. Instead of hardcoding 10 different `allow {}` blocks inside a `google_compute_firewall`, you want the user to pass a list of ports via a variable, and Terraform will generate the configurations programmatically using Dynamic Blocks. Additionally, you want to use a Lifecycle Precondition to explicitly halt execution if the user attempts to choose an invalid machine type.
- Declare a list variable open_ports defaulting to `["80", "443", "8080"]`.
- In a google_compute_firewall, use a dynamic "allow" { for_each = var.open_ports ... } structure to generate the nested blocks automatically.
- Deploy a google_compute_instance. Inside its lifecycle block, write a precondition evaluating the machine_type against a regex or custom logic to ensure it is part of the `e2` family.
Additional Context
Dynamic Blocks vs for_each: The meta-argument `for_each` creates entirely separate resources (e.g. 3 distinct `google_compute_firewall` resources). `dynamic` blocks create repeated nested configuration blocks inside a single resource.
Preconditions vs Variable Validation: Variable validation{} blocks can only look at the variable itself. A lifecycle { precondition {} } block can evaluate properties of other resources, local variables, or expressions available only at execution time.
variables.tf
variable "project_id" { type = string }
variable "open_ports" {
type = list(string)
default = ["22", "80", "443", "3000"]
description = "A list of TCP ports to punch through the firewall"
}
variable "vm_size" {
type = string
default = "e2-micro"
}main.tf
provider "google" {
project = var.project_id
}
# 1. Advanced HCL: Dynamic Nested Blocks
resource "google_compute_firewall" "dynamic_rules" {
name = "web-app-dynamic-fw"
network = "default"
# Iterates over the list, creating one of these 'allow' blocks for each port
dynamic "allow" {
for_each = var.open_ports
# The iterator defaults to the name of the dynamic block ("allow")
content {
protocol = "tcp"
# We reference the current item in the iteration via the iterator's .value property
ports = [allow.value]
}
}
source_ranges = ["0.0.0.0/0"]
}
# 2. Advanced HCL: Lifecycle Preconditions
resource "google_compute_instance" "app_node" {
name = "testing-node"
machine_type = var.vm_size
zone = "us-central1-a"
boot_disk {
initialize_params { image = "debian-cloud/debian-11" }
}
network_interface { network = "default" }
lifecycle {
# Preconditions evaluate during the `terraform plan` phase
precondition {
# Custom logic: ensure the machine string starts with "e2-"
condition = substr(self.machine_type, 0, 3) == "e2-"
error_message = "Only cheap e2 family instances are allowed in this testing lab!"
}
}
}Workflow Commands
terraform init
terraform apply -var="project_id=my-gcp-project-123"
# Look at the execution plan or state. Notice how the single firewall resource
# generated 4 distinct `allow {}` blocks internally.
# Test the Precondition Failure:
terraform apply -var="project_id=my-gcp-project-123" -var="vm_size=n1-standard-1"
# Output -> Error: Resource precondition failed: Only cheap e2 family instances...
terraform destroy -var="project_id=my-gcp-project-123"
