Dynamic Availability Zone Discovery With Terraform AWS Provider

Terraform configurations that hard code availability zone names break the moment you change regions or AWS adds or removes zones. Dynamic AZ discovery with the aws_availability_zones data source makes configurations portable across regions and resilient to zone changes. The data source retrieves a list of availability zones based on arguments supplied, and the state argument limits the results to only those that are currently available. Once discovered, the names and zone IDs can be referenced with the pattern data.<NAME>.<ATTRIBUTE> and used to drive subnet, NAT gateway, and VPC module inputs.

Basic Discovery Pattern

The simplest form of discovery requests all available zones in the current region.

```hcl
data "awsavailabilityzones" "available" {
state = "available"
}

output "availablezones" {
value = data.aws
availability_zones.available.names
}
```

This returns a list of all available AZ names in the current region, like ["us-east-1a", "us-east-1b", "us-east-1c", "us-east-1d", "us-east-1e", "us-east-1f"]. The output can be extended to expose both names and zone IDs.

```hcl
data "awsavailabilityzones" "available" {
state = "available"
}

output "zonedetails" {
value = {
names = data.aws
availabilityzones.available.names
zone
ids = data.awsavailabilityzones.available.zone_ids
}
}
```

The data source is part of the AWS provider and retrieves a list of availability zones based on the arguments supplied. In this case, the state argument limits the availability zones to only those that are currently available.

Filtering For Standard Zones Only

AWS has introduced Local Zones and Wavelength Zones, which show up in AZ listings but are not standard AZs. Filtering them out is required for standard VPC subnet design.

hcl data "aws_availability_zones" "available" { state = "available" filter { name = "opt-in-status" values = ["opt-in-not-required"] } }

The opt-in-not-required status means standard AZs. Using this filter ensures that Local Zones and Wavelength Zones are excluded from the result set.

A more explicit filter by zone type can also be applied:

hcl data "aws_availability_zones" "available" { state = "available" filter { name = "zone-type" values = ["availability-zone"] } }

Filtering is used when you need to guarantee that only traditional availability zones are considered for subnet placement.

Excluding Specific Physical Zones

Sometimes an account has restrictions or you want to avoid a specific physical zone. The data source supports exclude_zone_ids for this purpose.

hcl data "aws_availability_zones" "available" { state = "available" exclude_zone_ids = ["use1-az3"] }

Use exclude_zone_ids when you need to exclude a specific physical zone. The exclusion is evaluated after the state filter.

Limiting The Number Of AZs

Most applications do not need subnets in every AZ. Three is usually sufficient for high availability. Deploy to us-east-1 and you get 6 subnets. Deploy to ap-southeast-1 and you get 3. The infrastructure adapts automatically.

A limiting pattern uses locals with min and slice:

```hcl
data "awsavailabilityzones" "available" {
state = "available"
}

locals {
azcount = min(3, length(data.awsavailabilityzones.available.names))
azs = slice(data.aws
availabilityzones.available.names, 0, local.azcount)
}
```

The same logic appears in a production-ready VPC pattern with variables:

```hcl
variable "az_count" {
description = "Number of availability zones to use"
type = number
default = 3
}

data "awsavailabilityzones" "available" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}

locals {
azcount = min(var.azcount, length(data.awsavailabilityzones.available.names))
azs = slice(data.awsavailabilityzones.available.names, 0, local.az_count)
}
```

The az_count local caps the number of zones used while still allowing the configuration to shrink automatically in regions with fewer zones.

Creating Subnets Across All Available AZs

Creating one subnet per discovered AZ ensures the footprint matches the region.

```hcl
data "awsavailabilityzones" "available" {
state = "available"
}

resource "awsvpc" "main" {
cidr
block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}

resource "awssubnet" "private" {
count = length(data.aws
availabilityzones.available.names)
vpc
id = awsvpc.main.id
cidr
block = cidrsubnet(awsvpc.main.cidrblock, 8, count.index)
availabilityzone = data.awsavailabilityzones.available.names[count.index]
tags = {
Name = "private-${data.aws
availability_zones.available.names[count.index]}"
Tier = "private"
}
}
```

This creates one subnet per available AZ, regardless of region. Deploy to us-east-1 and you get 6 subnets. Deploy to ap-southeast-1 and you get 3. The infrastructure adapts automatically.

A dual tier example with public and private subnets:

```hcl
resource "awssubnet" "public" {
count = local.az
count
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(awsvpc.main.cidrblock, 8, count.index)
availability
zone = local.azs[count.index]
mappubliciponlaunch = true
tags = {
Name = "public-${local.azs[count.index]}"
Tier = "public"
}
}

resource "awssubnet" "private" {
count = local.az
count
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(awsvpc.main.cidrblock, 8, count.index + local.azcount)
availability_zone = local.azs[count.index]
tags = {
Name = "private-${local.azs[count.index]}"
Tier = "private"
}
}
```

Both tiers use the same local.azs list so subnets stay aligned across AZs.

Multi-Tier VPC Production Pattern

A complete multi-AZ VPC pattern combines filtering, limiting, and tiered subnets.

```hcl
data "awsavailabilityzones" "available" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}

locals {
azcount = min(var.azcount, length(data.awsavailabilityzones.available.names))
azs = slice(data.awsavailabilityzones.available.names, 0, local.az_count)
}

resource "awsvpc" "main" {
cidr
block = var.vpccidr
enable
dnssupport = true
enable
dns_hostnames = true
tags = {
Name = "${var.project}-vpc"
}
}

resource "awssubnet" "public" {
count = local.az
count
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index)
availabilityzone = local.azs[count.index]
map
publicipon_launch = true
tags = {
Name = "${var.project}-public-${local.azs[count.index]}"
Tier = "public"
}
}

resource "awssubnet" "private" {
count = local.az
count
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index + local.azcount)
availability
zone = local.azs[count.index]
tags = {
Name = "${var.project}-private-${local.azs[count.index]}"
Tier = "private"
}
}

resource "awssubnet" "database" {
count = local.az
count
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index + (local.azcount * 2))
availability
zone = local.azs[count.index]
tags = {
Name = "${var.project}-database-${local.azs[count.index]}"
Tier = "database"
}
}
```

The pattern adds Internet Gateway, NAT Gateways per AZ, and route tables for private subnets. NAT Gateways are created with one per AZ for high availability:

```hcl
resource "awseip" "nat" {
count = local.az
count
domain = "vpc"
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
}

resource "awsnatgateway" "main" {
count = local.azcount
allocation
id = awseip.nat[count.index].id
subnet
id = awssubnet.public[count.index].id
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
depends
on = [awsinternetgateway.main]
}
```

Using The Data Source With VPC Modules

The discovered AZ list integrates directly with community modules. Example with the VPC module:

hcl module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "3.14.0" cidr = var.vpc_cidr_block azs = data.aws_availability_zones.available.names private_subnets = slice(var.private_subnet_cidr_blocks, 0, 2) public_subnets = slice(var.public_subnet_cidr_blocks, 0, 2) }

You can reference data source attributes with the pattern data.<NAME>.<ATTRIBUTE>. Update the VPC configuration to use this data source to set the list of availability zones.

awsavailabilityzone For Single Zone Details

The AWS provider also provides aws_availability_zone for a specific zone.

The aws_availability_zone resource provides details about a specific availability zone (AZ) in the current Region. This can be used both to validate an availability zone given in a variable and to split the AZ name into its component parts of an AWS Region and an AZ identifier letter.

Table of provider data sources

Data Source Purpose Key Attributes
awsavailabilityzones List of AZs in region names, zone_ids
awsavailabilityzone Details of one AZ name, region, identifier

Operational Considerations

  • It also fails if one of those zones is not available for your account. Discovery does not guarantee quota or service availability in every zone.
  • Dynamic AZ discovery makes your Terraform configurations portable across regions and resilient to zone changes.
  • Limiting the number of AZs avoids over-provisioning and reduces costs while retaining high availability.
  • Filtering by opt-in-status and zone-type prevents accidental placement into Local Zones or Wavelength Zones.

Conclusion

Dynamic discovery of availability zones with aws_availability_zones is the foundation for portable and resilient AWS networking in Terraform. By filtering for state = "available" and opt-in-status = "opt-in-not-required", you obtain a reliable set of standard AZs. Limiting with min and slice lets you cap usage to three AZs for high availability while still allowing automatic adaptation when deployed to regions with fewer zones. The discovered list drives subnet counts, NAT gateways, and VPC module inputs without hard coding names. Excluding specific physical zones with exclude_zone_ids and validating individual zones with aws_availability_zone adds additional safety. The result is infrastructure that deploys to us-east-1 with six subnets and to ap-southeast-1 with three subnets using the same configuration, adapting automatically to the target region.

Sources

  1. Terraform Availability Zone Discovery
  2. Terraform Data Sources Tutorial
  3. AWS Availability Zone Provider Documentation

Related Posts