Terraform - nested loop for resource creation - azure

I'm trying to create all defined virtual_networks in all defined resource_groups.
So the variables looks like this:
inputs = {
virtual_networks = {
"test-net1": {
address_space = ["10.0.0.0/24"]
}
"test-net2": {
address_space = ["10.0.1.0/24"]
}
}
resource_groups = {
"group1": {
location = "West Europe"
}
"group2": {
location = "East Europe"
}
}
}
The resource block looks something like this:
resource "azurerm_virtual_network" "virtual_network" {
for_each = var.resource_groups
name = <FIRST KEY OF var.virtual_networks>
resource_group_name = each.key
location = each.value["location"]
address_space = <ADDRESS_SPACE VALUE OF FIRST NETWORK FROM var.virtual_networks>
}
I tried many different ways transforming/restructuring/merging the maps from the variables so i can iterate over them in a better way for this use-case. However i just could not find the right structure and how to build it.
I built the following local variable to help solving this. However i was not able to structure the map in a way so i can loop over it in a single resource block.
locals = {
resource_groups = {
for key, resource_groups in var.resource_groups : "${key}" =>
merge(
var.virtual_networks,
resource_groups
)
}
}
Outputs:
group1 = {
location = "West Europe"
test-net1 = {
address_space = [
"10.0.0.0/24",
]
}
test-net2 = {
address_space = [
"10.0.1.0/24",
]
}
}
group2 = {
location = "West Europe"
test-net1 = {
address_space = [
"10.0.0.0/24",
]
}
test-net2 = {
address_space = [
"10.0.1.0/24",
]
}
}

As far as i understand, you want to created a neseted loop over two dictionaries. You cloud structure your local like this :
locals {
resource_groups = distinct(flatten([
for k1, rg in var.resource_groups : [
for k2, vn in var.virtual_networks : {
group = merge({name = "${k1}"}, rg )
network = merge({name = "${k2}"}, vn )
}
]
]))
}
The output would result in:
Changes to Outputs:
+ test = [
+ {
+ group = {
+ location = "West Europe"
+ name = "group1"
}
+ network = {
+ address_space = [
+ "10.0.0.0/24",
]
+ name = "test-net1"
}
},
+ {
+ group = {
+ location = "West Europe"
+ name = "group1"
}
+ network = {
+ address_space = [
+ "10.0.1.0/24",
]
+ name = "test-net2"
}
},
+ {
+ group = {
+ location = "East Europe"
+ name = "group2"
}
+ network = {
+ address_space = [
+ "10.0.0.0/24",
]
+ name = "test-net1"
}
},
+ {
+ group = {
+ location = "East Europe"
+ name = "group2"
}
+ network = {
+ address_space = [
+ "10.0.1.0/24",
]
+ name = "test-net2"
}
},
]
And your resource would look like this:
resource "azurerm_virtual_network" "virtual_network" {
for_each = {for obj in local.resource_groups : "${obj.group.name}_${obj.network.name}" => obj}
name = each.value.network.name
resource_group_name = each.value.group.name
location = each.value.group.location
address_space = each.value.network.address_space
}

Related

How to access a sequence in a map inside a for_each loop in Terraform

Using Terraform, I have a list of maps defined as variable e.g.
storage_accounts = {
stacctest1 = {
resource_group_name = "testrg",
location = "uksouth",
account_tier = "Standard",
account_replication_type = "GRS",
containers_list = [
{ name = "test_private_x", access_type = "private" },
{ name = "test_blob_x", access_type = "blob" },
{ name = "test_container_x", access_type = "container" }
]
}
stacctest2 = {
resource_group_name = "testrg",
location = "uksouth",
account_tier = "Standard",
account_replication_type = "GRS",
containers_list = [
{ name = "test_private_a", access_type = "private" },
{ name = "test_blob_a", access_type = "blob" },
{ name = "test_container_a", access_type = "container" }
]
}
}
Then in a module, I can use a for_each loop to go through each item in the list to create each storage account, e.g.
resource "azurerm_storage_account" "storage" {
for_each = var.storage_accounts
name = each.key
resource_group_name = each.value.resource_group_name
location = each.value.location
account_tier = each.value.account_tier
account_replication_type = each.value.account_replication_type
}
}
To process the container sequence in the map, I was thinking I can just loop though the list again process the containers, but struggling to work out how to fit a nested loop in with
resource "azurerm_storage_container"
You have to flatten your variable. For example:
locals {
flat_storage_accounts = merge([
for account_name, details in var.storage_accounts: {
for container in details.containers_list:
"${account_name}-${container.name}" =>
merge(details, {
"account_name" = account_name
"container" = container
})
}
]...)
}
then
resource "azurerm_storage_account" "storage" {
for_each = local.flat_storage_accounts
name = each.account_name
resource_group_name = each.value.resource_group_name
location = each.value.location
account_tier = each.value.account_tier
account_replication_type = each.value.account_replication_type
# use individual container_list as
# some_attribute = each.value.container.access_type
}
You haven't provided code where you want to use containers_list, so I can only make comment above.

