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.awsavailability_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.awsavailabilityzones.available.names
zoneids = 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.awsavailabilityzones.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" {
cidrblock = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "awssubnet" "private" {
count = length(data.awsavailabilityzones.available.names)
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(awsvpc.main.cidrblock, 8, count.index)
availabilityzone = data.awsavailabilityzones.available.names[count.index]
tags = {
Name = "private-${data.awsavailability_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.azcount
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(awsvpc.main.cidrblock, 8, count.index)
availabilityzone = local.azs[count.index]
mappubliciponlaunch = true
tags = {
Name = "public-${local.azs[count.index]}"
Tier = "public"
}
}
resource "awssubnet" "private" {
count = local.azcount
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" {
cidrblock = var.vpccidr
enablednssupport = true
enabledns_hostnames = true
tags = {
Name = "${var.project}-vpc"
}
}
resource "awssubnet" "public" {
count = local.azcount
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index)
availabilityzone = local.azs[count.index]
mappublicipon_launch = true
tags = {
Name = "${var.project}-public-${local.azs[count.index]}"
Tier = "public"
}
}
resource "awssubnet" "private" {
count = local.azcount
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index + local.azcount)
availabilityzone = local.azs[count.index]
tags = {
Name = "${var.project}-private-${local.azs[count.index]}"
Tier = "private"
}
}
resource "awssubnet" "database" {
count = local.azcount
vpcid = awsvpc.main.id
cidrblock = cidrsubnet(var.vpccidr, 8, count.index + (local.azcount * 2))
availabilityzone = 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.azcount
domain = "vpc"
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
}
resource "awsnatgateway" "main" {
count = local.azcount
allocationid = awseip.nat[count.index].id
subnetid = awssubnet.public[count.index].id
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
dependson = [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-statusandzone-typeprevents 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.