Multiple Vnets and subnets using terraform modules - terraform

I am new to terraform and I am trying to create module for multiple vnets and under that multiple subnets
I am able to create multiple vnets using the module but I am facing issue in creating multiple subnets on one or more of the vnets created.
Below is my code for the module and as well as main.tf
network resources module/main.tf
data "azurerm_resource_group" "network" {
name = var.resource_group_name
}
resource "azurerm_virtual_network" "vnets" {
count = length(var.vnet_names)
name = var.vnet_names[count.index]
resource_group_name = data.azurerm_resource_group.network.name
location = var.vnet_location != null ? var.vnet_location : data.azurerm_resource_group.network.location
address_space = [var.vnet_adress_spaces[count.index]]
}
network resources module/variables.tf
variable "vnet_names" {
description = "Name of the vnets to be created"
type = list(string)
default = ["vnet1","vnet2","vnet3"]
}
variable "vnet_adress_spaces" {
description = "Name of the vnets to be created"
type = list(string)
default = ["192.168.0.0/16" ,"10.0.0.0/16","10.80.0.0/16"]
}
variable "resource_group_name" {
description = "Name of the resource group to be imported."
type = string
}
variable "vnet_location" {
description = "The location of the vnet to create. Defaults to the location of the resource group."
type = string
default = null
}
variable "subnet_names" {
description = "The list of subnets which needs to be created"
type = list(list(string))
default = [[],["subnet1_vnet1","subnet2_vnet1"],["subnet1_vnet3","subnet2_vnet3","subnet3_vnet3"]]
}
variable "subnet_addresses" {
description = "The list of subnets which needs to be created"
type = list(list(string))
default = [[],["10.0.2.0/24","10.0.0.0/24"],["10.80.2.0/24","10.80.1.0/24","10.80.0.0/24"]]
}
main.tf
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "=2.98.0"
}
}
}
# Configure the Microsoft Azure Provider
provider "azurerm" {
features {}
}
resource "azurerm_resource_group" "rg1" {
name = "rg1"
location = "West Europe"
}
module network {
source = "./network_resources"
resource_group_name = azurerm_resource_group.rg1.name
}
3 vnets are successfully created but I am facing issue in writing subnet creation as mentioned in variables.tf
Please can you help me with that or suggest a better way of implementing this
Output.tf of module
output "vnet_names" {
description = "The name of the virtual networks"
value = {for k, v in azurerm_virtual_network.vnets: k => v.name}
}
output "vnet_addresses" {
description = "The name of the virtual networks"
value = {for k, v in azurerm_virtual_network.vnets: k => v.address_space}
}
output "subnet_names" {
description = "The name of the subnets"
value = {for k, v in azurerm_subnet.subnets: k => v.name}
}
output "subnet_addresses" {
description = "The name of the subnet addresses"
value = {for k, v in azurerm_subnet.subnets: k => v.address_prefixes}
}
output "subnet_ids" {
description = "The name of the subnet addresses"
value = {for k, v in azurerm_subnet.subnets: k => v.id}
}
When I am taking the subnet value same for two vnets as per the updated variables.tf
vnets = {
"mel-dev-identity-vnet01" = {
address_space = "10.0.0.0/16",
subnets = [
{
subnet_name = "subnet-mel-AD-dev"
subnet_address = "10.0.2.0/24"
service_endpoints = []
},
{
subnet_name = "subnet-mel-okt-dev"
subnet_address = "10.0.0.0/24"
service_endpoints = []
},
{
subnet_name = "GatewaySubnet"
subnet_address = "10.0.0.0/26"
service_endpoints = []
},
]
},
"mel-dev-identity-vnet02" = {
address_space = "10.80.0.0/16"
subnets = [
{
subnet_name = "subnet-syd-AD-dev"
subnet_address = "10.80.2.0/24"
service_endpoints = []
},
{
subnet_name = "subnet-syd-okt-dev"
subnet_address = "10.80.1.0/24"
service_endpoints = []
},
{
subnet_name = "GatewaySubnet"
subnet_address = "10.80.0.0/26"
service_endpoints = []
},
]
}
}
I am getting the below error:
│ Error: Duplicate object key
│
│ on network_resources\locals.tf line 11, in locals:
│ 11: subnets = { for subnet in local.subnets_flatlist : subnet.subnet_name => subnet }
│ ├────────────────
│ │ subnet.subnet_name is "GatewaySubnet"
│
│ Two different items produced the key "GatewaySubnet" in this 'for' expression. If duplicates are expected, use the
│ ellipsis (...) after the value expression to enable grouping by key.