How do I loop through te below given variable in terraform

I am trying to create vnets for multiple environments using one single variable. I am not quite sure if it is possible. My variable is given below
azure_vnets = {
Prod = [
{
cidr = "10.10.0.0/24"
vnet_name = "prod-vnet1"
dns = "10.10.0.1"
rg = "prodrg1"
location = "eastus"
},
{
cidr = "10.10.1.0/24"
vnet_name = "prod-vnet2"
dns = "10.10.0.2"
rg = "prodrg2"
location = "eastus"
}
],
nonProd = [
{
cidr = "10.10.0.0/24"
vnet_name = "nonprod-vnet1"
dns = "10.10.0.1"
rg = "nonprodrg1"
location = "eastus"
},
{
cidr = "10.10.1.0/24"
vnet_name = "nonprod-vnet2"
dns = "10.10.0.2"
rg = "nonprodrg2"
location = "eastus"
}
]
}
So as to create multiple vnets from this
resource "azurerm_virtual_network" "this" {
for_each = xxx
name = each.xxx
xxxx
xxx
}
You have to flatten it first:
locals {
flat_azure_vnets = merge([
for env_name, env_vn_list in var.azure_vnets:
{
for idx, env_vn in env_vn_list:
"${env_name}-${idx}" => env_vn
}
]...)
}
then you use it:
resource "azurerm_virtual_network" "this" {
for_each = local.flat_azure_vnets
name = each.value["vnet_name"]
xxxx
xxx
}

Obtain value from a local list

