Terraform for_each if value exists in object - terraform

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
}

Related

Loop over objects within objects using terraform

I have a route_table variable containing route table objects which each have several route objects.
variable "route_tables" {
description = "a map of route tables"
type = map(any)
default = {
"route_table01" = {
name = "route_table01"
route = {
name = "route1"
address_prefix = "17.65.255.0/24"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = "10.18.128.6"
}
route = {
name = "route2"
address_prefix = "204.61.3.87/32"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = "10.18.128.6"
}
route = {
name = "route3"
address_prefix = "13.61.37.248/29"
next_hop_type = "VirtualAppliance"
next_hop_in_ip_address = "10.18.128.6"
}
}
}
}
I'm now trying to loop over them in my module:
resource "azurerm_route_table" "route_tables" {
for_each = var.route_tables
name = each.value.name
location = azurerm_resource_group.resource_group.location
resource_group_name = azurerm_resource_group.resource_group.name
tags = local.tags
dynamic "route" {
for_each = each.value.route
content {
name = each.value.route.name
address_prefix = each.value.route.address_prefix
next_hop_type = each.value.route.next_hop_type
next_hop_in_ip_address = each.value.route.next_hop_in_ip_address
}
}
}
The tf plan completes successfully, but only 1 route is being added to the route table:
How can I loop over these routes so that all of them are added to the route table?
I believe one change you'll want to make is that you currently have nothing allowing you to iterate through each nested block. To use for_each, your dynamic block needs to use a different collection as the basis for how it iterates. Instead:
resource "azurerm_route_table" "route_tables" {
for_each = {
for routes in var.route_tables : routes.name => {
name = routes.name
location = routes.location
resource_group_name = routes.resource_group.name
route = routes.route
}
}
name = each.value.name
location = each.value.location
resource_group_name = each.value.resource_group_name
dynamic "route" {
for_each = each.value.route
content {
name = route.value.name
address_prefix = route.value.address_prefix
next_hop_type = route.value.next_hop_type
next_hop_in_ip_address = route.value.next_hop_in_ip_address
}
}
}
Where your variable might be
variable "route_tables" {
description = "List of route tables to create."
type = any
default = []
}
And your input might be
route_tables = [
{
name = ""
location = ""
resource_group_name = ""
num_loops = 2
route = [
{
name = ""
address_prefix = ""
next_hop_type = ""
next_hop_in_ip_address = ""
},
{
name = ""
address_prefix = ""
next_hop_type = ""
next_hop_in_ip_address = ""
}
}
]
}
Good luck and I hope this helps.
Cheers.

Using multiple module outputs to a new module in Terraform

I have seen some codes with same intention but somehow I couldnt make it work.
I have two different modules,
subnet - Where I'm creating two subnets where subnet name is provided in tfvars
nsg - Where I'm creating two nsg where nsg name is provided in tfvars
And I output the created subnet id's and nsg_ids to my main.tf from both module
What I'm trying to do is to associate each subnets to each nsg's like
subnet1 to nsg1
subnet2 to nsg2
Main.tf
module "nsg" {
source = "./Modules/NSGConfig"
nsglist = var.nsglist
resource_group_name = azurerm_resource_group.resource_group.name
location = azurerm_resource_group.resource_group.location
nsg = tomap(
{
for k, subnet_id in module.SUBNETS.subnet_ids : k =>
{
subnet_id = subnet_id
}
}
)
}
NSG.tf (only including association part)
resource "azurerm_subnet_network_security_group_association" "nsg_association" {
for_each=var.nsg
subnet_id = each.value.subnet_id
network_security_group_id = azurerm_network_security_group.nsg[*].nsg_id #wont work
}
variable.tf (NSG module)
variable "nsg" {
type = map(object({
subnet_id = string
}))
}
I tried to nest the for (in main.tf) to include the output from nsgid but failed.
Ps. I'm really new to terraform
Main.tfvars
RGlocation = "westus"
RGname = "TEST-RG1-TERRAFORM"
VNETname = "TEST-VNET-TERRAFORM"
address_space = "10.0.0.0/16"
Subnetlist = {
"s1" = { name = "TESTSUBNET1-TERRAFORM", address = "10.0.1.0/24" },
"s2" = { name = "TESTSUBNET2-TERRAFORM", address = "10.0.2.0/24" },
"s3" = { name = "TESTSUBNET3-TERRAFORM", address = "10.0.3.0/24" }
}
niclist = {
"s1" = { name = "TESTNIC1-TERRAFORM" },
"s2" = { name = "TESTNIC2-TERRAFORM" },
"s3" = { name = "TESTNIC3-TERRAFORM" }
}
nsglist = {
"s1" = { name = "TESTNSG1-TERRAFORM" },
"s2" = { name = "TESTNSG1-TERRAFORM" },
"s3" = { name = "TESTNSG1-TERRAFORM" }
}
--- Update 2
Module output from the subnet module and NSG module is as below
Outputs:
nsg_id = tomap({
"s1" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/networkSecurityGroups/TESTNSG1-TERRAFORM"
"s2" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/networkSecurityGroups/TESTNSG1-TERRAFORM"
"s3" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/networkSecurityGroups/TESTNSG1-TERRAFORM"
})
sub_id = tomap({
"s1" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/virtualNetworks/SACHIN-TEST-VNET-TERRAFORM/subnets/TESTSUBNET1-TERRAFORM"
"s2" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/virtualNetworks/SACHIN-TEST-VNET-TERRAFORM/subnets/TESTSUBNET2-TERRAFORM"
"s3" = "./resourceGroups/TEST-RG1-TERRAFORM/providers/Microsoft.Network/virtualNetworks/SACHIN-TEST-VNET-TERRAFORM/subnets/TESTSUBNET3-TERRAFORM"
})

