Find the Best Cosmetic Hospitals

Explore trusted cosmetic hospitals and make a confident choice for your transformation.

“Invest in yourself — your confidence is always worth it.”

Explore Cosmetic Hospitals

Start your journey today — compare options in one place.

Terraform Loops and Iteration: Complete Guide to count, for_each, for Expressions, and Dynamic Blocks

Terraform Loops and Iteration — Summary Table

#Option / MethodPurposeInputCommon use caseCreates resources?
1countRepeats a resource or module a specified number of timesNumberCreate 3 EC2 instances or Azure VMsYes, when used on a resource/module
2for_eachRepeats resources or modules using named keysMap or set of stringsCreate VMs for dev, test, and prodYes, when used on a resource/module
3for expression — listTransforms elements into a new listList, tuple, or setConvert environment names to uppercaseNo
4for expression — mapTransforms elements into a map/objectCollectionBuild a map of application names and settingsNo
5for expression with ifFilters elements based on a conditionCollectionSelect only enabled applicationsNo, but the result can drive resource creation
6Nested for expressionsIterates over nested collectionsNested lists, maps, or objectsGenerate application/environment combinationsNo, but the result can drive resource creation
7dynamic blockGenerates repeated nested configuration blocksCollection via for_eachGenerate AWS security group ingress rulesNo new top-level resources
8flatten() with iterationConverts nested lists into a single listNested listsFlatten application/environment combinationsNo
9setproduct() with iterationGenerates combinations from collectionsTwo or more collectionsCreate resources for every app/environment combinationNo, but the result can drive resource creation
10toset() with for_eachConverts a list to a set of unique stringsListCreate one resource per unique application nameYes, through for_each
11keys() / values()Extracts map keys or valuesMapPrepare data for iteration or outputsNo
12Module countRepeats module instances numericallyNumberOptionally deploy a moduleYes, creates module instances
13Module for_eachRepeats module instances by keyMap or set of stringsDeploy a module for each applicationYes, creates module instances

Complete Guide to count, for_each, for Expressions, and Dynamic Blocks

Audience: Beginners through advanced DevOps / cloud engineers
Cloud examples: AWS EC2 and Azure Linux Virtual Machines
Terraform version: Terraform 1.3+ recommended
Approach: Each hands-on cloud lab is a standalone main.tf, with run steps, expected outcomes, and cleanup.

Cost and security: Cloud labs create billable resources. Review plans before applying and destroy resources when finished. SSH rules should be limited to your own public IPv4 address using CIDR notation such as 203.0.113.10/32. Do not open SSH to 0.0.0.0/0 in production. Never put cloud credentials or private keys in Terraform files.


Contents

  1. Learning goals
  2. Before you start
  3. The five main iteration patterns
  4. Lab 1 — count: create multiple instances
  5. Lab 2 — for_each: create named instances
  6. Lab 3 — for expressions: transform collections
  7. Lab 4 — Filtering with for ... if
  8. Lab 5 — dynamic blocks
  9. Lab 6 — Nested iteration and flatten
  10. Lab 7 — setproduct for combinations
  11. Lab 8 — Iterating over objects and maps
  12. Lab 9 — Conditional iteration
  13. Lab 10 — Iterating over modules
  14. count vs for_each
  15. Common errors and troubleshooting
  16. Best practices
  17. Quick reference
  18. Practice exercises

1. Learning goals

After this guide, you should be able to:

  • Repeat resources with count or for_each.
  • Transform a list, map, or object with a for expression.
  • Filter collections with an if clause.
  • Generate repeated nested blocks with dynamic.
  • Flatten nested collections and generate combinations.
  • Repeat modules with count or for_each.
  • Understand resource addresses, plan-time requirements, and common iteration errors.
  • Run and clean up practical AWS and Azure labs.

2. Before you start

Install Terraform and the relevant CLI.

AWS

Configure credentials using an AWS profile, environment variables, or an IAM role. Confirm the identity:

terraform version
aws sts get-caller-identity
Code language: JavaScript (javascript)

You need permissions to create and delete EC2 instances and security groups. Use a valid Ubuntu AMI ID from the same AWS region as the provider.

Azure

Sign in and select the right subscription:

terraform version
az login
az account show --output table
az account set --subscription "<SUBSCRIPTION_ID_OR_NAME>"
Code language: HTML, XML (xml)

You need permissions to create and delete resource groups, networking, public IPs, and Linux VMs. Regional quota, policy, and VM-size availability can prevent creation.

General workflow

For each lab, use a separate empty directory and save that lab’s code as main.tf.

terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform output
terraform destroy

Terraform prompts before applying or destroying unless you pass an automatic approval flag. For learning, review the plan and type the confirmation yourself.

Azure SSH key: Most Azure labs expect the contents of a public key, for example:

cat ~/.ssh/id_ed25519.pub
Code language: JavaScript (javascript)

