Skip to content

Latest commit

 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

☁️ Azure Availability Set Terraform Module

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.

Terraform azurerm Module Type Resources


🧩 Overview

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 tags and timeouts tail.

💡 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.

❤️ Support this project

If this module saves you time, consider supporting the work:


🗺️ Where this fits in the family

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
Loading

🧬 What this module builds

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
Loading

Resource inventory

Resource Count Role
azurerm_availability_set.this 1 The keystone availability set.

✅ Provider / Versions

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, and location are force-new — changing any of them replaces the set.
  • 🔒 managed is force-new — flipping between aligned (true) and classic (false) replaces the set.
  • 🔒 platform_update_domain_count and platform_fault_domain_count are force-new — resizing the domain topology replaces the set.
  • 🔒 proximity_placement_group_id is force-new — attaching, detaching, or changing the PPG replaces the set.
  • 🏷️ Only tags (and timeouts) update in place; everything else is immutable.
  • ⚠️ platform_fault_domain_count is 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.

🔑 Required Azure RBAC Roles / Permissions

  • 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).

Azure Prerequisites

  • An existing resource group in a supported US Azure region (this module references a resource group and does not create one).
  • The Microsoft.Compute resource provider registered on the target subscription.
  • If pairing with a proximity placement group, that PPG must already exist and its id is supplied as input.
  • The caller configures the provider "azurerm" { features {} } block, authentication, and subscription; the module declares none of these.

📁 Module Structure

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

⚙️ Quick Start

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.

🔌 Cross-Module Contract

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

📚 Example Library

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 = true is 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 = false produces a classic set for VMs on unmanaged (page-blob) disks. Flipping managed later 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"
  }
}

🔒 tags is 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 timeouts to 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 name and location flow into the set; the set's id flows into every VM's availability_set_id. Two members across three fault domains is the minimum shape that earns the platform availability SLA.

📥 Inputs

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
}

🧾 Outputs

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.

🧠 Architecture Notes

  • Almost everything is force-new. name, resource_group_name, location, managed, platform_update_domain_count, platform_fault_domain_count, and proximity_placement_group_id are 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 tags and timeouts update 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 validate will pass either way (it does not call Azure); a mismatch surfaces only at plan/apply against 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 combine availability_set_id with 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-side provider "azurerm" { features {} } block — expected behavior for a library module.

🧱 Design Principles

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

🚀 Runbook

# 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 plan and terraform apply from CI against real credentials.
  • The caller supplies provider "azurerm" { features {} }, authentication, and the subscription.

🧪 Testing

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.

💬 Example Output

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"

🔍 Troubleshooting

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.

🔗 Related Docs

💙 "Infrastructure as Code should be standardized, consistent, and secure."