Manages an Azure Availability Set — a resilience primitive that spreads virtual machines across fault and update domains so planned maintenance and localized hardware failures never take down every member at once. Targets
hashicorp/azurerm ~> 4.0.
This module manages a single azurerm_availability_set — the keystone resource, named this — and nothing else.
- 🧱 Creates one availability set in an existing resource group and region.
- 🛡️ Spreads joined virtual machines across fault domains (independent power and network racks) and update domains (planned-maintenance reboot batches).
- 🎛️ Defaults to an aligned (managed) set, the modern shape required by VMs that use managed disks.
- 📌 Optionally co-locates members through a proximity placement group for low-latency networking.
- 🏷️ Carries the universal
tagsandtimeoutstail.
💡 Why it matters: a lone VM has no platform SLA against a single-instance failure. Placing two or more VMs of the same role into one availability set gives Azure the domain topology it needs to keep at least one instance serving during a rack fault or a maintenance wave. It is the classic, region-local alternative to Availability Zones.
If this module saves you time, consider supporting the work:
- ⭐ Star the repository.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
The availability set sits between the resource group that scopes it and the virtual machines that join it. It is the region-local resilience option; Availability Zones are the modern, zone-spanning alternative.
flowchart TD
RG["terraform-azurerm-resource-group"]
AS["terraform-azurerm-availability-set"]
LVM["terraform-azurerm-linux-virtual-machine"]
WVM["terraform-azurerm-windows-virtual-machine"]
AZ["Availability Zones (modern alternative)"]
RG -->|"resource_group_name + location"| AS
AS -->|"id -> availability_set_id"| LVM
AS -->|"id -> availability_set_id"| WVM
AZ -.->|"zone-based alternative to this set"| AS
style AS fill:#0078D4,color:#fff,stroke:#004578,stroke-width:2px
style RG fill:#F5F5F5,color:#222,stroke:#999999
style LVM fill:#F5F5F5,color:#222,stroke:#999999
style WVM fill:#F5F5F5,color:#222,stroke:#999999
style AZ fill:#F5F5F5,color:#222,stroke:#999999
A single keystone resource fed by placement inputs and emitting the identifiers a virtual machine needs to join.
flowchart LR
NAME["name"] --> THIS
RG["resource_group_name"] --> THIS
LOC["location"] --> THIS
MAN["managed = true (aligned)"] --> THIS
UDC["platform_update_domain_count = 5"] --> THIS
FDC["platform_fault_domain_count = 3"] --> THIS
PPG["proximity_placement_group_id (optional)"] --> THIS
subgraph MOD["terraform-azurerm-availability-set"]
THIS["azurerm_availability_set.this"]
end
THIS -->|"id"| OID["output: id"]
THIS -->|"name"| ONAME["output: name"]
style MOD fill:#0078D4,color:#fff,stroke:#004578,stroke-width:2px
style THIS fill:#004578,color:#fff,stroke:#002733,stroke-width:2px
style NAME fill:#F5F5F5,color:#222,stroke:#999999
style RG fill:#F5F5F5,color:#222,stroke:#999999
style LOC fill:#F5F5F5,color:#222,stroke:#999999
style MAN fill:#F5F5F5,color:#222,stroke:#999999
style UDC fill:#F5F5F5,color:#222,stroke:#999999
style FDC fill:#F5F5F5,color:#222,stroke:#999999
style PPG fill:#F5F5F5,color:#222,stroke:#999999
style OID fill:#F5F5F5,color:#222,stroke:#999999
style ONAME fill:#F5F5F5,color:#222,stroke:#999999
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_availability_set.this |
1 | The keystone availability set. |
| Requirement | Value |
|---|---|
| Terraform floor | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, auth, and subscription. |
Schema notes that bite (verified against the live provider schema):
- 🔒
name,resource_group_name, andlocationare force-new — changing any of them replaces the set. - 🔒
managedis force-new — flipping between aligned (true) and classic (false) replaces the set. - 🔒
platform_update_domain_countandplatform_fault_domain_countare force-new — resizing the domain topology replaces the set. - 🔒
proximity_placement_group_idis force-new — attaching, detaching, or changing the PPG replaces the set. - 🏷️ Only
tags(andtimeouts) update in place; everything else is immutable. ⚠️ platform_fault_domain_countis region-dependent — several regions and aligned/managed sets cap the maximum at 2, not 3. Set it explicitly for those regions.⚠️ A VM joins a set only at creation — you cannot move an existing VM into or out of an availability set.
- Virtual Machine Contributor on the target resource group — sufficient to create, read, update, and delete availability sets, or a custom role granting
Microsoft.Compute/availabilitySets/*scoped to the resource group (least privilege).
- An existing resource group in a supported US Azure region (this module references a resource group and does not create one).
- The
Microsoft.Computeresource provider registered on the target subscription. - If pairing with a proximity placement group, that PPG must already exist and its
idis supplied as input. - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription; the module declares none of these.
terraform-azurerm-availability-set/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # deeply-typed inputs, secure defaults, tags + timeouts tail
├── main.tf # keystone azurerm_availability_set.this + dynamic timeouts
├── outputs.tf # id first, then name, then placement facts
├── README.md # this document
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
The smallest real call — an aligned set with the module's secure, resilient defaults:
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
}ℹ️ The caller owns the provider. Configure
provider "azurerm" { features {} }, authentication, and the target subscription in your root module — this module intentionally declares none of them. Always pin?ref=v1.0.0, never a branch.
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group (name) |
location |
string |
caller / terraform-azurerm-resource-group (location) |
proximity_placement_group_id |
string |
a proximity-placement-group module (id), optional |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Availability set Resource ID (first) | a VM's availability_set_id, downstream modules |
name |
Availability set name | diagnostics / tagging |
location |
Region the set resides in | co-located siblings |
resource_group_name |
Resource group the set resides in | co-located siblings |
managed |
Aligned (true) vs classic (false) |
disk-model decisions |
platform_update_domain_count |
Number of update domains | capacity / resilience reporting |
platform_fault_domain_count |
Number of fault domains | capacity / resilience reporting |
1 · Minimal call (aligned set, secure defaults)
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
}💡 The empty call yields an aligned (managed) set with 5 update domains and 3 fault domains — the resilient, modern default.
2 · Aligned/managed set, made explicit
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-app-eastus2"
resource_group_name = "rg-platform-eastus2"
location = "eastus2"
managed = true
}🔒
managed = trueis required for VMs that use managed disks. Keep it unless you are supporting legacy unmanaged disks.
3 · Classic/unmanaged set (legacy only)
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-legacy-centralus"
resource_group_name = "rg-legacy-centralus"
location = "centralus"
managed = false
}
⚠️ managed = falseproduces a classic set for VMs on unmanaged (page-blob) disks. Flippingmanagedlater forces replacement — choose it deliberately.
4 · Maximum resilience (3 fault domains, high update spread)
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-max-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
platform_fault_domain_count = 3
platform_update_domain_count = 10
}💡 More update domains means smaller planned-maintenance reboot batches; more fault domains means broader rack isolation. Both are capped by the region.
5 · Region capped at 2 fault domains
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-data-westus2"
resource_group_name = "rg-platform-westus2"
location = "westus2"
platform_fault_domain_count = 2
}
⚠️ Some regions and aligned sets support at most 2 fault domains. Set the count explicitly there — leaving the default of 3 would be rejected at apply.
6 · Tighter maintenance batching (fewer update domains)
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-batch-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
platform_update_domain_count = 3
}ℹ️ With 3 update domains, at most one-third of members reboot together during planned platform maintenance.
7 · Proximity placement group pairing
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-lowlatency-eastus"
resource_group_name = "rg-hpc-eastus"
location = "eastus"
proximity_placement_group_id = azurerm_proximity_placement_group.hpc.id
}💡 A PPG co-locates members for low network latency. It is force-new here, so attach it at creation. Balance latency against the reduced placement flexibility.
8 · Governance tags
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-prod-eastus"
resource_group_name = "rg-prod-eastus"
location = "eastus"
tags = {
environment = "production"
workload = "web"
cost_center = "cc-1042"
owner = "platform-team"
}
}🔒
tagsis the one field that updates in place — safe to iterate on without replacing the set.
9 · Custom timeouts
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-app-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
timeouts = {
create = "30m"
read = "5m"
update = "30m"
delete = "30m"
}
}ℹ️ Omit
timeoutsto use the provider defaults (create/update/delete 30m, read 5m).
10 · Multiple tiers with for_each
locals {
tiers = {
web = { name = "avset-web-eastus", fault = 3 }
app = { name = "avset-app-eastus", fault = 3 }
data = { name = "avset-data-eastus", fault = 2 }
}
}
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
for_each = local.tiers
name = each.value.name
resource_group_name = "rg-platform-eastus"
location = "eastus"
platform_fault_domain_count = each.value.fault
}💡 One set per role. Keying by a stable tier name keeps the resource addresses stable as tiers are added or removed.
11 · VM association pattern
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
}
resource "azurerm_linux_virtual_machine" "web" {
for_each = toset(["web-0", "web-1"])
name = each.key
resource_group_name = "rg-platform-eastus"
location = "eastus"
size = "Standard_D2s_v5"
admin_username = "azureadmin"
# Join the availability set by its id.
availability_set_id = module.availability_set.id
network_interface_ids = [azurerm_network_interface.web[each.key].id]
disable_password_authentication = true
admin_ssh_key {
username = "azureadmin"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "ubuntu-24_04-lts"
sku = "server"
version = "latest"
}
}🔒 Placing two or more VMs of the same role in one set is what earns the platform SLA. A single-member set earns nothing.
12 · Choosing this set vs Availability Zones
# Availability Set: region-local, spreads across fault/update domains within one datacenter footprint.
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
}
# Availability Zones (the modern alternative) are set on the VM directly, not here:
# resource "azurerm_linux_virtual_machine" "web" {
# zone = "1" # ... a second VM with zone = "2", etc.
# }
# A VM cannot use both an availability_set_id and a zone.ℹ️ Availability Zones give datacenter-level isolation and are preferred for new workloads in zone-enabled regions. Availability sets remain the choice where zones are unavailable or where the workload must stay within a single zone/footprint. The two are mutually exclusive per VM.
13 · Migrating away from zones into a set
# When consolidating a zone-spread workload back into a single-footprint set,
# the availability set itself is unremarkable — the migration happens on the VMs.
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-consolidated-eastus"
resource_group_name = "rg-platform-eastus"
location = "eastus"
}
# Each VM must be re-created to switch from `zone = "..."` to
# `availability_set_id = module.availability_set.id`; membership and zone are both
# creation-time-only on a VM. Plan the VM replacements deliberately.
⚠️ Neither zone membership nor availability-set membership can be changed on a live VM — both migration directions require re-creating the VMs.
14 · 🏗️ End-to-end composition (resource group + set + Linux VMs)
# Provider configured by the caller — features {} is mandatory.
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-platform-eastus"
location = "eastus"
}
module "availability_set" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-availability-set.git?ref=v1.0.0"
name = "avset-web-eastus"
resource_group_name = module.resource_group.name
location = module.resource_group.location
tags = {
environment = "production"
workload = "web"
}
}
resource "azurerm_network_interface" "web" {
for_each = toset(["web-0", "web-1"])
name = "nic-${each.key}"
resource_group_name = module.resource_group.name
location = module.resource_group.location
ip_configuration {
name = "internal"
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}
resource "azurerm_linux_virtual_machine" "web" {
for_each = toset(["web-0", "web-1"])
name = each.key
resource_group_name = module.resource_group.name
location = module.resource_group.location
size = "Standard_D2s_v5"
admin_username = "azureadmin"
# Join every member to the availability set by its emitted id.
availability_set_id = module.availability_set.id
network_interface_ids = [azurerm_network_interface.web[each.key].id]
disable_password_authentication = true
admin_ssh_key {
username = "azureadmin"
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "ubuntu-24_04-lts"
sku = "server"
version = "latest"
}
}💡 The resource group's
nameandlocationflow into the set; the set'sidflows into every VM'savailability_set_id. Two members across three fault domains is the minimum shape that earns the platform availability SLA.
Identity & placement (required)
| Name | Type | Description |
|---|---|---|
name |
string |
Availability set name (force-new). |
resource_group_name |
string |
Existing resource group (force-new). |
location |
string |
Azure region (force-new). |
Resilience shape (optional, secure defaults)
| Name | Type | Default | Description |
|---|---|---|---|
managed |
bool |
true |
Aligned/managed (true) vs classic (false). Force-new. |
platform_update_domain_count |
number |
5 |
Update domains, 1–20. Force-new. |
platform_fault_domain_count |
number |
3 |
Fault domains, 1–3 (region-capped). Force-new. |
proximity_placement_group_id |
string |
null |
Optional PPG resource ID. Force-new. |
Universal tail
| Name | Type | Default | Description |
|---|---|---|---|
tags |
map(string) |
{} |
Tags applied to the set (updatable in place). |
timeouts |
object({...}) |
null |
Optional create/read/update/delete timeouts. |
Full input schemas
variable "name" {
type = string
}
variable "resource_group_name" {
type = string
}
variable "location" {
type = string
}
variable "managed" {
type = bool
default = true
}
variable "platform_update_domain_count" {
type = number
default = 5
# validation: 1 <= value <= 20
}
variable "platform_fault_domain_count" {
type = number
default = 3
# validation: 1 <= value <= 3
}
variable "proximity_placement_group_id" {
type = string
default = null
}
variable "tags" {
type = map(string)
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Notes |
|---|---|---|
id |
Availability set Resource ID | Emitted first; assign to a VM's availability_set_id. |
name |
Availability set name | — |
location |
Region the set resides in | — |
resource_group_name |
Resource group the set resides in | — |
managed |
Aligned (true) vs classic (false) |
— |
platform_update_domain_count |
Number of update domains | — |
platform_fault_domain_count |
Number of fault domains | Read from the resource — several regions cap aligned sets at 2 |
proximity_placement_group_id |
PPG the set is constrained to, or null |
Non-null narrows the physical spread |
set_provides_no_hardware_isolation |
platform_fault_domain_count is 1 |
Every member on one rack |
set_provides_no_maintenance_isolation |
platform_update_domain_count is 1 |
Azure may reboot everything at once |
trades_resilience_for_latency |
A PPG is set | The two goals are in direct tension |
requires_explicit_fault_domain_count_in_some_regions |
Fault domains left at the default of 3 | 🔴 The most common apply-time failure here |
every_argument_except_tags_is_force_new |
Always true |
tags is the only mutable field |
replacing_this_set_requires_emptying_it_first |
Always true |
A replacement is an outage of every member |
this_module_joins_no_virtual_machines |
Always true |
Membership is declared on the VM |
availability_sets_do_not_span_availability_zones |
Always true |
Sets and zones are mutually exclusive |
No secret is emitted — this resource has no secret surface.
- Almost everything is force-new.
name,resource_group_name,location,managed,platform_update_domain_count,platform_fault_domain_count, andproximity_placement_group_idare all immutable. Any change replaces the set, which in turn requires the joined VMs to be re-created. Treat the set's shape as a decision made once at creation. - Only
tagsandtimeoutsupdate in place. Governance tagging can iterate freely; the resilience topology cannot. - Fault-domain count is region-dependent. The module defaults to 3, but several regions and aligned/managed sets cap the effective maximum at 2.
terraform validatewill pass either way (it does not call Azure); a mismatch surfaces only atplan/applyagainst the real region, so set the count explicitly for capped regions. - Membership is creation-time-only on the VM. A VM references the set through its own
availability_set_id, set only when the VM is created. You cannot move a running VM into or out of a set, and a VM cannot combineavailability_set_idwith a zone. - The set is a placement primitive, not an exposure surface. It has no public network, data-plane, encryption, or credential knobs. "Secure by default" here means a resilient default topology and an aligned (managed) set, plus honest immutability documentation.
features {}lives with the caller. If the configuration appears not to initialize in isolation, the cause is a missing caller-sideprovider "azurerm" { features {} }block — expected behavior for a library module.
This resource exposes no public-access, TLS, or firewall knobs, so the secure-by-default posture is expressed as a resilient, forward-looking topology:
| Concern | Secure/sensible default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Disk model compatibility | managed = true (aligned — required for managed disks) |
managed = false (classic/legacy) |
| Update-domain spread | platform_update_domain_count = 5 |
set 1–20 |
| Fault-domain isolation | platform_fault_domain_count = 3 |
set 1–3 (lower for capped regions) |
| Proximity placement group | not attached (maximum placement flexibility) | supply proximity_placement_group_id |
| Tags | {} |
supply a tag map |
# From the module folder — offline, no backend, no cloud:
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0— never track a branch. - This library is plan-only during authoring; a human runs
terraform planandterraform applyfrom CI against real credentials. - The caller supplies
provider "azurerm" { features {} }, authentication, and the subscription.
The offline proof gate is what this module guarantees before release:
terraform init -backend=false— resolves the pinned provider without a backend.terraform validate— proves the configuration is type-correct against the pinned provider schema; it catches every input-shape mistake the typed variables are designed to surface.terraform fmt -check— enforces canonical formatting.
What only terraform plan (run by a human from CI) can exercise: the region-specific fault-domain cap, the existence of the resource group and any PPG, and RBAC on the target scope. validate and fmt never call Azure.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform-eastus/providers/Microsoft.Compute/availabilitySets/avset-web-eastus"
location = "eastus"
managed = true
name = "avset-web-eastus"
platform_fault_domain_count = 3
platform_update_domain_count = 5
resource_group_name = "rg-platform-eastus"
| Symptom | Cause | Fix |
|---|---|---|
| Plan wants to replace the whole set after a small edit | You changed a force-new field (name, location, managed, a domain count, or the PPG). |
Confirm the change is intended; replacing the set requires re-creating joined VMs. Otherwise revert the field. |
| Apply fails: fault domain count not supported in region | The region (or an aligned set) caps fault domains at 2. | Set platform_fault_domain_count = 2 explicitly for that region. |
validate passes but plan errors on the fault-domain count |
validate does not call Azure; the region cap is only enforced live. |
Adjust the count to the region's supported maximum. |
| VM will not join the set | The VM's availability_set_id was set after creation, or the VM also declares a zone. |
Set availability_set_id at VM creation; a VM cannot use both a set and a zone. |
| Provider fails to initialize in isolation | No caller-side features {} block. |
Add provider "azurerm" { features {} } to the root module. |
| Only one VM in the set, no SLA | An availability set needs two or more members to earn the platform SLA. | Place at least two same-role VMs in the set. |
- Provider resource:
azurerm_availability_set - Azure concept: Availability sets overview
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-linux-virtual-machine,terraform-azurerm-windows-virtual-machine - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."