Ankit Singh
Ankit Singh

Reputation: 363

Automatically create a subnet for each AWS availability zone in Terraform

Is there a better way to optime the code below so I don't have to ask for availability zone again and again instead can do it in once. as the region is variable so I cant define hardcoded availability zone. can you guys please I want my public subnets to be /24

provider "aws" {
    region = var.region
}

resource "aws_vpc" "app_vpc" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags = {
    Name = var.vpc_name
  }
}


data "aws_availability_zones" "available" {
  state = "available"
}

#provision public subnet
resource "aws_subnet" "public_subnet_01" {
  vpc_id     = aws_vpc.app_vpc.id
  cidr_block = var.public_subnet_01
  availability_zone = data.aws_availability_zones.available.names[0]
  tags = {
    Name = "public_subnet_01"
  }
  depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}
resource "aws_subnet" "public_subnet_02" {
  vpc_id     = aws_vpc.app_vpc.id
  cidr_block = var.public_subnet_02
  availability_zone = data.aws_availability_zones.available.names[1]
  tags = {
    Name = "public_subnet_02"
  }
  depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}
resource "aws_subnet" "public_subnet_03" {
  vpc_id     = aws_vpc.app_vpc.id
  cidr_block = var.public_subnet_03
  availability_zone = data.aws_availability_zones.available.names[2]
  tags = {
    Name = "public_subnet_03"
  }
  depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}

Upvotes: 8

Views: 10610

Answers (2)

Martin Atkins
Martin Atkins

Reputation: 74055

An important hazard to consider with the aws_availability_zones data source is that the set of available zones can change over time, and so it's important to write your configuration so that you don't find yourself trapped in a situation where Terraform thinks you intend to replace a subnet that you are currently using and therefore cannot destroy.

A key part of that is ensuring that Terraform understands that each of the subnets belongs to a specific availability zone, so that when the set of availability zones changes Terraform can either add a new subnet for a new availability zone or remove an existing subnet for a now-removed availability zone, without affecting the others that haven't changed. The easiest way to achieve that is to use resource for_each with the set of availability zones:

resource "aws_subnet" "public" {
  for_each = aws_avaiability_zones.available.names

  # ...
}

The above will declare subnet instances with addresses that each include the availability zone name, like this:

  • aws_subnet.public["eu-west-1a"]
  • aws_subnet.public["eu-west-1b"]
  • aws_subnet.public["eu-west-1e"]

Because they are identified by the availability zone name, Terraform can see that each subnet belongs to a particular availability zone.

For subnets in particular there is an additional challenge: we must assign each subnet its own CIDR block, which means we need a systematic way of allocating IP address space to availability zones so that the networks won't get renumbered by future changes to the set of availability zones.

The documentation for the aws_availability_zone data source includes an example of declaring a mapping table that assigns each region and each availability zone a number between 1 and 14 which is then used to populate one of the octets of the IP address to create a separate prefix per (region, AZ) pair. That example creates only a single VPC and a single subnet, but we can expand on that by using for_each to do it for each of the availability zones, as long as we update the mapping tables whenever we use a new region or a new availability zone suffix letter is assigned (up to 14 of each):

variable "region_number" {
  # Arbitrary mapping of region name to number to use in
  # a VPC's CIDR prefix.
  default = {
    us-east-1      = 1
    us-west-1      = 2
    us-west-2      = 3
    eu-central-1   = 4
    ap-northeast-1 = 5
  }
}

variable "az_number" {
  # Assign a number to each AZ letter used in our configuration
  default = {
    a = 1
    b = 2
    c = 3
    d = 4
    e = 5
    f = 6
    # and so on, up to n = 14 if that many letters are assigned
  }
}

data "aws_region" "current" {}

# Determine all of the available availability zones in the
# current AWS region.
data "aws_availability_zones" "available" {
  state = "available"
}

# This additional data source determines some additional
# details about each VPC, including its suffix letter.
data "aws_availability_zone" "all" {
  for_each = aws_avaiability_zones.available.names

  name = each.key
}

# A single VPC for the region
resource "aws_vpc" "example" {
  cidr_block = cidrsubnet("10.1.0.0/16", 4, var.region_number[data.aws_region.current.name])
}

# A subnet for each availability zone in the region.
resource "aws_subnet" "example" {
  for_each = aws_availability_zone.all

  vpc_id            = aws_vpc.example.id
  availability_zone = each.key
  cidr_block        = cidrsubnet(aws_vpc.example.cidr_block, 4, var.az_number[each.value.name_suffix])
}

For example, if we were working in us-west-2 and there were availability zones us-west-2a and us-west-2c, the above would declare:

  • A single aws_vpc.example with CIDR block 10.1.48.0/20, where 48 is the decimal representation of hex 0x30, where 3 is the number for us-west-2.
  • A subnet aws_subnet.example["us-west-2a"] in us-west-2a with CIDR block 10.1.49.0/24, where 49 is the decimal representation of hex 0x31.
  • A subnet aws_subnet.example["us-west-2c"] in us-west-2c with CIDR block 10.1.51.0/24, where 51 is the decimal representation of hex 0x33.

Notice that there is no subnet for 10.1.50.0/24, because 50 (hex 0x32) is reserved for a hypothetical us-west-2b. By allocating these addresses statically by subnet letter we can ensure that they will not change over time as availability zones are added and removed.

Upvotes: 9

Marcin
Marcin

Reputation: 238051

You can automate creation of subnets using count and cidrsubnets.

An example would be:

resource "aws_subnet" "public_subnet" {

  count             = length(data.aws_availability_zones.available.names)

  vpc_id            = aws_vpc.app_vpc.id
  cidr_block        = cidrsubnet(aws_vpc.app_vpc.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]
  
  tags = {
    Name = "public_subnet_${count.index}"
  }
  
  depends_on = [aws_vpc_dhcp_options_association.dns_resolver]
}

The above will automatically create subnet in each AZ as well as assing cidr block (/24, assuming that vpc is /16) to it.

Upvotes: 4

Related Questions