I see two issues in your code:
You store attributes (e.g. subnet_name, subnet_addressed) of the same resources in different variables of type list or nested list.
This way you have to ensure consistency across the different variables manually which can become tedious and error prone. Imagine you want to add a third subnet to vnet bupavnet2. You have to make sure that you insert the new name and subnet at the corresponding positions in two nested lists.
BTW: Is it intended that the subnets in vnet bupavnet2 are named subnet#-bupavnet1 ? See what I mean ??? :)
In order to create multiple subnets in multiple vnets dynamically, you would need a nested loop which iterates over the vnets in the outer loop and across the subnets in the inner loop. Terraform, however, does not support nested for_each loops on resource level.
One way to overcome the first issue is to use complex types like objects or maps in order to make the relationship between the attributes of a resource explicit. This way it would be easy to spot the naming issue of the subnets in bupavnet2. If you want to add an additional subnet, you just need to add an additional object to the subnets list.
variable "vnets" {
type = map(object({
address_space = string
subnets = list(object({
subnet_name = string
subnet_address = string
}))
}))
default = {
"bupavnet1" = {
address_space = "192.168.0.0/16",
subnets = []
},
"bupavnet2" = {
address_space = "10.0.0.0/16",
subnets = [
{
subnet_name = "subnet1_bupavnet1"
subnet_address = "10.0.2.0/24"
},
{
subnet_name = "subnet2_bupavnet1"
subnet_address = "10.0.0.0/24"
}
]
},
"bupavnet3" = {
address_space = "10.80.0.0/16"
subnets = [
{
subnet_name = "subnet1_bupavnet3"
subnet_address = "10.80.2.0/24"
},
{
subnet_name = "subnet2_bupavnet3"
subnet_address = "10.80.1.0/24"
},
{
subnet_name = "subnet3_bupavnet3"
subnet_address = "10.80.0.0/24"
},
]
}
}
}
In succession, the creation of the vnets would change to
resource "azurerm_virtual_network" "vnets" {
for_each = var.vnets
name = each.key
resource_group_name = data.azurerm_resource_group.network.name
location = var.vnet_location != null ? var.vnet_location : data.azurerm_resource_group.network.location
address_space = [each.value.address_space]
}
Now let's have a look how to tackle the nested for loop issue. In Terraform you would address this by flattening the nested structure which we defined above. In the first step we create a flat list of objects representing the subnets to be created. So our variable subnets_flatlist has the type tuple of objects.
Unfortunately the for_each argument in Terraform requires the type map or set of strings. So we need a second step to create a map out of our flat list.
locals {
subnets_flatlist = flatten([for key, val in var.vnets : [
for subnet in val.subnets : {
vnet_name = key
subnet_name = subnet.subnet_name
subnet_address = subnet.subnet_address
}
]
])
subnets = { for subnet in local.subnets_flatlist : subnet.subnet_name => subnet }
}
Once we created our helper structures we can iterate over the subnets map and create the subnet resources:
resource "azurerm_subnet" "subnets" {
for_each = local.subnets
name = each.value.subnet_name
resource_group_name = data.azurerm_resource_group.network.name
virtual_network_name = azurerm_virtual_network.vnets[each.value.vnet_name].name
address_prefixes = [each.value.subnet_address]
}

Related

Terraform - Error: Unsupported attribute, This object does not have an attribute named "name"

