Terraform data sources let you dynamically fetch data from APIs or other Terraform state backends. Examples of data sources include machine image IDs from a cloud provider or Terraform outputs from other configurations. Data sources make your configuration more flexible and dynamic and let you reference values from other configurations, helping you scope your configuration while still referencing any dependent resource attributes. In HCP Terraform, data sources let you share data between workspaces.
In this tutorial, you will use data sources to make your configuration more dynamic. First, you will use Terraform to create an AWS VPC and security groups. Next, you will use the awsavailabilityzones data source to make your configuration deployable across any region. You will then deploy application infrastructure defined by a separate Terraform configuration, and use the terraformremotestate data source to query information about your VPC. Finally, you will use the aws_ami data source to configure the correct AMI for the current region.
You can complete this tutorial using the same workflow with either Terraform Community Edition or HCP Terraform. HCP Terraform is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.
This tutorial assumes that you are familiar with the Terraform and HCP Terraform workflows
Understanding the awsavailabilityzones Data Source
The awsavailabilityzones 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.
The data source is accessed with the pattern data.
The state argument accepts the value available. Setting state = "available" ensures that only availability zones that are currently in an available state are returned by the AWS API. The impact for an operator is that Terraform will not attempt to place resources into a zone that AWS reports as impaired or unavailable, which prevents plan failures at apply time due to zone unavailability.
A basic declaration is:
hcl
data "aws_availability_zones" "available" {
state = "available"
}
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 contextual connection to VPC design is that the number of zones returned by this data source directly controls the count of subnets that can be created in a multi-AZ deployment. Because the data source is evaluated during plan, the infrastructure adapts automatically to the target region without manual edits.
Basic Discovery and Output
Basic Availability Zone Discovery is the simplest usage pattern.
hcl
data "aws_availability_zones" "available" {
state = "available"
}
output "available_zones" {
value = data.aws_availability_zones.available.names
}
This pattern exposes the names attribute as an output. The real-world consequence is that teams can inspect which zones Terraform sees before provisioning dependent resources. The output can be consumed by other workspaces via terraformremotestate, which is the mechanism used to share data between workspaces in HCP Terraform.
A more detailed output includes both names and zone_ids.
hcl
output "zone_details" {
value = {
names = data.aws_availability_zones.available.names
zone_ids = data.aws_availability_zones.available.zone_ids
}
}
The zone_ids attribute provides the physical zone identifiers. Having both attributes allows configurations to reference zones by name for readability and by ID when AWS APIs require physical identifiers.
Integrating with VPC Modules
Add the following to main.tf
hcl
data "aws_availability_zones" "available" {
state = "available"
filter {
name = "zone-type"
values = ["availability-zone"]
}
}
The filter block with name = "zone-type" and values = ["availability-zone"] further constrains the result set to standard availability zones. This is important because AWS listings can include other zone types that are not suitable for general workload placement.
You can reference data source attributes with the pattern data.
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)
}
The module receives azs from the data source. The impact is that the same module call can be applied to us-east-1 and ap-southeast-1 without changing the azs argument. The configuration becomes portable across regions.
Configure the VPC workspace to output the region, which the application workspace will require as an input. This cross-workspace sharing is enabled by HCP Terraform remote state and execution features.
Multi-AZ Subnet Patterns
Creating Subnets Across All Available AZs demonstrates dynamic count.
hcl
data "aws_availability_zones" "available" {
state = "available"
}
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
tags = {
Name = "main-vpc"
}
}
resource "aws_subnet" "private" {
count = length(data.aws_availability_zones.available.names)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = data.aws_availability_zones.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.
The impact layer for the operator is that no manual count adjustment is required when moving between regions. The contextual layer is that count = length(data.awsavailabilityzones.available.names) couples subnet creation directly to the data source result, so any change in AWS zone availability triggers a corresponding Terraform plan change.
Limiting the Number of AZs
Most applications do not need subnets in every AZ. Three is usually sufficient for high availability.
Limiting is achieved with locals and slice.
hcl
data "aws_availability_zones" "available" {
state = "available"
}
locals {
# Use at most 3 AZs
az_count = min(3, length(data.aws_availability_zones.available.names))
azs = slice(data.aws_availability_zones.available.names, 0, local.az_count)
}
The min function caps the number of zones used. The slice function selects the first N names. This pattern prevents over-provisioning in regions with many zones while still allowing full use in regions with fewer zones.
Subnets are then created with count = local.az_count.
hcl
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "public-${local.azs[count.index]}"
Tier = "public"
}
}
resource "aws_subnet" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + local.az_count)
availability_zone = local.azs[count.index]
tags = {
Name = "private-${local.azs[count.index]}"
Tier = "private"
}
}
The real-world consequence is cost control and reduced operational complexity. The contextual connection is that the same locals block can be reused for public, private, and database tiers by offsetting the cidrsubnet index.
Filtering Opt-In Status and Zone Types
AWS has introduced Local Zones and Wavelength Zones, which show up in AZ listings but are not standard AZs. Filter them out.
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. Filtering by opt-in-status prevents Terraform from selecting zones that require explicit opt-in from the AWS account, which would cause apply failures.
A production-ready pattern combines opt-in filtering with locals.
hcl
data "aws_availability_zones" "available" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
locals {
az_count = min(var.az_count, length(data.aws_availability_zones.available.names))
azs = slice(data.aws_availability_zones.available.names, 0, local.az_count)
}
The impact is that deployments remain valid even when AWS adds new zone types to the region. The configuration will only select standard, opt-in-not-required zones.
Excluding Specific Physical Zones
It also fails if one of those zones is not available for your account. Use excludezoneids when you need to exclude a specific physical zone.
hcl
data "aws_availability_zones" "available" {
state = "available"
exclude_zone_ids = ["use1-az3"]
}
The excludezoneids argument removes a specific physical zone from the result set. This is useful when a particular zone is known to be unavailable for the account or when a team wants to avoid a zone due to operational experience.
The contextual layer is that excludezoneids works in combination with state and filter blocks, allowing fine-grained control over the zone set before it is passed to resources.
Production-Ready Multi-AZ VPC Pattern
Here is a production-ready pattern for a multi-AZ VPC.
hcl
data "aws_availability_zones" "available" {
state = "available"
filter {
name = "opt-in-status"
values = ["opt-in-not-required"]
}
}
locals {
az_count = min(var.az_count, length(data.aws_availability_zones.available.names))
azs = slice(data.aws_availability_zones.available.names, 0, local.az_count)
}
variable "az_count" {
description = "Number of availability zones to use"
type = number
default = 3
}
variable "project" {
description = "Project name used for resource tags"
type = string
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.project}-vpc"
}
}
Public subnets - one per AZ
hcl
resource "aws_subnet" "public" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = local.azs[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project}-public-${local.azs[count.index]}"
Tier = "public"
}
}
Private subnets - one per AZ
hcl
resource "aws_subnet" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + local.az_count)
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project}-private-${local.azs[count.index]}"
Tier = "private"
}
}
Database subnets - one per AZ
hcl
resource "aws_subnet" "database" {
count = local.az_count
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + (local.az_count * 2))
availability_zone = local.azs[count.index]
tags = {
Name = "${var.project}-database-${local.azs[count.index]}"
Tier = "database"
}
}
Internet Gateway
hcl
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project}-igw"
}
}
NAT Gateways - one per AZ for high availability
hcl
resource "aws_eip" "nat" {
count = local.az_count
domain = "vpc"
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
}
resource "aws_nat_gateway" "main" {
count = local.az_count
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.project}-nat-${local.azs[count.index]}"
}
depends_on = [aws_internet_gateway.main]
}
Route tables for private subnets
hcl
resource "aws_route_table" "private" {
count = local.az_count
vpc_id = aws_vpc.main.id
}
The pattern provides one public, private, and database subnet per selected AZ. The impact for operations is high availability with AZ failure isolation. The contextual connection is that NAT Gateways are placed in public subnets per AZ, ensuring private subnets retain outbound connectivity even if a single AZ is impaired.
Dynamic Portability Across Regions
Dynamic AZ discovery makes your Terraform configurations portable across regions and resilient to zone changes.
The combination of data.awsavailabilityzones.available.names with locals for az_count and slice ensures that the same configuration produces the correct number of subnets in us-east-1 and ap-southeast-1. The configuration adapts automatically.
Attributes available from the data source include:
| Attribute | Description |
| names | List of availability zone names in the current region |
| zone_ids | List of physical zone identifiers for the availability zones |
Using names for availabilityzone arguments and zoneids for filtering or exclusion provides complete coverage of the zone metadata.
Data Sources and Workspace Sharing in HCP Terraform
Terraform data sources let you dynamically fetch data from APIs or other Terraform state backends. Examples of data sources include machine image IDs from a cloud provider or Terraform outputs from other configurations. Data sources make your configuration more flexible and dynamic and let you reference values from other configurations, helping you scope your configuration while still referencing any dependent resource attributes.
In HCP Terraform, data sources let you share data between workspaces. The terraformremotestate data source can query information about a VPC created in a separate workspace, and the awsavailabilityzones data source can be used in the VPC workspace to make the configuration deployable across any region.
The workflow described in the tutorial creates an AWS VPC and security groups, uses awsavailabilityzones to make the configuration deployable across any region, then deploys application infrastructure defined by a separate Terraform configuration using terraformremotestate to query VPC information, and finally uses the aws_ami data source to configure the correct AMI for the current region.
This separation of concerns allows the networking layer to be managed independently from application layer while still maintaining a dynamic dependency on region-specific data.
Conclusion
Terraform AWS availability zone discovery is built around the awsavailabilityzones data source with state = "available" as the baseline filter. Adding filter blocks for zone-type and opt-in-status removes non-standard zones and opt-in required zones from the result set. The names and zone_ids attributes provide the inputs needed for subnet and resource placement.
Limiting the number of AZs with min and slice prevents over-provisioning and keeps costs predictable. Excluding specific physical zones with excludezoneids handles account-specific unavailability. Combining these techniques with count-based subnet creation and per-AZ NAT Gateway patterns yields a production-ready multi-AZ VPC that is portable across regions and resilient to zone changes.
The use of data sources for dynamic discovery, combined with HCP Terraform workspace sharing via terraformremotestate, creates a configuration graph where networking outputs feed application inputs without hard-coding region-specific values. This approach is the foundation for repeatable, region-agnostic Terraform deployments on AWS.