Terraform Availability Zone Placement in Azure VMs and AWS Discovery Patterns

High availability is a consideration that every Architect needs to think about when building cloud infrastructure. In Azure, Availability Zones help you achieve resiliency by distributing your resources across physically separate locations within a region. This ensures that even if one zone experiences downtime, your resources remain available. Terraform, as an Infrastructure-as-Code tool, makes deploying these resources simple, repeatable, and allows you to version control deployments. The same principle of zone awareness extends to AWS where dynamic discovery of availability zones enables portable configurations across regions and resilience to zone changes. The following material explores the concrete Terraform patterns that surface in the reference implementations for both Azure VM zone placement and AWS availability zone discovery, with emphasis on how each configuration choice translates into operational impact and architectural context.

Azure Availability Zone Concept and Resilience Impact

Availability Zones in Azure are physically separate locations within a region. The reference material frames this as a core resiliency mechanism. Distributing resources across zones means a failure domain is limited to a single zone. For an architect, this translates into a design decision that reduces the blast radius of hardware failures, network partitions, or localized outages. For an operator, the consequence is that a VM placed in a specific zone can be targeted for recovery planning, compliance boundaries, or latency-sensitive workloads that must stay near a particular data center within the region.

Terraform captures this intent declaratively. Rather than clicking through the portal, the zone selection is stored as code, versioned, and repeatable. The reference implementation highlights that deploying VMs in specific Availability Zones is a small Terraform configuration change, but can make a big difference in your resources’ resilience and allow you to follow design choices by your architects. Terraform helps you automate and manage this process efficiently, ensuring your infrastructure is reliable and easy to maintain.

Prerequisites and Toolchain Preparation for Azure Deployment

Before you start, you’ll need:

  • An Azure subscription.
  • Azure CLI is installed on your machine.
  • Terraform is installed on your machine.
  • Basic Terraform knowledge

The impact layer of these prerequisites is direct. An Azure subscription provides the identity and billing context required for resource creation. Azure CLI is required for authentication via Az login. Terraform installed locally enables Terraform init, Terraform plan, and Terraform apply to be executed against the AzureRM provider. Basic Terraform knowledge ensures the variable interpolation, resource references, and module wiring are understood.

In practice, the workflow begins with authentication and preparation:

bash Az login

bash Terraform init

bash Terraform plan

bash Terraform apply -auto-approve

Within a few minutes, the resources will have been deployed and you can SSH using the output information to the Linux VM. And you can also check the Azure portal to check the availability zone deployment.

The repository referenced for the full configuration is:

bash git clone https://github.com/weeyin83/terraform-azure-vm-az.git

The clone operation provides the three-file structure described in the reference: variables.tf, main.tf, and outputs.tf.

Variable Definition and Zone Selection Strategy

Choosing a zone: variables.tf

We’ve defined a variable file, keeping it simple now, with the Azure Subscription ID and Azure Availability Zone in there.

The variable block is:

hcl variable "availability_zone" { description = "Azure availability zone (1, 2, or 3)" type = string default = "1" }

This variable lets you choose which zone your VM should be placed in. Azure identifies zones with string values "1", "2", and "3", so the Terraform variable is a string rather than a number.

The impact of this design is that the zone becomes a first-class parameter of the deployment. Changing the value from "1" to "2" or "3" moves the VM to a different failure domain without rewriting the rest of the infrastructure. The default of "1" provides a safe starting point while still allowing override via tfvars or CLI.

The reference also mentions an additional variable for the subscription context:

azure_subscription_id = "your-subscription-id" availability_zone = "1" # Options: "1", "2", or "3" Specifying your specific information and choices.

The context layer connects this to governance. Hardcoding zone values in variables allows peer review, change tracking, and automated policy checks to enforce zone diversity for critical workloads.

Random Region Selector Implementation

Before we get to the VM, there’s a small but fun addition in main.tf: a random region selector.

hcl locals { azure_regions = [ "ukwest", "westeurope", "francecentral", "swedencentral" ] selected_location = element(local.azure_regions, random_integer.region_index.result) }

hcl resource "random_integer" "region_index" { min = 0 max = length(local.azure_regions) - 1 }