I'm kind of new to Terraform and I am trying to construct Azure Private DNS zones within an Azure subscription.
In Terraform, I am trying to create that resource utilizing a map object within a module. Here is the code that I have:
Root Module
dns_zone = {
"private.vaultcore.azure.net" = {
registration_enabled = false
vnet_link_name = join("-", [data.azurerm_key_vault.existing-keyvault.name, "link"])
vnet_id = data.azurerm_virtual_network.existing-vnet.id
a_record = [
{
name = "test"
ttl = 300
ip_addresses = ["10.10.10.10"]
}
]
}
}
Child Module - main.tf
resource "azurerm_private_dns_zone" "zone" {
for_each = var.dns_zone
name = each.key
resource_group_name = var.rsg_name
}
resource "azurerm_private_dns_zone_virtual_network_link" "link" {
for_each = var.dns_zone
name = each.value.vnet_link_name
resource_group_name = var.rsg_name
private_dns_zone_name = azurerm_private_dns_zone.zone[each.key].name
virtual_network_id = each.value.vnet_id
registration_enabled = each.value.registration_enabled
}
resource "azurerm_private_dns_a_record" "records" {
for_each = { for a_record in var.dns_zone : a_record.name => a_record }
name = each.value.name
zone_name = azurerm_private_dns_zone.zone[each.key].name
resource_group_name = var.rsg_name
ttl = each.value.ttl
records = split(",", replace(each.value.ip_addresses, " ", ""))
}
Child Module - variable.tf
variable "dns_zone" {
type = map(object({
registration_enabled = bool
vnet_link_name = string
vnet_id = string
a_record = list(object({
name = string,
ttl = number,
ip_addresses = list(string)
}))
}))
Child Module - output.tf
output "zone" {
value = azurerm_private_dns_zone.zone
}
output "records" {
value = azurerm_private_dns_a_record.records
}
When I run terraform plan I get the following message:
│ Error: Unsupported attribute
│
│ on ..\modules\vnet\main.tf line 176, in resource "azurerm_private_dns_a_record" "records":
│ 176: for_each = { for a_record in var.dns_zone : a_record.name => a_record }
│
│ This object does not have an attribute named "name".
My goal is to be able to create a dns_zone mapping that will create private DNS zones with each key. Within the key, I will be able to adjust specific settings and to also create a list of A records.
I've been working at this for awhile but I can't seem to pinpoint the issue in the for_each loop. If I comment out the Azure Private DNS A record resource only utilizing Private DNS zone and the Virtual Network link resources, it will display the Terraform Plan correctly.
In case anyone reads this in the future and wonders how to resolve this, I found that I had to use the flatten function with nested for expressions. Here is the additional code. This went into the child module - main.tf file. Everything else remained the same. Once I ran a terraform plan, it successfully produced the plan information.
locals {
dns_a_records = flatten([
for dns_zone_key, zone in var.dns_zone : [
for dns_record_key, record in zone.a_record : {
dns_zone_key = dns_zone_key
dns_record_key = dns_record_key
dns_zone_name = azurerm_private_dns_zone.zone[dns_zone_key].name
name = record.name
ttl = record.ttl
ip_addresses = record.ip_addresses
}
]
])
}
resource "azurerm_private_dns_a_record" "records" {
for_each = { for record in local.dns_a_records : "${record.dns_zone_key}.${record.dns_record_key}" => record }
name = each.value.name
zone_name = each.value.dns_zone_name
resource_group_name = var.rsg_name
ttl = each.value.ttl
records = each.value.ip_addresses
}

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"
})

Multiple subnets in same VNet with Terrafrom

I'm completely new to terraform and I'm trying to learn and write a TF code to automate Azure VM deployment. I'm trying to cover each parts as modules (except rg) rather than keeping it in a single main.tf file.
My intention is to create 1 vnet (TESTVNET) and create multiple subnets in same Vnet, where I can define the subnet name and address in my tfvars file.
I'm able to reach till creation on VNet, but cant loop through the defined subnets
Please go through my code. File Main.tf
resource "azurerm_resource_group" "resource_group" {
name = var.RGname
location = var.RGlocation
}
module "VNET" {
source = "./Modules/NetworkConfig"
name = var.VNETname
address_space = var.address_space
location = var.RGlocation
resource_group_name = azurerm_resource_group.resource_group.name
}
module "SUBNETS" {
source = "./Modules/SubnetConfig"
Subnetlist = var.Subnetlist
virtual_network_name = module.VNET.vnet_name
resource_group_name = azurerm_resource_group.resource_group.name
depends_on = [azurerm_resource_group.resource_group, module.VNET.vnet]
}
Variables.tf (of main)
variable "RGlocation" {
}
variable "RGname" {
}
variable "VNETname" {
}
variable "address_space" {
}
variable "Subnetlist" {
type = map(object({
name = list(string)
address = list(string)
}))
}
File main.tfvars
RGlocation = "westus"
RGname = "TEST-RG1"
VNETname = "TEST-VNET-01"
address_space = "10.0.0.0/16"
Subnetlist = {
"list" = {
name = ["TESTSUBNET","TESTSUBNET1","TESTSUBNET2"]
address = ["10.0.1.0/24","10.0.2.0/24","10.0.3.0/24"]
}
}
File Subnets.tf (module)
resource "azurerm_subnet" "SUBNETS" {
for_each=var.Subnetlist
name=each.value.name
address_prefixes=each.value.address
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
}
File variable.tf (subnet module)
variable "resource_group_name" {
}
variable "virtual_network_name" {
}
variable "Subnetlist" {
type = map(object({
name = list(string)
address = list(string)
}))
}
Below if the error that I'm getting
╷
│ Error: Incorrect attribute value type
│
│ on Modules\SubnetConfig\Subnet.tf line 3, in resource "azurerm_subnet" "SUBNETS":
│ 3: name=each.value.name
│ ├────────────────
│ │ each.value.name is list of string with 3 elements
│
│ Inappropriate value for attribute "name": string required.
Could anyone please tell me how to resolve it? Also, please do let me know if this is not the right approach.
The way you iterate over Subnetlist is incorrect - you only get the value of "list" key, ending up with a bundle of subnets instead of individual items. Make it a map of individual subnet objects instead:
variable "Subnetlist" {
type = map(object({
name = string
address = string
}))
}
Then pass it in tfvars like:
Subnetlist = {
"s1" = { name = "TESTSUBNET", address = "10.0.1.0/24" },
"s2" = { name = "TESTSUBNET1", address = "10.0.2.0/24" },
"s3" = { name = "TESTSUBNET2", address = "10.0.3.0/24" }
}
Finally consume it in the module like this:
resource "azurerm_subnet" "SUBNETS" {
for_each = var.Subnetlist
name = each.value.name
address_prefixes = each.value.address
resource_group_name = var.resource_group_name
virtual_network_name = var.virtual_network_name
}