If no key exists, create one with ssh-keygen -t ed25519. Pass the public key to Terraform, never the private key.


3. The five main iteration patterns


Pattern Main purpose Typical use


count Create a numeric number Three identical test of resource/module instances, or one instances optional instance

for_each Create instances from a One VM per named map or set of strings application

for expression Transform a collection Convert names to into another collection uppercase or build a map

for ... if Filter a collection Keep only enabled applications

dynamic block Generate repeated AWS security-group nested blocks inside ingress rules one resource

Supporting tools include flatten(), toset(), keys(), values(), zipmap(), and setproduct(). These are functions that help prepare collections for iteration.


4. Lab 1 — count: create multiple instances

How count works

count accepts a non-negative integer. Terraform creates that many instances, and each instance has a numeric index:

  • count.index starts at 0.
  • With count = 3, the addresses are resource.example[0], [1], and [2].
  • resource.example[*].id collects IDs from all counted instances.

Use count when instances are interchangeable or when a resource should exist only zero or one times. If removing an item from a list shifts indexes, Terraform can plan unexpected replacements.

4A. AWS — create three EC2 instances

Save as main.tf:

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "instance_count" {
  type    = number
  default = 3

  validation {
    condition     = var.instance_count >= 0 && var.instance_count <= 5
    error_message = "instance_count must be between 0 and 5 for this lab."
  }
}

variable "ami_id" {
  type        = string
  description = "Valid Ubuntu AMI ID in the selected AWS region."
}

variable "ssh_cidr" {
  type        = string
  description = "Your public IPv4 address in CIDR notation, e.g. 203.0.113.10/32."
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-count-lab-"
  description = "SSH restricted to the student's public IP"

  ingress {
    description = "SSH from student IP only"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "tf-count-lab-sg"
  }
}

resource "aws_instance" "web" {
  count                  = var.instance_count
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name  = "count-web-${count.index + 1}"
    Index = tostring(count.index)
  }
}

output "instance_ids" {
  value = aws_instance.web[*].id
}

output "instance_public_ips" {
  value = aws_instance.web[*].public_ip
}

output "created_count" {
  value = length(aws_instance.web)
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
Code language: JavaScript (javascript)

To test zero instances, use -var="instance_count=0" with plan and then apply. The security group remains because it is not counted. Destroy the lab when finished, using the same required variables.

Expected: The default creates three EC2 instances. Output lists their IDs and public IPs. AMI IDs are regional; get a current Ubuntu AMI for the configured region before applying.

4B. Azure — create three Linux VMs

This example includes a resource group, VNet, subnet, public IPs, NSG, NICs, NSG association, and Linux VMs.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "vm_count" {
  type    = number
  default = 3

  validation {
    condition     = var.vm_count >= 0 && var.vm_count <= 5
    error_message = "vm_count must be between 0 and 5 for this lab."
  }
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type        = string
  description = "Contents of your SSH public key."
}

variable "ssh_cidr" {
  type        = string
  description = "Your public IPv4 address in CIDR notation, e.g. 203.0.113.10/32."
}

locals {
  prefix = "tfcount"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.100.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.100.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "vm" {
  count               = var.vm_count
  name                = "${local.prefix}-pip-${count.index + 1}"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "vm" {
  count               = var.vm_count
  name                = "${local.prefix}-nic-${count.index + 1}"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.vm[count.index].id
  }
}

resource "azurerm_network_interface_security_group_association" "vm" {
  count                     = var.vm_count
  network_interface_id      = azurerm_network_interface.vm[count.index].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "vm" {
  count               = var.vm_count
  name                = "${local.prefix}-vm-${count.index + 1}"
  computer_name       = "${local.prefix}-${count.index + 1}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.vm[count.index].id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Lab   = "count"
    Index = tostring(count.index)
  }
}

output "vm_ids" {
  value = azurerm_linux_virtual_machine.vm[*].id
}

output "public_ips" {
  value = azurerm_public_ip.vm[*].ip_address
}

output "created_count" {
  value = length(azurerm_linux_virtual_machine.vm)
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
Code language: JavaScript (javascript)

Try -var="vm_count=0" to see the VMs, NICs, and public IPs removed while the shared network remains. Use terraform destroy when finished. Verify VM size availability and quota for your subscription and region.


5. Lab 2 — for_each: create named instances

How for_each works

for_each accepts a map or a set of strings. Terraform creates one instance for every key:

  • Map: each.key is the map key; each.value is the associated value.
  • Set of strings: each.key and each.value are the current string.
  • Addresses use stable keys, for example aws_instance.app["frontend"].

This is usually the better choice when each resource has a meaningful identity, such as an application, environment, or subnet.

5A. AWS — create one EC2 instance per application

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    instance_type = string
    environment   = string
  }))
  default = {
    frontend = { instance_type = "t3.micro", environment = "dev" }
    backend  = { instance_type = "t3.micro", environment = "test" }
    worker   = { instance_type = "t3.micro", environment = "dev" }
  }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-foreach-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "app" {
  for_each               = var.applications
  ami                    = var.ami_id
  instance_type          = each.value.instance_type
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = "app-${each.key}"
    Application = each.key
    Environment = each.value.environment
  }
}

