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.