terraform loops - ghdrako/doc_snipets GitHub Wiki

count is what is known as a ‘meta-argument’ defined by the Terraform language. Meta-arguments help achieve certain requirements within the resource block.

There are actually five meta-arguments that can be used within resource blocks at the time of writing:

Meta-arguments can also be used within modules, which differ slightly from the resource meta-arguments:

  • depends_on
  • count
  • for_each
  • providers

Note count, for_each, depends_on are the same between resource blocks and modules

Loops supported by Terraform:

  • count: Looping over resources
  • for_each: Looping over resources and argument must be a map or set of string. The magical object is each. This magical object is an iterator wrapper object. To access the direct object, you use .key and .value.
  • dynamic block: for each inline blocks within a resource. The magical object name corresponds to the original configuration name: dynamic {}
  • for: Looping over defined lists and maps

Limitation:

  • You cannot reference any resource output in count or for_each
  • You cannot use count and for_each within a module configuration. In version 0.13 and above this limitation is taken.

count expression

count indexes start with zero. If count=3 then count.index will be 0,1,2 . If we want start to 1 then chanege ${count.index} on ${count.index+1}

# To Create 3  Resource Groups
resource "azurerm_resource_group" "example" {
  count = 3
  name     = "Terraform-rg${count.index}"
  location = "West Europe"
}

The count meta-argument also accepts numeric expressions. The values of these expressions must be known at runtime, before Terraform performs any remote resource actions. Terraform must know the values of the expression and can’t refer to any resources that values are known for only after the configuration is applied.

resource "aws_instance" "robin_2" {
  count             = length(data.aws.avaliability_zones.all.names)
  availability_zone = data.aws.avaliability_zones.all.names[cout.index]
  ami               = "ami-0c55b159cbfafe1"
  instance_type     = "t3.micro"
}

data "aws_avaliability_zones" "all" {}
# Use customised name for 3 resource groups
variable "rg_names" {
  description = "list of the resource group names"
  type        = list(string)
  default     = ["Azure-rg", "AWS-rg", "Google-rg"]
}

resource "azurerm_resource_group" "example" {
  count = length(var.rg_names)
  name     = var.rg_names[count.index]
  location = "West Europe"
}

feature flag by count

resource "azurerm_application_insights" "appinsight-app" {
  count = use_appinsight == true ? 1 : 0
  ....
}

length function that returns the number of items in the given list.

To read an attribute from a specific resource, then you would be required to define it in the following way:

<PROVIDER>_<TYPE>.<NAME>[INDEX].ATTRIBUTE
# Get id first created resource group
output "rg_id" {
  value       = azurerm_resource_group.example[0].id
  description = "The Id of the resource group"
}

# get ids all created rg
output "All_rg_id" {
  value       = azurerm_resource_group.example[*].id
  description = "The Id of all the resource group"
}

Limitation

  • When using count over an entire resource, you won't be able to use count within a resource to loop an inline code block.
  • Limitation with count is what will happen if you try to make any changes to your defined list.

**Terraform treats that resource as a list or array of resources and because of that, it is used to identify each resource defined within the array by its positional index in that array. The internal representation of these resource groups looks something like this when you run terraform apply:

azurerm_resource_group.example[0]: Azure-rg
azurerm_resource_group.example[1]: AWS-rg
azurerm_resource_group.example[2]: Google-rg

When you remove an item from the middle of an array, all the items after it shift back by one, so after running plan with just two items, Terraform's internal representation will be something like this:

azurerm_resource_group.example[0]: Azure-rg
azurerm_resource_group.example[1]: Google-rg

Teraform remove AWS-rg as we want and recreate Google-rg because change position but is not intended


for_each expression

Instead of specifying the number of resources, the for_each meta-argument accepts a map or a set of strings. This is useful when multiple resources are required that have different values.

A map would be defined like this:

for_each = {
    "Group1" = "[email protected]",
    "Group2" = "[email protected]",
    "Group3" = "[email protected]"
}

A set of strings would be defined like this (used with the toset function to convert a list of strings to a set):

for_each = toset( ["Group1", "[email protected]"] ["Group2", "[email protected]"] ["Group3", "[email protected]"])

If a map is defined, each.key will correspond to the map key, and each.value will correspond to the map value.

In the example above, each.key would show the group name (Group1, Group2, or Group3), and each.value would show the emails (“[email protected]”, “[email protected]”, or “[email protected]”).

If a set of strings is defined, each.key or each.value can be used and will correspond to the same thing.

# To Create Resource Group

resource "azurerm_resource_group" "example" {
  for_each = toset(var.rg_names)
  name     = each.value
  location = "West Europe"
}

toset function convert the var.rg_names list into a set of strings. for_each only supports a set of strings and maps in the resource code block.

We can use a for_each expression for the inline code block inside the resource code block. Dynamically, we will try to have multiple subnets within the virtual network in Azure.

resource "azurerm_resource_group" "example" {
  name     = "Terraform-rg"
  location = "West Europe"
}

resource "azurerm_virtual_network" "vnet" {
  name                = var.vnet_name
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  address_space       = var.address_space
  dynamic "subnet" {
    for_each = var.subnet_names
    content {
      name           = subnet.value.name
      address_prefix = subnet.value.address_prefix
    }
 }
}

variable "subnet_names" {
  default = {
    subnet1 = {
      name           = "subnet1"
      address_prefix = "10.0.1.0/24"
    }
    subnet2 = {
      name           = "subnet2"
      address_prefix = "10.0.2.0/24"
    }
  }
}

variable "vnet_name" {
  default = "terraform-vnet"
}

variable "address_space" {
  default = ["10.0.0.0/16"]
}

When for_each is used with a set, each.key and each.value are the same.


Dynamic block