output "instance_ids_by_application" {
  value = { for name, instance in aws_instance.app : name => instance.id }
}

output "public_ips_by_application" {
  value = { for name, instance in aws_instance.app : name => instance.public_ip }
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform destroy \
  -var="ami_id=ami-REPLACE_WITH_VALID_UBUNTU_AMI" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

Expected: Three EC2 instances with keys frontend, backend, and worker. The output maps application names to IDs and IPs.

5B. Azure — create one Linux VM per application

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    vm_size     = string
    environment = string
  }))
  default = {
    frontend = { vm_size = "Standard_B1s", environment = "dev" }
    backend  = { vm_size = "Standard_B1s", environment = "test" }
    worker   = { vm_size = "Standard_B1s", environment = "dev" }
  }
}

locals {
  prefix = "tfforeach"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.110.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.110.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "app" {
  for_each            = var.applications
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "app" {
  for_each            = var.applications
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.app[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "app" {
  for_each                  = var.applications
  network_interface_id      = azurerm_network_interface.app[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "app" {
  for_each            = var.applications
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = each.value.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.app[each.key].id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application = each.key
    Environment = each.value.environment
  }
}

output "vm_ids_by_application" {
  value = { for name, vm in azurerm_linux_virtual_machine.app : name => vm.id }
}

output "public_ips_by_application" {
  value = { for name, pip in azurerm_public_ip.app : name => pip.ip_address }
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform destroy \
  -var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
  -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

Expected: Three VMs, each with its own public IP, NIC, and stable key. Azure resource names have restrictions; keep names short and use supported characters.


6. Lab 3 — for expressions: transform collections

How for expressions work

A for expression reads items from a collection and builds a new list/tuple or map/object.

List result:

[for name in var.names : upper(name)]
Code language: JSON / JSON with Comments (json)

Map result:

{ for name in var.names : name => upper(name) }
Code language: JavaScript (javascript)

The for expression itself does not create cloud resources. It creates data that can be used by resources, outputs, or locals.

6A. AWS — build a map of instance metadata

This is a complete EC2 example that uses a for expression to prepare names and output the resources as a map.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "names" {
  type    = list(string)
  default = ["frontend", "backend", "worker"]
}

locals {
  normalized_names = [for name in var.names : lower(trimspace(name))]
  name_to_tag      = { for name in local.normalized_names : name => upper(name) }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-for-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "app" {
  for_each               = toset(local.normalized_names)
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name          = "for-${each.key}"
    NormalizedTag = local.name_to_tag[each.key]
  }
}

output "normalized_names" {
  value = local.normalized_names
}

output "uppercase_map" {
  value = local.name_to_tag
}

output "instance_ids" {
  value = { for name, instance in aws_instance.app : name => instance.id }
}
Code language: JavaScript (javascript)

Run with the standard AWS workflow and supply ami_id and ssh_cidr. Change the names list to experiment. Since this lab uses a set for for_each, duplicate names collapse to one key. Keep names unique to avoid confusing results.

6B. Azure — transform names and create VMs from the resulting collection

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "names" {
  type    = list(string)
  default = ["frontend", "backend", "worker"]
}

locals {
  normalized_names = [for name in var.names : lower(trimspace(name))]
  name_to_tag      = { for name in local.normalized_names : name => upper(name) }
  prefix           = "tffor"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.120.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.120.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "app" {
  for_each            = toset(local.normalized_names)
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "app" {
  for_each            = toset(local.normalized_names)
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.app[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "app" {
  for_each                  = toset(local.normalized_names)
  network_interface_id      = azurerm_network_interface.app[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "app" {
  for_each            = toset(local.normalized_names)
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [
    azurerm_network_interface.app[each.key].id
  ]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application   = each.key
    NormalizedTag = local.name_to_tag[each.key]
  }
}

output "normalized_names" {
  value = local.normalized_names
}

output "uppercase_map" {
  value = local.name_to_tag
}

output "public_ips" {
  value = { for name, pip in azurerm_public_ip.app : name => pip.ip_address }
}
Code language: JavaScript (javascript)

Run with the standard Azure workflow and SSH inputs. The for expressions prepare the normalized list and map; for_each then creates one VM per unique normalized name.


7. Lab 4 — Filtering with for ... if

Concept

The if at the end of a for expression filters elements. Only items for which the condition is true appear in the resulting collection.

Example:

locals {
  enabled_apps = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}
Code language: JavaScript (javascript)

7A. AWS — create instances only for enabled applications

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    enabled      = bool
    instance_type = string
  }))
  default = {
    frontend = { enabled = true,  instance_type = "t3.micro" }
    backend  = { enabled = true,  instance_type = "t3.micro" }
    sandbox  = { enabled = false, instance_type = "t3.micro" }
  }
}

locals {
  enabled_apps = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-filter-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "app" {
  for_each               = local.enabled_apps
  ami                    = var.ami_id
  instance_type          = each.value.instance_type
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = "filter-${each.key}"
    Application = each.key
  }
}

output "enabled_app_names" {
  value = keys(local.enabled_apps)
}

output "instance_ids" {
  value = { for name, instance in aws_instance.app : name => instance.id }
}
Code language: JavaScript (javascript)

Run with the standard AWS workflow. The default creates frontend and backend; sandbox is filtered out. To enable it, change enabled = false to true in the variable default or provide a complete replacement map in a terraform.tfvars file.

7B. Azure — create VMs only for enabled applications

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(object({
    enabled = bool
    vm_size = string
  }))
  default = {
    frontend = { enabled = true,  vm_size = "Standard_B1s" }
    backend  = { enabled = true,  vm_size = "Standard_B1s" }
    sandbox  = { enabled = false, vm_size = "Standard_B1s" }
  }
}

locals {
  prefix = "tffilter"
  enabled_apps = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.130.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.130.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "app" {
  for_each            = local.enabled_apps
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "app" {
  for_each            = local.enabled_apps
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.app[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "app" {
  for_each                  = local.enabled_apps
  network_interface_id      = azurerm_network_interface.app[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "app" {
  for_each            = local.enabled_apps
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = each.value.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.app[each.key].id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application = each.key
  }
}

output "enabled_app_names" {
  value = keys(local.enabled_apps)
}

output "public_ips" {
  value = { for name, pip in azurerm_public_ip.app : name => pip.ip_address }
}
Code language: JavaScript (javascript)

Run with the Azure workflow and SSH inputs. By default, only frontend and backend are created. Enabling sandbox creates its public IP, NIC, and VM.


8. Lab 5 — dynamic blocks

Concept

A dynamic block generates nested blocks within a resource. It is not the same as for_each on a resource: – for_each on a resource creates multiple top-level resources. – dynamic creates multiple nested blocks inside one resource.

8A. AWS — dynamic security group ingress rules

This example creates one security group and generates one ingress block per rule. SSH is restricted to your CIDR. HTTP is an example rule restricted to a private network range.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ssh_cidr" {
  type = string
}

variable "enable_http_rule" {
  type    = bool
  default = true
}

locals {
  fixed_rules = [
    {
      name = "ssh"
      port = 22
      cidr = var.ssh_cidr
    }
  ]

  optional_rules = var.enable_http_rule ? [
    {
      name = "http-private"
      port = 80
      cidr = "10.0.0.0/8"
    }
  ] : []

  ingress_rules = concat(local.fixed_rules, local.optional_rules)
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-dynamic-lab-"
  description = "Dynamic ingress rule demonstration"

  dynamic "ingress" {
    for_each = local.ingress_rules
    iterator = rule

    content {
      description = rule.value.name
      from_port   = rule.value.port
      to_port     = rule.value.port
      protocol    = "tcp"
      cidr_blocks = [rule.value.cidr]
    }
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "tf-dynamic-lab-sg"
  }
}

output "configured_rules" {
  value = local.ingress_rules
}

output "security_group_id" {
  value = aws_security_group.lab.id
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform apply -var="ssh_cidr=YOUR_PUBLIC_IP/32"
terraform output
terraform plan -var="ssh_cidr=YOUR_PUBLIC_IP/32" -var="enable_http_rule=false"
terraform destroy -var="ssh_cidr=YOUR_PUBLIC_IP/32"
Code language: JavaScript (javascript)

This lab creates a security group only, so it does not create an EC2 instance. The HTTP rule is limited to 10.0.0.0/8 as an example and will not necessarily be reachable from your laptop.

8B. Azure — iterate NSG rules with for_each

Azure NSG rules are normally represented as separate azurerm_network_security_rule resources, so use for_each instead of a dynamic block for this provider pattern.

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "ssh_cidr" {
  type = string
}

variable "enable_http_rule" {
  type    = bool
  default = true
}

locals {
  rules = merge(
    {
      ssh = {
        priority = 100
        port     = "22"
        source   = var.ssh_cidr
      }
    },
    var.enable_http_rule ? {
      http = {
        priority = 110
        port     = "80"
        source   = "10.0.0.0/8"
      }
    } : {}
  )
}

resource "azurerm_resource_group" "lab" {
  name     = "tfdynamic-rg"
  location = var.location
}

resource "azurerm_network_security_group" "lab" {
  name                = "tfdynamic-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_network_security_rule" "rule" {
  for_each                    = local.rules
  name                        = "Allow-${each.key}"
  priority                    = each.value.priority
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_port_range           = "*"
  destination_port_range      = each.value.port
  source_address_prefix       = each.value.source
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.lab.name
  network_security_group_name = azurerm_network_security_group.lab.name
}

output "created_rule_names" {
  value = keys(azurerm_network_security_rule.rule)
}
Code language: JavaScript (javascript)

Run terraform init, terraform validate, and terraform plan -var="ssh_cidr=YOUR_PUBLIC_IP/32". Set -var="enable_http_rule=false" to omit the HTTP rule. Destroy the lab when finished. This lab focuses on iteration and NSG rules; the next labs demonstrate complete VM networking.


9. Lab 6 — Nested iteration and flatten

Concept

When input data is nested, a for expression can create a nested list and flatten() can turn it into one list. The flattened list can then be converted to a map for for_each.

Example input:

variable "applications" {
  default = {
    frontend = ["dev", "test"]
    backend  = ["dev", "prod"]
  }
}
Code language: JavaScript (javascript)

Desired combinations: frontend-dev, frontend-test, backend-dev, backend-prod.

9A. AWS — one EC2 instance for each application/environment pair

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(list(string))
  default = {
    frontend = ["dev", "test"]
    backend  = ["dev", "prod"]
  }
}

locals {
  pairs_nested = [
    for app_name, environments in var.applications : [
      for environment in environments : {
        key         = "${app_name}-${environment}"
        app         = app_name
        environment = environment
      }
    ]
  ]

  pairs = flatten(local.pairs_nested)
  instances = {
    for item in local.pairs : item.key => item
  }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-flatten-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "pair" {
  for_each               = local.instances
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = each.key
    Application = each.value.app
    Environment = each.value.environment
  }
}

output "instance_keys" {
  value = keys(aws_instance.pair)
}

output "instance_ids" {
  value = { for key, instance in aws_instance.pair : key => instance.id }
}
Code language: PHP (php)

Run with the usual AWS commands and inputs. Expect four instances. The instances map provides stable, descriptive keys.

9B. Azure — one Linux VM for each application/environment pair

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type = map(list(string))
  default = {
    frontend = ["dev", "test"]
    backend  = ["dev", "prod"]
  }
}

locals {
  prefix = "tfflatten"
  pairs_nested = [
    for app_name, environments in var.applications : [
      for environment in environments : {
        key         = "${app_name}-${environment}"
        app         = app_name
        environment = environment
      }
    ]
  ]
  pairs = flatten(local.pairs_nested)
  instances = {
    for item in local.pairs : item.key => item
  }
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.140.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.140.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.pair[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "pair" {
  for_each                  = local.instances
  network_interface_id      = azurerm_network_interface.pair[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.pair[each.key].id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application = each.value.app
    Environment = each.value.environment
  }
}

output "instance_keys" {
  value = keys(azurerm_linux_virtual_machine.pair)
}

output "public_ips" {
  value = { for key, pip in azurerm_public_ip.pair : key => pip.ip_address }
}
Code language: PHP (php)

Run with the Azure workflow and SSH inputs. Expect four VMs. If you change the nested lists, inspect the plan carefully: removed keys cause the corresponding VMs and network resources to be destroyed.


10. Lab 7 — setproduct for combinations

Concept

setproduct() returns combinations from two or more collections. It is useful for combinations such as each application across each environment. Only use it when all combinations are truly desired.

7A. AWS — create EC2 instances across app and environment combinations

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type    = set(string)
  default = ["frontend", "backend"]
}

variable "environments" {
  type    = set(string)
  default = ["dev", "test"]
}

locals {
  combinations = [
    for pair in setproduct(var.applications, var.environments) : {
      app         = pair[0]
      environment = pair[1]
      key         = "${pair[0]}-${pair[1]}"
    }
  ]

  instances = { for item in local.combinations : item.key => item }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-product-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "pair" {
  for_each               = local.instances
  ami                    = var.ami_id
  instance_type          = "t3.micro"
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = each.key
    Application = each.value.app
    Environment = each.value.environment
  }
}

output "combination_keys" {
  value = keys(aws_instance.pair)
}
Code language: PHP (php)

Run with AWS inputs. The defaults create four EC2 instances: frontend-dev, frontend-test, backend-dev, and backend-test.

7B. Azure — create Linux VMs across app and environment combinations

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "applications" {
  type    = set(string)
  default = ["frontend", "backend"]
}

variable "environments" {
  type    = set(string)
  default = ["dev", "test"]
}

locals {
  prefix = "tfproduct"
  combinations = [
    for pair in setproduct(var.applications, var.environments) : {
      app         = pair[0]
      environment = pair[1]
      key         = "${pair[0]}-${pair[1]}"
    }
  ]
  instances = { for item in local.combinations : item.key => item }
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.150.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.150.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.pair[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "pair" {
  for_each                  = local.instances
  network_interface_id      = azurerm_network_interface.pair[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "pair" {
  for_each            = local.instances
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = "Standard_B1s"
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.pair[each.key].id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Application = each.value.app
    Environment = each.value.environment
  }
}

output "combination_keys" {
  value = keys(azurerm_linux_virtual_machine.pair)
}

output "public_ips" {
  value = { for key, pip in azurerm_public_ip.pair : key => pip.ip_address }
}
Code language: PHP (php)

Run with Azure inputs. Expect four VMs. Before scaling this pattern up, calculate the number of combinations: the total is the product of the collection sizes.


11. Lab 8 — Iterating over objects and maps

Concept

A map of objects lets each resource have its own settings. For example, each VM may have a name, size, and environment tag. each.value accesses that object’s attributes.

8A. AWS — per-instance configuration

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = var.aws_region
}

variable "aws_region" {
  type    = string
  default = "ap-northeast-1"
}

variable "ami_id" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "servers" {
  type = map(object({
    instance_type = string
    owner         = string
    environment   = string
  }))
  default = {
    api = {
      instance_type = "t3.micro"
      owner         = "platform"
      environment   = "dev"
    }
    batch = {
      instance_type = "t3.micro"
      owner         = "data"
      environment   = "test"
    }
  }
}

resource "aws_security_group" "lab" {
  name_prefix = "tf-object-lab-"

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "server" {
  for_each               = var.servers
  ami                    = var.ami_id
  instance_type          = each.value.instance_type
  vpc_security_group_ids = [aws_security_group.lab.id]

  tags = {
    Name        = each.key
    Owner       = each.value.owner
    Environment = each.value.environment
  }
}

output "server_configuration" {
  value = {
    for name, instance in aws_instance.server :
    name => {
      id        = instance.id
      public_ip = instance.public_ip
    }
  }
}
Code language: JavaScript (javascript)

Run with AWS inputs. Inspect the tags in the EC2 console and terraform output.

8B. Azure — per-VM configuration

terraform {
  required_version = ">= 1.3.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

provider "azurerm" {
  features {}
  resource_provider_registrations = "none"
}

variable "location" {
  type    = string
  default = "Japan East"
}

variable "admin_username" {
  type    = string
  default = "azureuser"
}

variable "ssh_public_key" {
  type = string
}

variable "ssh_cidr" {
  type = string
}

variable "servers" {
  type = map(object({
    vm_size     = string
    owner       = string
    environment = string
  }))
  default = {
    api = {
      vm_size     = "Standard_B1s"
      owner       = "platform"
      environment = "dev"
    }
    batch = {
      vm_size     = "Standard_B1s"
      owner       = "data"
      environment = "test"
    }
  }
}

locals {
  prefix = "tfobject"
}

resource "azurerm_resource_group" "lab" {
  name     = "${local.prefix}-rg"
  location = var.location
}

resource "azurerm_virtual_network" "lab" {
  name                = "${local.prefix}-vnet"
  address_space       = ["10.160.0.0/16"]
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
}

resource "azurerm_subnet" "lab" {
  name                 = "default"
  resource_group_name  = azurerm_resource_group.lab.name
  virtual_network_name = azurerm_virtual_network.lab.name
  address_prefixes     = ["10.160.1.0/24"]
}

resource "azurerm_network_security_group" "lab" {
  name                = "${local.prefix}-nsg"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  security_rule {
    name                       = "AllowSSHFromStudent"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = var.ssh_cidr
    destination_address_prefix = "*"
  }
}

resource "azurerm_public_ip" "server" {
  for_each            = var.servers
  name                = "${local.prefix}-${each.key}-pip"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  allocation_method   = "Static"
  sku                 = "Standard"
}

resource "azurerm_network_interface" "server" {
  for_each            = var.servers
  name                = "${local.prefix}-${each.key}-nic"
  location            = azurerm_resource_group.lab.location
  resource_group_name = azurerm_resource_group.lab.name
  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.lab.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.server[each.key].id
  }
}

resource "azurerm_network_interface_security_group_association" "server" {
  for_each                  = var.servers
  network_interface_id      = azurerm_network_interface.server[each.key].id
  network_security_group_id = azurerm_network_security_group.lab.id
}

resource "azurerm_linux_virtual_machine" "server" {
  for_each            = var.servers
  name                = "${local.prefix}-${each.key}-vm"
  computer_name       = "${local.prefix}-${each.key}"
  resource_group_name = azurerm_resource_group.lab.name
  location            = azurerm_resource_group.lab.location
  size                = each.value.vm_size
  admin_username      = var.admin_username
  network_interface_ids = [azurerm_network_interface.server[each.key].id]
  disable_password_authentication = true

  admin_ssh_key {
    username   = var.admin_username
    public_key = var.ssh_public_key
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  tags = {
    Owner       = each.value.owner
    Environment = each.value.environment
  }
}

output "server_configuration" {
  value = {
    for name, vm in azurerm_linux_virtual_machine.server :
    name => {
      id        = vm.id
      public_ip = azurerm_public_ip.server[name].ip_address
    }
  }
}
Code language: JavaScript (javascript)

Run with Azure SSH inputs. Each object drives the size and tags for the corresponding VM.


12. Lab 9 — Conditional iteration

Concept

Iteration and conditional logic can be combined. For example, use a map of services and include only those with enabled = true. This is useful for optional environments, apps, and infrastructure components.

9A. AWS — enabled instances and per-app types

Use the AWS filtering example from Lab 4 as the complete lab. Its key logic is:

locals {
  enabled_apps = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "aws_instance" "app" {
  for_each      = local.enabled_apps
  ami           = var.ami_id
  instance_type = each.value.instance_type
  # Include the security group and tags from Lab 4.
}
Code language: PHP (php)

The full standalone AWS configuration is in Lab 4A. Copy that entire example into its own directory to run it.

9B. Azure — enabled VMs and related network resources

Use the Azure filtering example from Lab 4 as the complete lab. Its key logic is:

locals {
  enabled_apps = {
    for name, app in var.applications : name => app
    if app.enabled
  }
}

resource "azurerm_linux_virtual_machine" "app" {
  for_each = local.enabled_apps
  # The full VM, NIC, public IP, and NSG configuration is in Lab 4B.
}
Code language: PHP (php)

The full standalone Azure configuration is in Lab 4B. Copy that entire example into its own directory. The public IPs, NICs, associations, and VMs all use the same filtered map, so their keys stay aligned.


13. Lab 10 — Iterating over modules

Concept

Terraform modules can use count or for_each, just like resources. This lab uses a local module so you can experiment without cloud costs. The same pattern can be applied to a module that provisions EC2 or Azure VMs.

Create this directory structure:

module-iteration-lab/
├── main.tf
└── modules/
    └── label/
        └── main.tf
Code language: JavaScript (javascript)

Root main.tf:

terraform {
  required_version = ">= 1.3.0"
}

variable "labels" {
  type    = set(string)
  default = ["frontend", "backend", "worker"]
}

module "label" {
  source   = "./modules/label"
  for_each = var.labels

  label = each.key
}

output "uppercase_labels" {
  value = { for name, module_instance in module.label : name => module_instance.value }
}
Code language: PHP (php)

modules/label/main.tf:

variable "label" {
  type = string
}

output "value" {
  value = upper(var.label)
}
Code language: JavaScript (javascript)

Run:

terraform init
terraform fmt
terraform validate
terraform plan
terraform apply
terraform output
terraform destroy

Expected output is a map like:

{
  "backend"  = "BACKEND"
  "frontend" = "FRONTEND"
  "worker"   = "WORKER"
}
Code language: JavaScript (javascript)

No cloud resources are created in this lab.


14. count vs for_each


Question count for_each


Input type Integer Map or set of strings

Resource address [0], [1], etc. ["key"]

Best use Numeric quantity or Named, independently on/off managed resources

Stable identity Index-based Key-based

Typical risk Index changes can shift Changing a key changes identities the address

Example addresses

# count
aws_instance.web[0]

# for_each
aws_instance.app["frontend"]
Code language: CSS (css)

Prefer for_each when resources have meaningful names or different configuration. Prefer count for a simple number of interchangeable resources or a basic conditional resource.


15. Common errors and troubleshooting

Invalid for_each argument

Cause: The value is not a map or set of strings, or Terraform cannot know the keys during planning.

Fix: Use a map/set and keys known before apply. A common pattern is to use known application names as keys and put computed IDs in values.

Invalid count argument

Cause: Terraform cannot determine the number of instances during planning.

Fix: Base count on known variables or configuration. Avoid using a resource attribute that is unknown until apply.

Invalid index when count is zero

Cause: Code references resource.example[0] even though count = 0.

Fix: Guard the reference with a conditional expression or use a splat expression such as resource.example[*].id.

Duplicate keys in a for expression

Cause: A map comprehension produces the same key more than once.

Fix: Make keys unique or use grouping mode (...) if grouping values under each key is intended.

Unexpected replacement when changing count

Cause: Counted resources are indexed numerically. Removing or reordering list elements can change which object belongs to an index.

Fix: Use for_each with stable names for independently meaningful resources.

Azure VM size or quota error

Cause: The size is not available in the region, restricted by policy, or exceeds subscription quota.

Fix: Verify regional availability, subscription quota, and policy. Terraform iteration cannot increase quota.

AWS AMI not found

Cause: AMI IDs are regional or stale.

Fix: Find a current Ubuntu AMI in the provider’s configured region.

SSH does not work

Check the source CIDR, public IP, routing, NSG/security group, and private key. Supply the SSH public key to Azure. Do not expose SSH globally to solve a connectivity issue.


16. Best practices

  1. Use for_each for named infrastructure that should retain stable identities.
  2. Use count for simple quantities and on/off resources.
  3. Keep for expressions readable; use intermediate locals for complex transformations.
  4. Filter a collection before passing it to for_each.
  5. Use the same key map for related resources such as public IPs, NICs, and VMs.
  6. Use dynamic only for nested blocks; it is not a replacement for resource for_each.
  7. Check the plan after changing keys or counts because instances may be destroyed or replaced.
  8. Keep resource names unique and within cloud-provider naming restrictions.
  9. Pin provider major versions and review upgrades.
  10. Use least-privilege networking and keep secrets out of Terraform files.
  11. Keep each lab in its own directory or isolated state.
  12. Destroy lab infrastructure when finished.

17. Quick reference

Task Syntax


Repeat a resource N times count = var.n Access count index count.index Collect count outputs resource.example[*].id Repeat for each map/set item for_each = var.items Access current key/value each.key, each.value Transform a list [for x in var.items : expression] Build a map { for x in var.items : x.key => x.value } Filter a collection [for x in var.items : x if condition] Filter a map { for k, v in var.map : k => v if condition } Flatten nested lists flatten(local.nested) Convert list to set toset(var.list) Extract map keys keys(var.map) Extract map values values(var.map) Generate combinations setproduct(var.a, var.b) Generate nested blocks dynamic "block_name" { for_each = ... } Repeat module instances module "x" { for_each = ... }


18. Practice exercises

Exercise 1 — Count

Create two EC2 instances or Azure Linux VMs using count. Output all IDs.

Hint: count = 2; use a splat expression in the output.

Exercise 2 — for_each

Create resources for frontend, backend, and worker, each with a different tag.

Hint: Use a map and each.key / each.value.

Exercise 3 — Filtering

Given an object map containing enabled = true/false, create only enabled resources.

Hint:

locals {
  enabled_items = {
    for name, item in var.items : name => item
    if item.enabled
  }
}
Code language: JavaScript (javascript)

Exercise 4 — Transform a list

Convert ["dev", "test", "prod"] to uppercase.

Answer:

[for environment in var.environments : upper(environment)]
Code language: JSON / JSON with Comments (json)

Exercise 5 — Flatten

Given a map of applications to environments, create one resource per app/environment pair.

Hint: Build a nested list with nested for expressions, call flatten(), and then convert the result to a map keyed by a unique string.

Exercise 6 — Dynamic blocks

Generate multiple AWS security group ingress rules from a list of objects.

Hint: Define a list of rule objects and use dynamic "ingress" with for_each.

Exercise 7 — Module iteration

Call a local module once per item in a set of strings.

Hint: module "example" { for_each = var.items ... }.


Final checklist

  • [ ] I can explain the difference between count and for_each.
  • [ ] I can use for expressions to transform a collection.
  • [ ] I can filter a map or list with if.
  • [ ] I understand when dynamic blocks are appropriate.
  • [ ] I can flatten nested lists and generate combinations.
  • [ ] I can read resource addresses in a Terraform plan.
  • [ ] I checked all plans before applying.
  • [ ] I destroyed all temporary cloud resources.

Key idea: count and for_each repeat resource or module instances; for expressions transform collections; dynamic blocks repeat nested configuration. Choose the construct that matches the job.

Find Trusted Cardiac Hospitals

Compare heart hospitals by city and services — all in one place.

Explore Hospitals
I'm Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms. I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.

Related Posts

Terraform Conditional Expressions and Logic: Complete Guide with Examples

Terraform Conditional Expressions and Logic — Summary Table # Concept Purpose Example Creates Resources? 1 Ternary operator Choose between two values var.env == “prod” ? “t3.large” :…

Read More

Terraform workspace explained!!!

What is Terraform workspace? Have you worked with terraform workflow? such as terraform init/plan/apply/destroy with terraform configuration file? If Yes, you already have worked with Terraform workspace….

Read More

Terraform Tutorials: Templating in Terraform

Top 10 Templating Engines Across Major Programming Languages # Templating Engine Programming Language Common Framework / Ecosystem Typical Real Use 1 Jinja2 Python Flask, Ansible HTML generation,…

Read More

Terraform provisioners Tutorials and Complete Guide

Terraform provisioners are used to execute scripts or shell commands on a local or remote machine as part of resource creation/deletion. They are similar to “EC2 instance…

Read More

Terraform Tutorials: Local Values using Local Block

What is local value in terraform? In Terraform, a locals block is used to define local variables within a module, allowing you to create reusable expressions and…

Read More

Terraform Tutorials: Module Complete Guide

A Terraform module is a collection of configuration files that encapsulate resources used together to achieve a specific outcome. Modules promote reusability, organization, and maintainability in infrastructure…

Read More