Dynamically Construct VPCs with Multiple Subnets (for_each limitations)

I’ve got an overall goal of constructing a clean way to describe, then provision a VPC structure in (variable or local) data. Usually this is done with the VPC module, but I’d prefer to avoid modules, and that particular one because of some tagging issues.

The for_each iterator seems ideal, but there’s a problem, in that we want to iterate over vpc’s (easy) then for each vpc iterate over subnets (not so easy because for_each does not nest).

I’ve been able to work around the issue in a clumsy way by provisioning a separate subnet resource for a known number of subnets, but wonder if there is a more dynamic way to accomplish the goal.

The example below works, but is clumsy and will get worse when we add routing tables, etc.

variable "vpcs" {
  default = {
    vpc1 = {
      vpc_name                          = "vpc1"
      vpc_cidr                  = "10.1.100.0/25"
      vpc_az_ids                = ["usw2-az1","usw2-az3"]
      vpc_private_subnets = ["10.1.100.0/26","10.1.100.64/26"]
    }
    vpc2 = {
      vpc_name            = "vpc2"
      vpc_cidr            = "10.1.200.0/25"
      vpc_az_ids          = ["usw2-az1","usw2-az3"]
      vpc_private_subnets = ["10.1.200.0/26","10.1.200.64/26"]
    }
  }
}

resource "aws_vpc" "main" {
        for_each = var.vpcs
  cidr_block       = each.value.vpc_cidr
  instance_tenancy = "default"

  tags = {
    Name = each.value.vpc_name
  }
}

resource "aws_subnet" "subnet1" {
        for_each = var.vpcs
        cidr_block = each.value.vpc_private_subnets[0]
        vpc_id = aws_vpc.main[each.key].id
}

resource "aws_subnet" "subnet2" {
  for_each = var.vpcs
  cidr_block = each.value.vpc_private_subnets[1]
  vpc_id = aws_vpc.main[each.key].id
}


output "vpc1_name" {
        value = var.vpcs.vpc1.vpc_name
}
output "vpc1_cidr" {
        value = var.vpcs.vpc1.vpc_cidr
}
output "vpc2_name" {
  value = var.vpcs.vpc2.vpc_name
}
output "vpc2_cidr" {
  value = var.vpcs.vpc2.vpc_cidr
}

The output is correct; the implementation clumsy:

Terraform will perform the following actions:

  # aws_subnet.subnet1["vpc1"] will be created
  + resource "aws_subnet" "subnet1" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = (known after apply)
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "10.1.100.0/26"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = false
      + owner_id                        = (known after apply)
      + tags_all                        = (known after apply)
      + vpc_id                          = (known after apply)
    }

  # aws_subnet.subnet1["vpc2"] will be created
  + resource "aws_subnet" "subnet1" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = (known after apply)
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "10.1.200.0/26"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = false
      + owner_id                        = (known after apply)
      + tags_all                        = (known after apply)
      + vpc_id                          = (known after apply)
    }

  # aws_subnet.subnet2["vpc1"] will be created
  + resource "aws_subnet" "subnet2" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = (known after apply)
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "10.1.100.64/26"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = false
      + owner_id                        = (known after apply)
      + tags_all                        = (known after apply)
      + vpc_id                          = (known after apply)
    }

  # aws_subnet.subnet2["vpc2"] will be created
  + resource "aws_subnet" "subnet2" {
      + arn                             = (known after apply)
      + assign_ipv6_address_on_creation = false
      + availability_zone               = (known after apply)
      + availability_zone_id            = (known after apply)
      + cidr_block                      = "10.1.200.64/26"
      + id                              = (known after apply)
      + ipv6_cidr_block_association_id  = (known after apply)
      + map_public_ip_on_launch         = false
      + owner_id                        = (known after apply)
      + tags_all                        = (known after apply)
      + vpc_id                          = (known after apply)
    }

  # aws_vpc.main["vpc1"] will be created
  + resource "aws_vpc" "main" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "10.1.100.0/25"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = (known after apply)
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name" = "vpc1"
        }
      + tags_all                         = {
          + "Name" = "vpc1"
        }
    }

  # aws_vpc.main["vpc2"] will be created
  + resource "aws_vpc" "main" {
      + arn                              = (known after apply)
      + assign_generated_ipv6_cidr_block = false
      + cidr_block                       = "10.1.200.0/25"
      + default_network_acl_id           = (known after apply)
      + default_route_table_id           = (known after apply)
      + default_security_group_id        = (known after apply)
      + dhcp_options_id                  = (known after apply)
      + enable_classiclink               = (known after apply)
      + enable_classiclink_dns_support   = (known after apply)
      + enable_dns_hostnames             = (known after apply)
      + enable_dns_support               = true
      + id                               = (known after apply)
      + instance_tenancy                 = "default"
      + ipv6_association_id              = (known after apply)
      + ipv6_cidr_block                  = (known after apply)
      + main_route_table_id              = (known after apply)
      + owner_id                         = (known after apply)
      + tags                             = {
          + "Name" = "vpc2"
        }
      + tags_all                         = {
          + "Name" = "vpc2"
        }
    }

Plan: 6 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + vpc1_cidr = "10.1.100.0/25"
  + vpc1_name = "vpc1"
  + vpc2_cidr = "10.1.200.0/25"
  + vpc2_name = "vpc2"