Each time you deploy, Terraform picks a random region from that list. This is something I use more often than not in my demos, but it might not be something you want to do in production, as you will probably need the predictability of where your resources are deployed.

The impact is that demo environments can be created with geographic variety without manual edits. The contextual connection is that the random region selection interacts with zone selection: the list of available zones is region-specific, so the same zone string "1" maps to a different physical location depending on which region is chosen by the random selector.

Main Terraform Resource Composition

The main.tf file might look busy at first glance.

That’s because it handles a complete virtual machine deployment with all the necessary components within the file, such as resource group, virtual network, subnet, network security group (NSG), NIC, virtual machine and also saving the private key for access to the VM.

I’ve included all of this configuration to make it a great starter set for you; however if you already have existing networking, you can plug the VM into that.

The key section we want to focus on is when you are defining the VM configuration, you can define the availability zone that the VM should deploy into:

hcl resource "azurerm_linux_virtual_machine" "vm" { name = module.naming.linux_virtual_machine.name_unique resource_group_name = azurerm_resource_group.rg.name location = azurerm_resource_group.rg.location size = "Standard_B1s" admin_username = "azureuser" network_interface_ids = [ azurerm_network_interface.nic.id ] zone = var.availability_zone admin_ssh_key { username = "azureuser" public_key = tls_private_key.vm_key.public_key_openssh } os_disk { caching = "ReadWrite" storage_account_type = "Standard_LRS" } source_image_reference { publisher = "Canonical" offer = "0001-com-ubuntu-server-jammy" sku = "22_04-lts-gen2" version = "latest" } }

A structured view of the VM specification:

Property Value
Resource Type azurermlinuxvirtual_machine
Size Standard_B1s
Admin Username azureuser
Zone var.availability_zone
OS Disk Caching ReadWrite
Storage Account Type Standard_LRS
Image Publisher Canonical
Image Offer 0001-com-ubuntu-server-jammy
Image SKU 22_04-lts-gen2
Image Version latest

The impact of setting zone = var.availability_zone is that the VM is pinned to a specific failure domain at creation time. Changing the variable forces a replacement of the VM, which is a significant operational event. The context layer ties this to the networking components defined in the same file: the NIC, subnet, and resource group must exist in the same region, and for zone-enabled subnets, the subnet must be zone-enabled for the zone placement to succeed.

Deployment Workflow Commands

To deploy this Terraform configuration, follow these steps:

Open a terminal and download the code to your machine:

bash git clone https://github.com/weeyin83/terraform-azure-vm-az.git

Open up your favourite editor, I like VS Code, and open up the Terraform folder

Add your details to it:

azure_subscription_id = "your-subscription-id" availability_zone = "1" # Options: "1", "2", or "3"

Specifying your specific information and choices.

Once you’ve saved the file. Open up your terminal again, this time you need to log in to your Azure subscription.

bash Az login

Once logged in you can start the Terraform deployment. Prepare Terraform and download provider modules:

bash Terraform init

Then we can run the plan command to understand what the Terraform configuration will do:

bash Terraform plan

Once you are happy, you can confirm the deployment using:

bash Terraform apply -auto-approve

The workflow ensures authentication, dependency resolution, change preview, and automated apply. The impact is reduced manual error and an auditable history of who applied which zone selection.

AWS Availability Zone Data Source Discovery

Dynamic AZ discovery makes your Terraform configurations portable across regions and resilient to zone changes.

The core data source used is:

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

Outputting the discovered values enables downstream consumption:

hcl output "zone_details" { value = { names = data.aws_availability_zones.available.names zone_ids = data.aws_availability_zones.available.zone_ids } }

The impact is portability. Rather than hardcoding us-east-1a, the configuration queries AWS for currently available zones in the selected provider region. If AWS adds or decommissions zones, the configuration adapts without code changes. The context layer connects this to multi-AZ architectures where subnet creation, instance placement, and volume attachment must share a common zone reference.

Excluding Specific Zones and Filtering

Use excludezoneids when you need to exclude a specific physical zone:

hcl data "aws_availability_zones" "available" { state = "available" exclude_zone_ids = ["use1-az3"] # Exclude a specific physical zone }

This pattern allows operators to avoid a zone with known maintenance, degraded performance, or compliance restrictions. The impact is fine-grained control without abandoning dynamic discovery.

A common filter for opt-in zones is:

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

This ensures only zones that do not require explicit opt-in are selected, avoiding deployment failures in regions where certain zones are gated.

Complete Multi-AZ VPC Pattern Construction

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"] } }

hcl 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) }

Variables used:

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

hcl variable "project" { description = "Project name used for resource tags" type = string }

hcl variable "vpc_cidr" { description = "VPC CIDR block" type = string default = "10.0.0.0/16" }

VPC resource:

hcl 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 =

The pattern ensures each subnet is created in a distinct AZ derived from dynamic discovery. The impact is high availability for both public and private tiers. The context layer links local.az_count to length(data.aws_availability_zones.available.names) to prevent requesting more AZs than exist in the region, and slice provides deterministic ordering.

Pre-existing Subnet Availability Zone Retrieval

A common question is how to find the availability zone for a specific pre-existing subnet.

My Current Sample code is as below:

hcl terraform { required_providers { aws = { source = “hashicorp/aws” version = “3.63.0” } } }

hcl provider “aws” { Configuration options access_key = “{var.access_key}" secret_key = "{var.secret_key}” profile = “default” region = “${var.region}” }

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

hcl resource “aws_instance” “default” { availability_zone = data.aws_availability_zones.available.names[0] ami = “{var.ami_id}" instance_type = "{var.instance_type}” key_name = “{var.key_pair}" subnet_id = "{var.subnet_id}” tags = { Owner = “Siva” Name = “${var.instance_name}” } }

hcl resource “aws_security_group” “default” { name = “{var.sgname}" description = "Allow TLS inbound traffic" vpc_id = "{var.vpc_id}” tags = { Owner = “Siva” Name = “${var.instance_name}” } }

hcl resource “aws_ebs_volume” “default” { availability_zone = aws_instance.default.availability_zone size = “{var.volume_size}" type = "{var.volume_type}” tags = { Owner = “Siva” Name = “${var.instance_name}” } }

hcl resource “aws_volume_attachment” “default” { device_name = “/dev/sdh” volume_id = aws_ebs_volume.default.id instance_id = aws_instance.default.id }

The data implementation of the aws-subnet provides an availability_zone return attribute: Terraform Registry.

The impact of this is that when a subnet is already provisioned, referencing its availability_zone attribute avoids the need to query the availability zones data source manually. The instance and EBS volume can be pinned to the same zone as the subnet, ensuring compatibility. The context layer shows a chain: aws_instance.default.availability_zone is derived from the data source selection, and the EBS volume must share that zone, otherwise attachment fails.

Conclusion

The reference implementations demonstrate two complementary philosophies for Terraform availability zone handling. In Azure, the zone is a user-supplied string parameter, typically "1", "2", or "3", passed via a variable into azurerm_linux_virtual_machine with zone = var.availability_zone. This explicit pinning supports architectural mandates for placement in a specific failure domain and integrates with a full VM deployment stack that includes resource group, virtual network, subnet, NSG, NIC, and private key handling. The inclusion of a random region selector illustrates how zone selection is region-bound and how demo variability can be introduced without sacrificing declarative control.

In AWS, the pattern shifts to dynamic discovery via data "aws_availability_zones" "available" with state = "available", optional filter for opt-in status, and optional exclude_zone_ids. The discovered names and zone_ids are exposed as outputs and fed into locals that calculate az_count and azs. This enables multi-AZ VPC construction where public and private subnets are created with count and availability_zone = local.azs[count.index], guaranteeing spread across real zones without hardcoding. The pattern also shows how an instance can be placed in data.aws_availability_zones.available.names[0] and how an EBS volume must share the instance's zone via availability_zone = aws_instance.default.availability_zone.

Together, these patterns illustrate the operational trade-offs between explicit zone pinning for compliance and repeatability in Azure and dynamic discovery for portability and resilience in AWS. Both rely on Terraform's data sources and variable mechanisms to keep zone decisions auditable, versioned, and repeatable across teams.

Sources

  1. Techie Lass
  2. OneUptime
  3. HashiCorp Discuss

Related Posts