Terraform: Object List Value Has No Attributes

Getting an error on Terraform Plan saying my object has no attributes for the name value. We are deploying about 7 private dns zones and many of them live in the same resource group. some may live in others, but most live in the same one.
Error: Unsupported attribute
on Modules/privatednszone/main.tf line 4, in data "azurerm_resource_group" "this":
name = each.value.name
This value does not have any attributes.
MAIN
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "2.83.0"
}
}
}
provider "azurerm" {
features {}
}
variable "private_dns_zones" {
type = map(object({
dns_zone_name = string
resource_group_name = string
tags = map(string)
vnet_links = list(object({
zone_to_vnet_link_name = string
vnet_name = string
networking_resource_group = string
zone_to_vnet_link_exists = bool
vnet_link_rg_name = string
}))
zone_exists = bool
registration_enabled = bool
}))
description = "Map containing Private DNS Zone Objects"
default = {}
}
data "azurerm_resource_group" "this" {
# read from local variable, index is resource_group_name
for_each = local.rgs_map
name = each.value.name
}
locals {
rgs_map = {
for n in var.private_dns_zones :
n.resource_group_name => {
name = n.resource_group_name
}
}
}
output "rgs_map" {
value = local.rgs_map
}
output "rg_data" {
value = data.azurerm_resource_group.this
}
TFVARS
Code below is a sample of two dns zones, but there are additional ones.
private_dns_zones = {
zone1 = {
dns_zone_name = "privatelink.vaultcore.azure.net"
resource_group_name = "Terraform1"
tags = {
iac = "Terraform"
syntax = "zone1"
}
zone_exists = false
vnet_links = [
{
zone_to_vnet_link_name = "vaultcore-vnet-eastus2-01"
vnet_name = "vnet-eastus2-01"
networking_resource_group = "Terraform1"
zone_to_vnet_link_exists = false
vnet_link_rg_name = "Terraform1"
}
]
registration_enabled = false
},
zone2 = {
dns_zone_name = "privatelink.monitor.azure.com"
resource_group_name = "Terraform1"
tags = {
iac = "Terraform"
syntax = "zone2"
}
zone_exists = false
vnet_links = [
{
zone_to_vnet_link_name = "monitor-vnet-eastus2-01"
vnet_name = "vnet-eastus2-01"
networking_resource_group = "Terraform1"
zone_to_vnet_link_exists = false
vnet_link_rg_name = "Terraform1"
}
]
registration_enabled = false
}
}
You code seems to work fine only if I use different resource group names. As you are using duplicate values of resource group names which is your requirement creating a map "rgs_map" with your code is not possible as it will error out with below :
So , in order to resolve the above error , I used something like below :
locals {
rgs_map = {
for i,n in var.private_dns_zones : "${i}" =>{
name = n.resource_group_name
}
}
}
Complete code:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "2.83.0"
}
}
}
provider "azurerm" {
features {}
}
variable "private_dns_zones" {
type = map(object({
dns_zone_name = string
resource_group_name = string
tags = map(string)
vnet_links = list(object({
zone_to_vnet_link_name = string
vnet_name = string
networking_resource_group = string
zone_to_vnet_link_exists = bool
vnet_link_rg_name = string
}))
zone_exists = bool
registration_enabled = bool
}))
description = "Map containing Private DNS Zone Objects"
default = {}
}
data "azurerm_resource_group" "this" {
# read from local variable, index is resource_group_name
for_each = local.rgs_map
name = each.value.name
}
locals {
rgs_map = {
for i,n in var.private_dns_zones : "${i}" =>{
name = n.resource_group_name
}
}
}
output "rgs_map" {
value = local.rgs_map
}
output "rg_data" {
value = data.azurerm_resource_group.this
}
Output:

Terraform dynamic block missing an argument or block definition

I am attempting to use a dynamic block to define multiple ip_rules and virtual_network exceptions in one resource. For some reason, when I attempt to use a variable as my for_each loop, it says the following errors.
variable "vnet_subnet_ids" {
description = "List of strings that are VNet Subnet IDs to whitelist."
type = list(string)
default = [
"/subscriptions/${subscription_id}/resourceGroups/${rg_name}/providers/Microsoft.Network/virtualNetworks/nonprod-vnet-gp-kubernetes/subnets/pods_pub_subnet_01",
"/subscriptions/${subscription_id}/resourceGroups/${rg_name}/providers/Microsoft.Network/virtualNetworks/nonprod-vnet-gp-kubernetes/subnets/pods_pub_subnet_02",
]
sensitive = false
}
resource "azurerm_container_registry" "devops" {
name = var.acr_name
resource_group_name = var.rg_name
location = var.rg_location
sku = var.acr_sku
admin_enabled = false
georeplication_locations = var.acr_geo_rep_locations
network_rule_set {
default_action = "Deny"
dynamic "ip_rule" {
for_each = [1]
content {
action = "Allow"
ip_range = "xxx.xxx.xxx.xxx/32"
}
}
#dynamic "ip_rule" {
# for_each = var.acr_ip_rules
# content {
# action = "Allow"
# ip_range = ip_rule.value
# }
#}
dynamic "virtual_network" {
for_each = var.vnet_subnet_ids
content {
action = "Allow"
subnet_id = virtual_network.value
}
}
tags = var.company_tags
}
However, I get the following error:
│ Error: Argument or block definition required
│
│ On ../../modules/azure/acr/main.tf line 41: An argument or block definition is required here.
╵
The part with the ip_rule works, but the virtual_network part does not. I do not understand why.
There seems to be an open bug related to this. I can't test this at the moment, but see if this variation works for you:
variable "acr_name" { default = "acr_name" }
variable "rg_location" { default = "rg_location" }
variable "acr_sku" { default = "acr_sku" }
variable "subscription_id" { default = "subscription_id" }
variable "rg_name" { default = "rg_name" }
variable "acr_geo_rep_locations" { default = "acr_geo_rep_locations" }
variable "company_tags" { default = "company_tags" }
variable "acr_ip_rules" { default = ["1", "2"]}
variable "vnet_subnet_ids" { default = ["1", "2"]}
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=2.79.1"
}
}
}
provider "azurerm" {
features {}
}
locals {
allowed_ips = [for ip in var.acr_ip_rules : {
action = "Allow",
ip_range = ip
}]
allowed_virtual_networks = [for sub in var.vnet_subnet_ids : {
action = "Allow",
subnet_id = sub
}]
}
resource "azurerm_container_registry" "devops" {
name = var.acr_name
resource_group_name = var.rg_name
location = var.rg_location
sku = var.acr_sku
admin_enabled = false
georeplication_locations = var.acr_geo_rep_locations
network_rule_set {
default_action = "Deny"
ip_rule = local.allowed_ips
virtual_network = local.allowed_virtual_networks
}
tags = var.company_tags
}
Turns out I'm missing a }. So, I'm on to new errors listed here
https://github.com/hashicorp/terraform/issues/22340

How to create multiple instances with multiple subnet ids using terraform?

