One of the main selling points of Terraform modules is repeatability — but “repeatable” means two different things and picking the wrong one causes real problems. You can repeat within a module (deploying multiple of the same resource) or repeat the module itself (deploying the same module into multiple regions or environments). The approach you choose at design time shapes how flexible your module is later.
This article covers both patterns with real examples, and explains when to reach for count versus for_each.
Repeatability Within the Module
When you need multiple instances of a resource inside a module, you have two main options: count and for_each. They’re not interchangeable — each suits different situations.
Count
count is useful when you need a fixed number of identical resources. The example below deploys a configurable number of Windows domain controllers. The format function builds a zero-padded name from a prefix variable, so you get DC01, DC02, and so on.
One gotcha: count has a hard dependency on ordering. Remove the first item from a list and Terraform will try to recreate every resource after it. That’s fine for a VM you own entirely, but painful for anything stateful.
resource "azurerm_windows_virtual_machine" "main" {
count = var.ad_host_count
name = format("%s%02d", var.ad_host_prefix, count.index + 1)
location = azurerm_resource_group.adds.location
resource_group_name = azurerm_resource_group.adds.name
network_interface_ids = [azurerm_network_interface.main[count.index].id]
size = var.ad_machine_sku
admin_username = var.admin_username
admin_password = random_password.ad_admin.result
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = var.ad_servers_os
version = "latest"
}
os_disk {
name = format("%s-disk-%s-osdisk-%02d", var.customer_prefix, var.ad_host_prefix, count.index + 1)
caching = "ReadWrite"
storage_account_type = var.disk_type
}
boot_diagnostics {
storage_account_uri = null
}
}
For Each
for_each with a map is better when you need named, distinct resources where each has different properties. Because Terraform tracks resources by their key rather than their position, removing one entry won’t trigger a cascade of replacements.
The example below creates Backup Policies from a map variable. Each key becomes the resource name and doubles as the unique identifier Terraform uses to track it.
Inside the module — main.tf:
resource "azurerm_backup_policy_vm" "main" {
for_each = var.vm_policies
name = each.key
resource_group_name = azurerm_resource_group.backup.name
recovery_vault_name = azurerm_recovery_services_vault.main.name
instant_restore_retention_days = each.value["instant"]
backup {
frequency = "Daily"
time = each.value["time"]
}
retention_daily {
count = each.value["daily"]
}
retention_weekly {
count = each.value["weekly"]
weekdays = ["Sunday"]
}
retention_monthly {
count = each.value["monthly"]
weekdays = ["Sunday"]
weeks = ["First"]
}
}
terraform.tfvars:
vm_policies = {
1yr2300 = {
instant = 5,
time = "23:00",
daily = 35,
weekly = 5,
monthly = 12
},
3yr2300 = {
instant = 5,
time = "23:00",
daily = 35,
weekly = 5,
monthly = 36
}
}
Repeatability of the Module Itself
Repeating the entire module is the more common pattern — deploying the same module into multiple regions or environments. This is where for_each with a map really shines. count doesn’t make sense here because each deployment has meaningfully different configuration (location, address space, etc.).
main.tf:
module "hub" {
for_each = var.hubs
source = "git::https://dev.azure.com/rawritscloud/TerraformModules/_git/terraform-azurerm-hub?ref=v1.0.0"
location = each.key
hub_address_prefix = each.value["hub_address_prefix"]
}
terraform.tfvars:
hubs = {
uksouth = {
hub_address_prefix = "10.100.0.0/23"
},
ukwest = {
hub_address_prefix = "10.101.0.0/23"
}
}
Notice the source block references a specific Git tag (ref=v1.0.0). This is important — if you’re pointing at main, any change to the module will affect all your deployments on the next apply.
Common Mistakes
Using count for resources with different names or properties. Once your resources diverge — different SKUs, different names, different configurations — count breaks down. Reach for for_each with a map instead.
Using count at the module level for multi-region. If each region has any configuration difference, count forces you to shove those differences into indexed lists, which quickly becomes unreadable. A for_each map is almost always the right choice at the module level.
Not pinning the source to a tag. Pointing at main means a breaking change to the module hits every consumer on their next apply. Always pin to a release tag.
Forgetting about uniqueness with count. When using count, you’re responsible for ensuring every resource has a unique name. Use format with count.index — and be aware that the index starts at zero, not one.
Summary
Use count when you need a simple, numbered set of identical resources and don’t expect to remove items from the middle. Use for_each with a map when resources have distinct identities, different properties, or when you’re repeating the entire module across environments. At the module level, for_each is almost always the right choice. Pin your module source to a Git tag so you control when updates roll out.
What to Explore Next
- Automating module workflows — documentation generation and testing pipelines (next in this series)
- Terraform
for_eachdocumentation - Terraform
countdocumentation - Terraform module sources — Git