resource "aws_elastic_beanstalk_environment" "tfenvtest" {
  name = "tf-test-name" # can use expressions here

  setting {
    # but the "setting" block is always a literal block
  }
}

You can dynamically construct repeatable nested blocks like setting using a special dynamic block type, which is supported inside resource, data, provider, and provisioner blocks:

resource "aws_elastic_beanstalk_environment" "tfenvtest" {
  name                = "tf-test-name"
  application         = "${aws_elastic_beanstalk_application.tftest.name}"
  solution_stack_name = "64bit Amazon Linux 2018.03 v2.11.4 running Go 1.12.6"

  dynamic "setting" {
    for_each = var.settings
    content {
      namespace = setting.value["namespace"]
      name = setting.value["name"]
      value = setting.value["value"]
    }
  }
}

Facts:

  • content {} contanin block of code which is iterate
  • Terraform magically provides an object. The object name matches the dynamic argument : dynamic {}.
  • The object is a wrapper iterator object that contains info for each element that was assigned with for_each
locals {
  rules = [{
    description = "description 0",
    port = 80,
    cidr_blocks = ["0.0.0.0/0"],
  },{
    description = "description 1",
    port = 81,
    cidr_blocks = ["10.0.0.0/16"],
  }]
}
resource "aws_security_group" "attrs" {
  name        = "demo-attrs"
  description = "demo-attrs"

  dynamic "ingress" {
    for_each = local.rules
    content {
      description = ingress.value.description
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}

Above localy structure data is List of Maps. The ingress object is a wrapper object described in the previous example. The ingress.value unravels the wrapper object and contains each element of the List, which is a Map. The ingress.key was not used because it contains the index number and is not very useful.

resource "azurerm_virtual_network" "vnet" {
  name                = var.vnet_name
  location            = azurerm_resource_group.example.location
  resource_group_name = azurerm_resource_group.example.name
  address_space       = var.address_space
  dynamic "subnet" {
   for_each = var.subnet_names
    content {
      name           = subnet.value.name
      address_prefix = subnet.value.address_prefix
    }
  }
}

variable "subnet_names" {
  default = {
    subnet1 = {
      name           = "subnet1"
      address_prefix = "10.0.1.0/24"
    }
    subnet2 = {
      name           = "subnet2"
      address_prefix = "10.0.2.0/24"
    }
  }
}

Above local data structure is map of map

locals {
  map = {
    "description 0" = {
      port = 80,
      cidr_blocks = ["0.0.0.0/0"],
    }
    "description 1" = {
      port = 81,
      cidr_blocks = ["10.0.0.0/16"],
    }
  }
}
resource "aws_security_group" "map" {
  name        = "demo-map"
  description = "demo-map"

  dynamic "ingress" {
    for_each = local.map
    content {
      description = ingress.key # IE: "description 0"
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
}
output "map" {
  value = aws_security_group.map
}

change iterator name using iterator =

dynamic "ingress" {
    for_each = local.map
    # normally would be "ingress" here, but we're overriding the name
    iterator = each
    content {

Multi-level Nested Block Structures

variable "load_balancer_origin_groups" {
  type = map(object({
    origins = set(object({
      hostname = string
    }))
  }))
}

dynamic "origin_group" {
    for_each = var.load_balancer_origin_groups
    content {
      name = origin_group.key

      dynamic "origin" {
        for_each = origin_group.value.origins
        content {
          hostname = origin.value.hostname
        }
      }
    }
  }

presence of a block conditionally using dynamic block

resource "azurerm_linux_virtual_machine" "virtual_machine" {
...
  dynamic "boot_diagnostics" {
     for_each = local.use_boot_diagnostics == true ? [1] : []
     content {
     storage_account_uri = "https://storageboot.blob.core.windows.net/"
     }
  }
}

in the for_each expression of the dynamic expression, we have a condition that returns a list with one element if the local value, use_boot_diagnostics, is true. Otherwise, this condition returns an empty list that will not make the boot_diagnostics block appear in the azurerm_virtual_machine resource.


for expression

for expression allows the construction of a list or map by transforming and filtering elements in another list or map.

for expression can be use for both list and map. The following is the syntax for defining the for expression in a code block:

[for <ITEM> in <LIST> : <OUTPUT>]
[for <KEY>, <VALUE> in <MAP> : <OUTPUT>]
variable "cloud" {
  description = "A list of cloud"
  type        = list(string)
  default     = ["azure", "aws", "gcp"]
}

output "cloud_names" {
  value = [for cloud_name in var.cloud : upper(cloud_name)]
}
variable "cloud_map" {
  description = "map"
  type        = map(string)
  default = {
    Azure = "Microsoft"
    AWS   = "Amazon"
    GCP   = "Google"
  }
}

output "cloud_mapping" {
  value = [for cloud_name, company in var.cloud_map : "${cloud_name} cloud is founded by ${company}"]
}
variable "subnet_numbers" {
  description = "List of 8-bit numbers of subnets of base_cidr_block that should be granted access."
  default = [1, 2, 3]
}


resource "aws_security_group" "example" {
  name        = "friendly_subnets"
  description = "Allows access from friendly subnets"
  vpc_id      = var.vpc_id

  ingress {
    from_port = 0
    to_port   = 0
    protocol  = -1

    # For each number in subnet_numbers, extend the CIDR prefix of the
    # requested VPC to produce a subnet CIDR prefix.
    # For the default value of subnet_numbers above and a VPC CIDR prefix
    # of 10.1.0.0/16, this would produce:
    #   ["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
    cidr_blocks = [
      for num in var.subnet_numbers:
      cidrsubnet(data.aws_vpc.example.cidr_block, 8, num)
    ]
  }
}
⚠️ **GitHub.com Fallback** ⚠️