Referencing a created subnet and associate it with a given EIP for a NLB (aws provider)

I'm trying to parametrize the creation of a NLB, and provision in the same plan the necessary public subnets.
The subnets are specified as a variable of the plan:
variable "nlb_public_subnets" {
type = list(object({
name = string
network_number = number
availability_zone = string
elastic_ip = string
}))
default = [
{
name = "sftp_sub_A"
network_number = 1
availability_zone = "eu-west-1a"
elastic_ip = "X.Y.Z.T"
},
{
name = "sftp_sub_B"
network_number = 2
availability_zone = "eu-west-1b"
elastic_ip = "XX.YY.ZZ.TT"
}
]
}
variable "common_tags" {
description = "A map containing the common tags to apply to all resources"
type = map(string)
default = {}
}
locals {
vpc_id = "dummy"
base_cidr = "10.85.23.0/24"
publicSubnets = { for s in var.nlb_public_subnets :
s.name => {
name = s.name
cidr_block = cidrsubnet(var.base_public_subnet_cidr_block, 6,
s.network_number )
availability_zone = s.availability_zone
elastic_ip = s.elastic_ip
}
}
}
I'm specifying a name, a network number (to compute the cidr block), an availability zone, and an elastic IP to map to when creating the NLB.
Here I'm creating the subnets:
#Comment added after solution was given
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
resource "aws_subnet" "sftp_nlb_subnets" {
for_each = { for subnet in local.publicSubnets :
subnet.name => subnet
}
cidr_block = each.value.cidr_block
vpc_id = local.vpc_id
availability_zone = each.value.availability_zone
tags = {
Name = each.key
Visibility = "public"
Purpose = "NLB"
}
}
Now I need to create my NLB, and this is where I'm struggling on how to associate the freshly created subnets with the Elastic IP provided in the configuration:
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets: subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags,{
Name=var.name
})
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = ????Help???
}
}
}
Could I somehow look up the configuration object with the help of the subnet name in the tags?
UPDATE1
Updated the dynamic block, as it had a typo.
UPDATE2
#tmatilai nailed the answer!
Here's the modified aws_lb block:
#
#This will result in a Map indexed by subnet.name provided in var.nlb_public_subnets
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
name = var.name
internal = false
load_balancer_type = "network"
subnets = [for subnet in aws_subnet.sftp_nlb_subnets : subnet.id]
enable_deletion_protection = true
tags = merge(var.common_tags, {
Name = var.name
})
dynamic "subnet_mapping" {
#subnet_mapping.key will contain subnet.name, so we can use it to access the Map data.aws_eip.nlb (also indexed by subnet.name) to get the eip allocation_id
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
The trick is to realize that both aws_subnet.sftp_nlb_subnets and data.aws_eip.nlb are a Map, indexed by the key of local.publicSubnets. This allows us to use this common key (the subnet name) in the map aws_subnet.sftp to look up information in the data (data.aws_eip.nlb) obtained from the original input, local.publicSubnets.
Thanks. This is a neat trick.
Passing the IP address of the elastic IPs sounds strange. If you create the EIPs elsewhere, why not pass the (allocation) ID of them instead?
But with this setup, you can get the allocation ID with the aws_eip data source:
data "aws_eip" "nlb" {
for_each = local.publicSubnets
public_ip = each.value.elastic_ip
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = data.aws_eip.nlb[subnet_mapping.key].id
}
}
}
But maybe it would make more sense to create the EIPs also here. For example something like this:
resource "aws_eip" "nlb" {
for_each = local.publicSubnets
vpc = true
}
resource "aws_lb" "sftp" {
# ...
dynamic "subnet_mapping" {
for_each = aws_subnet.sftp_nlb_subnets
content {
subnet_id = subnet_mapping.value.id
allocation_id = aws_eip.nlb[subnet_mapping.key].id
}
}
}

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"]
}

Resources