I have 2 services test1,test2 and for each service i have to create 6 vm's.This 6 vm's should be placed in 3 subnet id's which created in 3 different zones in a same region
In this services,test1 will be in private subnets and test2 will be in public subnets.So i have to pass that correct subnet id when creating ec2 instances
root module:
provider "aws" {
region = var.region
}
module "ecom-vpc" {
source = "./modules/vpc"
}
module "ecom-public-subnet" {
source = "./modules/subnets/public"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-private-subnet" {
source = "./modules/subnets/private"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-instances-sg" {
source = "./modules/sg"
vpc-id = module.ecom-vpc.vpc-id
}
module "ecom-vm-instances" {
source = "./modules/vm"
priv-subnet-ids = module.ecom-private-subnet.ecom_private_subnets
pub-subnet-ids = module.ecom-public-subnet.ecom_public_subnets
instances-sg = module.ecom-instances-sg.ecom-inst-sg
}
From child modules - vpc,subnets,ec2
variable "service-names" {
type = list(any)
default = ["ecom-app-TEST1","ecom-app-TEST2"]
}
variable "availability_zones" {
type = map
default = {
ap-south-1a = {
private_subnet = "10.0.1.0/24"
public_subnet = "10.0.4.0/24"
}
ap-south-1b = {
private_subnet = "10.0.2.0/24"
public_subnet = "10.0.5.0/24"
}
ap-south-1c = {
private_subnet = "10.0.3.0/24"
public_subnet = "10.0.6.0/24"
}
}
}
resource "aws_vpc" "ecom-vpc" {
cidr_block = var.ecom-cidr
}
output "vpc-id" {
value = aws_vpc.ecom-vpc.id
}
resource "aws_subnet" "ecom-private" {
for_each = var.availability_zones
vpc_id = var.vpc-id
cidr_block = each.value.private_subnet
availability_zone = each.key
map_public_ip_on_launch = false
tags = {
Name = "${split("-", each.key)[2]}"
Subnet_Type = "private"
}
}
output "ecom_private_subnets" {
value = aws_subnet.ecom-private
}
resource "aws_subnet" "ecom-public" {
for_each = var.availability_zones
vpc_id = var.vpc-id
cidr_block = each.value.public_subnet
availability_zone = each.key
map_public_ip_on_launch = true
tags = {
Name = "${split("-", each.key)[2]}"
Subnet_Type = "public"
}
depends_on = [aws_internet_gateway.igw
]
}
output "ecom_public_subnets" {
value = aws_subnet.ecom-public
}
I'm trying to achieve the same by creating a locals which combines service names,priv,public subnet id's,instance count(2).But the problem is i'm not able to make it because i'm not able to create a unique combination of keys
locals {
service_subnets = {
for pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
}
}
resource "aws_instance" "ecom-instances" {
for_each = local.service_subnets
ami = data.aws_ami.ecom.id
instance_type = "t3.micro"
tags = {
Name = each.value.service_name
Service = each.value.service_name
}
vpc_security_group_ids = [var.instances-sg[each.value.service_name].id]
subnet_id = "${split("-", each.value.service_name)[2] == "TEST1" ? each.value.pub-subnet.id : each.value.priv-subnet.id }"
}
I'm getting the below error.
Two different items produced the key "ecom-app-TEST1:ap-south-1c-1" in this 'for' expression. If duplicates are expected, use the ellipsis (...)
│ after the value expression to enable grouping by key.
If i add ... and change it as below then it is converted as tuple and i'm not sure how to get and pass the value from each.value in the aws_instance resource
locals {
service_subnets = {
for pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
... }
}
on modules/vm/main.tf line 60, in resource "aws_instance" "ecom-instances":
│ 60: subnet_id = "${split("-", each.value.service_name)[2] == "TEST1" ? each.value.pub-subnet.id : each.value.priv-subnet.id }"
│ ├────────────────
│ │ each.value is tuple with 3 elements
│
│ This value does not have any attributes.
Please Guide me
You could try adding the index to your for loop and making it part of your name, might help you avoid elipsis/tuple conversion.
locals {
service_subnets = {
for index, pair in setproduct(var.service-names, values(var.priv-subnet-ids),values(var.pub-subnet-ids),range(var.instance_count)) :
"${pair[0]}:${pair[1].availability_zone}-${pair[3]}=${index}" => {
service_name = pair[0]
priv-subnet = pair[1]
pub-subnet = pair[2]
}
}
}

Resources