I am trying to tag the docker swarm instances using terraform
I defined variable and locals as
variables.tf
variable "instance_count" {
default = "3"
}
variable "instance_type" {
default = "t2.micro"
}
variable "aws_region" {
default = "us-east-1"
}
variable "ami" {
default = "ami-09e67e426f25ce0d7"
}
variable "host_name" {
type = map(number)
default = {
"Manager" = 1
"Worker" = 2
}
}
When i refer to this list's each value to assign it as a tag to ec2 instance like this
ec2instance.tf
resource "aws_instance" "swarm_instance" {
count = var.instance_count
ami = var.ami
instance_type = var.instance_type
key_name = aws_key_pair.dockerswarm.key_name
tags = {
Name = "Swarm_Instance-${count.index + 1}"
}
tags = {
Name = "${local.expanded_names}"
}
locals {
expanded_names = {
for name, count in var.host_name : name => [
for i in range(count) : format("%s-%02d", name, i+1)
]
}
}
Terraform complains
local.expanded_names is object with 2 attributes
I tried with ${local.expanded_names.value}, but then it complained object does not have an attribute named "value".
So how to retrieve the value from the list when value attribute is not available in terraform.
The tags should be strings, in your case I would use jsonencode to get a string out of that object you are building, see my sample code below
variable "host_name" {
type = map(number)
default = {
"Manager" = 1
"Worker" = 2
}
}
locals {
expanded_names = jsonencode({
for name, count in var.host_name : name => [
for i in range(count) : format("%s-%02d", name, i+1)
]
})
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "instance" {
ami = "ami-1c761163"
instance_type = "r5.large"
tags = {
Terraformed = "true"
Name = local.expanded_names
}
}
if we run a terraform plan on that, here is what we get:
Terraform will perform the following actions:
# aws_instance.instance will be created
+ resource "aws_instance" "instance" {
+ ami = "ami-1c761163"
...
+ instance_state = (known after apply)
+ instance_type = "r5.large"
...
+ subnet_id = (known after apply)
+ tags = {
+ "Name" = jsonencode(
{
+ Manager = [
+ "Manager-01",
]
+ Worker = [
+ "Worker-01",
+ "Worker-02",
]
}
)
+ "Terraformed" = "true"
}
Or maybe what you meant to do is create an array of names:
Manager-01
Worker-01
Worker-02
Then use that as the instance names... if that is the case your expanded_names should not be an object {} but an array [], then we use that instead of your count, see code sample below:
variable "host_name" {
type = map(number)
default = {
"Manager" = 1
"Worker" = 2
}
}
locals {
expanded_names = flatten([
for name, count in var.host_name : [
for i in range(count) : format("%s-%02d", name, i+1)
]
])
}
provider "aws" {
region = "us-east-1"
}
resource "aws_instance" "instance" {
for_each = toset(local.expanded_names)
ami = "ami-1c761163"
instance_type = "r5.large"
tags = {
Terraformed = "true"
Name = each.value
}
}
and a terraform plan on that outputs:
Terraform will perform the following actions:
# aws_instance.instance["Manager-01"] will be created
+ resource "aws_instance" "instance" {
+ ami = "ami-1c761163"
...
+ tags = {
+ "Name" = "Manager-01"
+ "Terraformed" = "true"
}
...
}
# aws_instance.instance["Worker-01"] will be created
+ resource "aws_instance" "instance" {
+ ami = "ami-1c761163"
...
+ tags = {
+ "Name" = "Worker-01"
+ "Terraformed" = "true"
}
...
}
# aws_instance.instance["Worker-02"] will be created
+ resource "aws_instance" "instance" {
+ ami = "ami-1c761163"
...
+ tags = {
+ "Name" = "Worker-02"
+ "Terraformed" = "true"
}
...
}
Plan: 3 to add, 0 to change, 0 to destroy.

Terraform: How to iterate the name or another variable in a resource group when the variable being passed in is a list of a list of maps?

This is what I have so far, while I can pull a specific list or name using the local variables, I am having trouble transitioning this into the resource group. First, am I attempting this the right way? If not how can I iterate on the name for the subnet so that the subnets belonging to the respective vnet map are added?
variable "vnets" {
default = [
{
vnet_name = "test-vnet"
address_space = "10.250.0.0"
network_size = 16
subnets = [
{
name = "first-subnet"
network_security_group = "first-nsg"
security_group_rules = [
{
name = "first-sg"
priority = 100
}
]
},
{
name = "second-subnet"
network_security_group = "second-nsg"
security_group_rules = [
{
name = "second-sg"
priority = 100
}
]
}
]
}
]
}
locals {
subnet_names = {
for vnet in var.vnets[*]:
(vnet.vnet_name) => vnet.subnets[*].name
}
security_group_names = flatten(var.vnets[*].subnets[*].security_group_rules[*].name)
}
resource "azurerm_subnet" "subnets" {
count = length(var.vnets)
#??? name = locals.subnet_names[count.index].subnets.name
resource_group_name = data.azurerm_resource_group.network_group.name
virtual_network_name = azurerm_virtual_network.vnets.*.name
address_prefixes = ["10.0.1.0/24"]
}
I think the easiest would be to flatten your subnet_names:
locals {
subnet_names = {
for vnet in var.vnets[*]:
(vnet.vnet_name) => vnet.subnets[*].name
}
security_group_names = flatten(var.vnets[*].subnets[*].security_group_rules[*].name)
# uniqueness of "${vnet}-${subnet}" pairs is assumed. it will not work
# if the pairs are not unique
subnet_names_flat = merge([
for vnet, subnets in local.subnet_names:
{
for subnet in subnets:
"${vnet}-${subnet}" => {name = vnet, subnet = subnet}
}
]...)
}
Which will result in subnet_names_flat being:
{
"test-vnet-first-subnet" = {
"name" = "test-vnet"
"subnet" = "first-subnet"
}
"test-vnet-second-subnet" = {
"name" = "test-vnet"
"subnet" = "second-subnet"
}
}
Then your azurerm_subnet.subnets could as below. However, I'm not able to verify correctness of your the azurerm_subnet, thus you may need to change it further. But the idea is to iterate over local.subnet_names_flat, which makes the for_each very easy to use:
resource "azurerm_subnet" "subnets" {
for_each = local.subnet_names_flat
name = each.value.subnet
resource_group_name = data.azurerm_resource_group.network_group.name
virtual_network_name = each.value.vnet
address_prefixes = ["10.0.1.0/24"]
}

Terraform for_each if value exists in object

I would like to dynamically create some subnets and route tables from a .tfvars file, and then link each subnet to the associated route table if specified.
Here is my .tfvars file:
vnet_spoke_object = {
specialsubnets = {
Subnet_1 = {
name = "test1"
cidr = ["10.0.0.0/28"]
route = "route1"
}
Subnet_2 = {
name = "test2"
cidr = ["10.0.0.16/28"]
route = "route2"
}
Subnet_3 = {
name = "test3"
cidr = ["10.0.0.32/28"]
}
}
}
route_table = {
route1 = {
name = "route1"
disable_bgp_route_propagation = true
route_entries = {
re1 = {
name = "rt-rfc-10-28"
prefix = "10.0.0.0/28"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = "10.0.0.10"
}
}
}
route2 = {
name = "route2"
disable_bgp_route_propagation = true
route_entries = {
re1 = {
name = "rt-rfc-10-28"
prefix = "10.0.0.16/28"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = "10.0.0.10"
}
}
}
}
...and here is my build script:
provider "azurerm" {
version = "2.18.0"
features{}
}
variable "ARM_LOCATION" {
default = "uksouth"
}
variable "ARM_SUBSCRIPTION_ID" {
default = "asdf-b31e023c78b8"
}
variable "vnet_spoke_object" {}
variable "route_table" {}
module "names" {
source = "./nbs-azure-naming-standard"
env = "dev"
location = var.ARM_LOCATION
subId = var.ARM_SUBSCRIPTION_ID
}
resource "azurerm_resource_group" "test" {
name = "${module.names.standard["resource-group"]}-vnet"
location = var.ARM_LOCATION
}
resource "azurerm_virtual_network" "test" {
name = "${module.names.standard["virtual-network"]}-test"
location = var.ARM_LOCATION
resource_group_name = azurerm_resource_group.test.name
address_space = ["10.0.0.0/16"]
}
resource "azurerm_subnet" "test" {
for_each = var.vnet_spoke_object.specialsubnets
name = "${module.names.standard["subnet"]}-${each.value.name}"
resource_group_name = azurerm_resource_group.test.name
virtual_network_name = azurerm_virtual_network.test.name
address_prefixes = each.value.cidr
}
resource "azurerm_route_table" "test" {
for_each = var.route_table
name = "${module.names.standard["route-table"]}-${each.value.name}"
location = var.ARM_LOCATION
resource_group_name = azurerm_resource_group.test.name
disable_bgp_route_propagation = each.value.disable_bgp_route_propagation
dynamic "route" {
for_each = each.value.route_entries
content {
name = route.value.name
address_prefix = route.value.prefix
next_hop_type = route.value.next_hop_type
next_hop_in_ip_address = contains(keys(route.value), "next_hop_in_ip_address") ? route.value.next_hop_in_ip_address: null
}
}
}
That part works fine in creating the vnet/subnet/route resources, but the problem I face is to dynamically link each subnet to the route table listed in the .tfvars. Not all the subnets will have a route table associated with it, thus it will need to only run IF the key/value route is listed.
resource "azurerm_subnet_route_table_association" "test" {
for_each = {
for key, value in var.vnet_spoke_object.specialsubnets:
key => value
if value.route != null
}
lifecycle {
ignore_changes = [
subnet_id
]
}
subnet_id = azurerm_subnet.test[each.key].id
route_table_id = azurerm_route_table.test[each.key].id
}
The error I face with the above code is:
Error: Unsupported attribute
on main.tf line 65, in resource "azurerm_subnet_route_table_association" "test":
65: if value.route != null
This object does not have an attribute named "route".
I have tried various ways with no success, and I'm at a loss here and would appreciate any guidance posisble.
Based on your scenario, I'm guessing vnet_spoke_object in input looks like this:
vnet_spoke_object = {
specialsubnets = {
subnetA = {
cidr = "..."
}
subnetB = {
cidr = "..."
route = "..."
}
}
}
The problem with that is that a missing route entry doesn't resolve to null, it causes a panic or crash. You'd need to write your input like this (with explicit nulls):
vnet_spoke_object = {
specialsubnets = {
subnetA = {
cidr = "..."
route = null
}
subnetB = {
cidr = "..."
route = "..."
}
}
}
Or lookup route by name and provide a null default in your for map generator expression like this:
for_each = {
for key, value in var.vnet_spoke_object.specialsubnets:
key => value
if lookup(value, "route", null) != null
}

Resources