Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Orchestrated Virtual Machine Scale Set Terraform Module

A standalone module that provisions a hardened Orchestrated Virtual Machine Scale Set (Flexible orchestration mode) and all of its nested configuration as deeply-typed inputs, targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Version Type Resources


🧩 Overview

  • πŸ–₯️ Provisions a single azurerm_orchestrated_virtual_machine_scale_set (Flexible orchestration mode) named this.
  • 🌐 Models the full networking shape β€” network interfaces, IP configurations, backend-pool membership, and optional per-configuration public IPs β€” as deeply-typed inputs.
  • πŸ’½ Models storage (OS disk with optional ephemeral/CMK settings, and any number of data disks) and image selection (marketplace reference or image ID).
  • πŸ” Models the OS profile: Linux SSH-key credentials or Windows credentials, plus certificates delivered from Key Vault.
  • πŸͺͺ Attaches user-assigned managed identities for keyless access to Azure resources.
  • ♻️ Exposes lifecycle controls β€” spread across availability zones and fault domains, Spot priority, rolling/automatic upgrades, automatic instance repair, and termination notification.

πŸ’‘ Why it matters: Flexible orchestration decouples the scale set from a single VM SKU family and gives you fault-domain and availability-zone placement with per-instance visibility. This module keeps that power behind a type-safe contract and a locked-down default posture, so the common mistakes β€” a public instance, a password-authenticated Linux node, an unencrypted host β€” require the caller to type the opt-out.

❀️ Support this project

If this module saves you time, please consider supporting its continued development:


πŸ—ΊοΈ Where this fits in the family

flowchart TB
  rg["terraform-azurerm-resource-group"]
  vnet["terraform-azurerm-virtual-network"]
  lb["terraform-azurerm-load-balancer"]
  uai["terraform-azurerm-user-assigned-identity"]
  this["terraform-azurerm-orchestrated-virtual-machine-scale-set"]
  vmss["azurerm_orchestrated_virtual_machine_scale_set"]
  diag["terraform-azurerm-monitor-diagnostic-setting"]

  rg -->|"resource_group_name / location"| this
  vnet -->|"subnet_id"| this
  lb -->|"backend address pool id"| this
  uai -->|"identity_ids"| this
  this -->|"creates"| vmss
  vmss -->|"id"| diag

  classDef module fill:#0078D4,stroke:#004578,color:#fff;
  classDef keystone fill:#004578,stroke:#002a4d,color:#fff;
  classDef sibling fill:#eef3f8,stroke:#b8c4d0,color:#1b1b1b;
  class this module;
  class vmss keystone;
  class rg,vnet,lb,uai,diag sibling;
Loading

🧬 What this module builds

flowchart TB
  subgraph inputs["Inputs"]
    id_in["name / sku_name / instances / platform_fault_domain_count"]
    net_in["network_interface[] + ip_configuration[]"]
    disk_in["os_disk + source_image_reference + data_disk[]"]
    sec_in["os_profile (SSH keys) / identity / encryption_at_host_enabled"]
    life_in["zones / priority / upgrade_mode / automatic_instance_repair"]
  end

  vmss["azurerm_orchestrated_virtual_machine_scale_set (this)"]

  subgraph blocks["Rendered nested blocks"]
    b1["os_disk (+ diff_disk_settings)"]
    b2["network_interface -> ip_configuration -> public_ip_address"]
    b3["os_profile -> linux/windows_configuration"]
    b4["identity (UserAssigned only)"]
    b5["extension / boot_diagnostics / termination_notification / rolling_upgrade_policy"]
  end

  subgraph outputs["Outputs"]
    o_id["id"]
    o_name["name"]
    o_uid["unique_id"]
  end

  id_in --> vmss
  net_in --> vmss
  disk_in --> vmss
  sec_in --> vmss
  life_in --> vmss
  vmss --> b1
  vmss --> b2
  vmss --> b3
  vmss --> b4
  vmss --> b5
  vmss --> o_id
  vmss --> o_name
  vmss --> o_uid

  classDef keystone fill:#004578,stroke:#002a4d,color:#fff;
  classDef sibling fill:#eef3f8,stroke:#b8c4d0,color:#1b1b1b;
  class vmss keystone;
  class id_in,net_in,disk_in,sec_in,life_in,b1,b2,b3,b4,b5,o_id,o_name,o_uid sibling;
Loading

Resource inventory: one keystone azurerm_orchestrated_virtual_machine_scale_set.this, rendered through 31 nested block instances across 29 block types (os_disk, diff_disk_settings, source_image_reference, data_disk, os_profile, linux_configuration, admin_ssh_key, secret, certificate, windows_configuration, winrm_listener, additional_unattend_content, identity, network_interface, ip_configuration, public_ip_address, ip_tag, extension, protected_settings_from_key_vault, additional_capabilities, automatic_instance_repair, boot_diagnostics, termination_notification, rolling_upgrade_policy, plan, priority_mix, sku_profile, virtual_machine_size, timeouts).

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
azurerm provider ~> 4.0
Provider block None β€” the caller configures provider "azurerm" { features {} }, auth, and subscription

Schema notes that bite (verified against the live provider schema):

  • Changing any of name, resource_group_name, location, or platform_fault_domain_count forces replacement.
  • The force-new set, verified field by field against the resource's schema: name, resource_group_name, location, platform_fault_domain_count, priority, eviction_policy, upgrade_mode, zone_balance, extension_operations_enabled, capacity_reservation_group_id and proximity_placement_group_id. location and resource_group_name get it from the provider's shared schema helpers, so they appear in neither a ForceNew grep of the resource nor the binary schema.
  • sku_name is NOT force-new β€” resizing the fleet is an in-place update. Neither are single_placement_group, encryption_at_host_enabled, source_image_id, license_type or network_api_version.
  • zones is force-new in ONE DIRECTION ONLY. A ForceNewIfChange walks the old list and fires if any entry is missing from the new one, so adding a zone is an in-place update and removing one replaces every VM in the set. The rule is per removed element β€” not "the list changed", and not "the list became empty" β€” and nothing in the schema shows it.
  • sku_profile is force-new conditionally: removing it while changing sku_name away from Mix replaces the set.
  • priority, eviction_policy, and upgrade_mode are force-new; you cannot flip a Regular set to Spot in place.
  • encryption_at_host_enabled updates in place. It does require a subscription feature registration and a supporting VM size, neither of which Terraform can see, so the failure mode is an apply error rather than a rebuild.
  • proximity_placement_group_id, capacity_reservation_group_id and extension_operations_enabled are force-new. network_api_version, source_image_id and license_type are not.
  • The plan and rolling_upgrade_policy blocks are force-new as WHOLE BLOCKS. Editing any field inside either β€” a batch percentage, a pause time, a plan product name β€” destroys and recreates the scale set and every instance in it. The rule sits in each block's schema helper rather than in this resource's schema map, so it appears in no walk of the top-level arguments; the plan is the only warning. Note that under upgrade_mode = "Automatic" the rolling policy has no effect on behaviour and still rebuilds the fleet if you edit it.
  • Twelve cross-field rules bind this resource, and eight of them are not in a schema entry at all β€” they live in a CustomizeDiff or in the Create function, so none of the twelve appears in the binary provider schema. sku_profile and sku_name = "Mix" require each other in both directions; rolling_upgrade_policy and upgrade_mode = "Rolling" likewise, while "Manual" refuses the policy; a rank needs allocation_strategy = "Prioritized"; vm_sizes and virtual_machine_size conflict and exactly one is required; auxiliary_mode and auxiliary_sku require each other; capacity_reservation_group_id conflicts with proximity_placement_group_id; and source_image_id conflicts with source_image_reference. This module mirrors every one of them.
  • πŸ”΄ auxiliary_mode and auxiliary_sku are refused at the DEFAULT network_api_version. The provider rejects them whenever that value is 2020-11-01, which is exactly what you get by not setting the argument β€” so the obvious configuration is unplannable, and the provider's error names the API version rather than the field being edited. Set a later version.
  • πŸ”΄ Two rules are checked in the Create function rather than in a CustomizeDiff, so unmirrored they fail at apply, after everything earlier in the apply has been built: zone_balance = true requires a non-empty zones, and priority_mix requires priority = "Spot". Both halves of each are module inputs, so this module catches them at plan.
  • sku_profile VM size names are validated more tightly than sku_name: each must begin Standard_ and belong to the A, B, D, E or F families β€” and DC and EC sizes are refused even though they begin with D and E, because the provider tests those two-letter prefixes first. Standard_EC8ads_v5 is rejected; Standard_E8ds_v5 is accepted.
  • platform_fault_domain_count carries no provider validation at all, and its legal range is region-dependent. A value of 1 means maximum spreading, which is the opposite of what the number reads like and is the value to pair with zones.
  • Within network_interface, changing name is force-new; within os_profile, custom_data, admin_username, computer_name_prefix, and provision_vm_agent are force-new.
  • This resource supports user-assigned managed identities only (identity.type = "UserAssigned"); there is no system-assigned identity and no principal_id.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Virtual Machine Contributor on the target resource group (create/update the scale set), or a custom role granting Microsoft.Compute/virtualMachineScaleSets/* at the resource-group scope.
  • Network Contributor (or Microsoft.Network/virtualNetworks/subnets/join/action plus the network-interface write actions) where the scale set joins an existing subnet, load-balancer backend pool, or application-gateway backend pool.
  • Managed Identity Operator on any user-assigned identity supplied through identity.identity_ids.

Azure Prerequisites

  • An existing resource group in a supported US Azure region (examples use eastus / eastus2 / westus2 / centralus).
  • The Microsoft.Compute resource provider registered on the subscription.
  • For encryption_at_host_enabled = true (the default), the EncryptionAtHost feature registered on the subscription.
  • Any referenced subnet, backend pool, network security group, user-assigned identity, and disk encryption set already exist β€” their ids are inputs.
  • The caller configures the provider "azurerm" { features {} } block, auth, and subscription.

πŸ“ Module Structure

terraform-azurerm-orchestrated-virtual-machine-scale-set/
β”œβ”€β”€ providers.tf   # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
β”œβ”€β”€ variables.tf   # deeply-typed object() schemas + tags/timeouts tail
β”œβ”€β”€ main.tf        # keystone `this`; dynamic blocks + try() for every optional field
β”œβ”€β”€ outputs.tf     # id first, then name, unique_id, identity type/ids
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # the cross-module contract
β”œβ”€β”€ LICENSE        # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore     # the canonical library ignore set

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "orchestrated_vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-web-eastus2"
  resource_group_name         = "rg-platform-eastus2"
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 3

  source_image_reference      = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  os_disk                     = {
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"
  }

  os_profile                  = {
    linux_configuration = {
      admin_username = "azureuser"
      admin_ssh_key = [{
        username   = "azureuser"
        public_key = file("~/.ssh/id_rsa.pub")
      }]
    }
  }

  network_interface           = [{
    name    = "nic-primary"
    primary = true
    ip_configuration = [{
      name      = "ipconfig1"
      primary   = true
      subnet_id = "/subscriptions/.../subnets/snet-web"
    }]
  }]

  tags                        = {
    environment = "prod"
    workload    = "web"
  }
}

ℹ️ The caller owns the provider, its authentication, and the mandatory features {} block. The module never declares a provider block.

πŸ”Œ 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)
network_interface[*].ip_configuration[*].subnet_id string terraform-azurerm-virtual-network (subnet_ids)
network_interface[*].ip_configuration[*].load_balancer_backend_address_pool_ids list(string) terraform-azurerm-load-balancer (backend pool id)
network_interface[*].network_security_group_id string terraform-azurerm-network-security-group (id)
identity.identity_ids set(string) terraform-azurerm-user-assigned-identity (id)
os_disk.disk_encryption_set_id string disk-encryption-set (customer-managed key)

Emits

Output Description Consumed by
id Scale set Resource ID (first) diagnostics, RBAC, autoscale, downstream modules
name Scale set name diagnostics / tagging
unique_id Immutable unique ID stable across the resource's lifetime correlation / tagging
identity_type Managed identity type in effect (or null) documentation / RBAC wiring
identity_ids User-assigned identity resource IDs attached (or null) role-assignment wiring

πŸ“š Example Library

1 Β· Minimal Linux SSH-key scale set
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-min-eastus2"
  resource_group_name         = "rg-platform-eastus2"
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 2

  source_image_reference      = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  os_disk                     = {
    caching              = "ReadWrite"
    storage_account_type = "Premium_LRS"
  }

  os_profile                  = {
    linux_configuration = {
      admin_username = "azureuser"
      admin_ssh_key = [{
        username   = "azureuser"
        public_key = file("~/.ssh/id_rsa.pub")
      }]
    }
  }

  network_interface           = [{
    name    = "nic-primary"
    primary = true
    ip_configuration = [{
      name      = "ipconfig1"
      primary   = true
      subnet_id = var.subnet_id
    }]
  }]
}

πŸ”’ The empty security path applies: disable_password_authentication is true, encryption_at_host_enabled is true, and no public IP is attached.

2 Β· Zone-redundant across three availability zones
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-zr-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D4s_v5"
  instances                   = 6

  zones                       = ["1", "2", "3"]
  zone_balance                = true

  source_image_reference      = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_ZRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }

  network_interface           = [{
    name             = "nic-primary"
    primary          = true
    ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }]
  }]
}

⚠️ zone_balance is force-new. zones is force-new only when a zone is removed β€” adding one is an in-place update, so a set can grow across zones but not shrink without a rebuild. Pair zone spread with a zone-redundant OS disk (Premium_ZRS), and note zone_balance = true is refused unless zones is non-empty.

3 Β· Spot instances (eviction, priority, max bid price)
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-spot-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 10

  priority                    = "Spot"
  eviction_policy             = "Delete"
  max_bid_price               = 0.25

  source_image_reference      = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Standard_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }

  network_interface           = [{
    name             = "nic-primary"
    primary          = true
    ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }]
  }]
}

πŸ’‘ max_bid_price = -1 (the default) never evicts for price and bills at the on-demand rate. Set a positive cap only for cost-bounded, interruptible batch workloads.

4 Β· Regular / Spot priority mix
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-mix-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 20

  priority                    = "Spot"
  eviction_policy             = "Delete"

  priority_mix                = {
    base_regular_count            = 4
    regular_percentage_above_base = 50
  }

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Standard_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

ℹ️ priority_mix keeps a guaranteed floor of Regular instances while filling the rest with Spot; it applies only when priority = "Spot".

5 Β· Customer-managed key (CMK) on the OS disk
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-cmk-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 3

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }

  os_disk                     = {
    caching                = "ReadWrite"
    storage_account_type   = "Premium_LRS"
    disk_encryption_set_id = var.disk_encryption_set_id
  }

  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

πŸ”’ Encryption at rest is always on; supplying disk_encryption_set_id upgrades the OS disk to a customer-managed key. Host-level encryption (encryption_at_host_enabled) remains on by default.

6 Β· Data disks (including UltraSSD)
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-data-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D4s_v5"
  instances                   = 3

  additional_capabilities     = { ultra_ssd_enabled = true }

  data_disk                   = [
    {
      caching              = "ReadOnly"
      storage_account_type = "Premium_LRS"
      lun                  = 0
      disk_size_gb         = 256
      create_option        = "Empty"
    },
    {
      caching                        = "None"
      storage_account_type           = "UltraSSD_LRS"
      lun                            = 1
      disk_size_gb                   = 1024
      create_option                  = "Empty"
      ultra_ssd_disk_iops_read_write = 5000
      ultra_ssd_disk_mbps_read_write = 200
    }
  ]

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

⚠️ UltraSSD_LRS data disks require additional_capabilities.ultra_ssd_enabled = true and an AZ-enabled region/SKU combination.

7 Β· Behind a load-balancer backend pool with automatic instance repair
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-lb-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 4

  automatic_instance_repair   = {
    enabled      = true
    action       = "Replace"
    grace_period = "PT30M"
  }

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }

  network_interface           = [{
    name    = "nic-primary"
    primary = true
    ip_configuration = [{
      name                                   = "ipconfig1"
      primary                                = true
      subnet_id                              = var.subnet_id
      load_balancer_backend_address_pool_ids = [var.lb_backend_pool_id]
    }]
  }]
}

πŸ’‘ Automatic instance repair needs a health signal. A load-balancer probe on the backend pool (or an application-health extension) supplies it; without one, repair stays inert.

8 Β· Application Gateway backend with accelerated networking
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-agw-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D4s_v5"
  instances                   = 4

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }

  network_interface           = [{
    name                          = "nic-primary"
    primary                       = true
    enable_accelerated_networking = true
    network_security_group_id     = var.nsg_id
    ip_configuration = [{
      name                                         = "ipconfig1"
      primary                                      = true
      subnet_id                                    = var.subnet_id
      application_gateway_backend_address_pool_ids = [var.agw_backend_pool_id]
    }]
  }]
}

ℹ️ Accelerated networking requires a supported VM SKU; the association to the application-gateway backend pool is expressed on the primary ip_configuration.

9 Β· User-assigned managed identity (keyless access)
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-mi-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 2

  identity                    = {
    type         = "UserAssigned"
    identity_ids = [var.user_assigned_identity_id]
  }

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

πŸ”’ This resource supports user-assigned identities only. Prefer a managed identity over embedded credentials, and grant it least-privilege roles at the smallest scope that works.

10 Β· Rolling upgrades
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-rolling-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 6

  upgrade_mode                = "Rolling"
  rolling_upgrade_policy      = {
    max_batch_instance_percent              = 20
    max_unhealthy_instance_percent          = 20
    max_unhealthy_upgraded_instance_percent = 20
    pause_time_between_batches              = "PT30S"
    cross_zone_upgrades_enabled             = true
  }

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

⚠️ upgrade_mode is force-new, so choose it at creation. Rolling upgrades require an application health signal to gate each batch.

11 Β· VM extension delivered from Key Vault (no inline secret)
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-ext-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 3

  extension                   = [{
    name                       = "AzureMonitorLinuxAgent"
    publisher                  = "Microsoft.Azure.Monitor"
    type                       = "AzureMonitorLinuxAgent"
    type_handler_version       = "1.0"
    auto_upgrade_minor_version_enabled = true
    protected_settings_from_key_vault = {
      secret_url      = var.kv_secret_url
      source_vault_id = var.key_vault_id
    }
  }]

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

πŸ”’ Prefer protected_settings_from_key_vault over inline protected_settings, so no secret is expressed in configuration. Inline protected_settings is redacted by the provider but still lives in state.

12 Β· Windows scale set (password out of band)
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmsswin-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 2
  license_type                = "Windows_Server"

  source_image_reference      = {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2022-datacenter-azure-edition"
    version   = "latest"
  }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }

  os_profile                  = {
    windows_configuration = {
      admin_username           = "azureadmin"
      admin_password           = var.windows_admin_password
      enable_automatic_updates = true
    }
  }

  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

⚠️ Windows requires admin_password. Provision it out of band (a Key Vault reference passed as var.windows_admin_password); the provider marks it sensitive so it is redacted from plan output. Never commit a plaintext value.

13 Β· Boot diagnostics + termination notification + fault-domain isolation
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-ops-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 3
  sku_name                    = "Standard_D2s_v5"
  instances                   = 6

  boot_diagnostics            = { storage_account_uri = null } # managed storage account

  termination_notification    = {
    enabled = true
    timeout = "PT5M"
  }

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }
  network_interface           = [{ name = "nic-primary", primary = true, ip_configuration = [{ name = "ipconfig1", primary = true, subnet_id = var.subnet_id }] }]
}

πŸ’‘ storage_account_uri = null enables a platform-managed boot-diagnostics store. platform_fault_domain_count is force-new, so set the fault-domain spread at creation.

14 Β· Multiple NICs, DNS, and a deliberate public IP
module "vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-multinic-eastus2"
  resource_group_name         = var.resource_group_name
  location                    = "eastus2"
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D4s_v5"
  instances                   = 3

  source_image_reference      = { publisher = "Canonical", offer = "0001-com-ubuntu-server-jammy", sku = "22_04-lts-gen2", version = "latest" }
  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_LRS" }
  os_profile                  = { linux_configuration = { admin_username = "azureuser", admin_ssh_key = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }] } }

  network_interface           = [
    {
      name        = "nic-frontend"
      primary     = true
      dns_servers = ["10.0.0.4", "10.0.0.5"]
      ip_configuration = [{
        name      = "ipconfig-public"
        primary   = true
        subnet_id = var.subnet_id
        public_ip_address = [{
          name                    = "pip-instance"
          idle_timeout_in_minutes = 15
        }]
      }]
    },
    {
      name             = "nic-backend"
      primary          = false
      ip_configuration = [{ name = "ipconfig-internal", primary = true, subnet_id = var.backend_subnet_id }]
    }
  ]
}

⚠️ Attaching a public_ip_address is the explicit opt-out from the private-by-default posture. Only one network_interface and one ip_configuration per NIC may be primary = true.

15 Β· πŸ—οΈ End-to-end composition (resource group + VNet subnet + load balancer + this)
provider "azurerm" {
  features {}
}

module "resource_group" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
  name     = "rg-web-eastus2"
  location = "eastus2"
}

module "network" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
  name                = "vnet-web-eastus2"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
  address_space       = ["10.20.0.0/16"]

  subnets = {
    web = {
      name             = "snet-web"
      address_prefixes = ["10.20.1.0/24"]
    }
  }
}

module "load_balancer" {
  source              = "git::https://github.com/microsoftexpert/terraform-azurerm-load-balancer.git?ref=v1.0.0"

  frontend_ip_configurations = { public = { name = "public-fe", public_ip_address_id = azurerm_public_ip.scale.id } }
  name                = "lb-web-eastus2"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
  # ... frontend, backend pool, and health probe configuration ...
}

module "orchestrated_vmss" {
  source                      = "git::https://github.com/microsoftexpert/terraform-azurerm-orchestrated-virtual-machine-scale-set.git?ref=v1.0.0"

  name                        = "vmss-web-eastus2"
  resource_group_name         = module.resource_group.name
  location                    = module.resource_group.location
  platform_fault_domain_count = 1
  sku_name                    = "Standard_D2s_v5"
  instances                   = 4
  zones                       = ["1", "2", "3"]

  source_image_reference      = {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts-gen2"
    version   = "latest"
  }

  os_disk                     = { caching = "ReadWrite", storage_account_type = "Premium_ZRS" }

  os_profile                  = {
    linux_configuration = {
      admin_username = "azureuser"
      admin_ssh_key  = [{ username = "azureuser", public_key = file("~/.ssh/id_rsa.pub") }]
    }
  }

  automatic_instance_repair   = { enabled = true, action = "Replace", grace_period = "PT30M" }

  network_interface           = [{
    name    = "nic-primary"
    primary = true
    ip_configuration = [{
      name                                   = "ipconfig1"
      primary                                = true
      subnet_id                              = module.network.subnet_ids["web"]
      load_balancer_backend_address_pool_ids = [module.load_balancer.backend_pool_ids]
    }]
  }]

  tags                        = { environment = "prod", workload = "web" }
}

πŸ’‘ Each sibling's output feeds this module's input: the resource group's name/location, the VNet module's subnet_ids["web"], and the load balancer's backend pool id. Pin every ?ref= to a released tag.

πŸ“₯ Inputs

Required: name, resource_group_name, location, platform_fault_domain_count.

Common optional: sku_name, instances, source_image_reference / source_image_id, os_disk, os_profile, network_interface, identity.

Placement & lifecycle: zones, zone_balance, single_placement_group, priority, eviction_policy, max_bid_price, priority_mix, upgrade_mode, rolling_upgrade_policy, automatic_instance_repair, termination_notification.

Security & platform: encryption_at_host_enabled (default true), license_type, proximity_placement_group_id, capacity_reservation_group_id, network_api_version, user_data_base64 (sensitive), extension_operations_enabled, extensions_time_budget.

Storage & extras: data_disk, additional_capabilities, boot_diagnostics, extension, plan, sku_profile.

Universal tail: tags, timeouts.

Full object() schemas
# Identity and required fields
name                        = string
resource_group_name         = string
location                    = string
platform_fault_domain_count = number

# Sizing & scheduling
sku_name                     = optional(string)
instances                    = optional(number)
priority                     = optional(string, "Regular")   # Regular | Spot
eviction_policy              = optional(string)               # Deallocate | Delete (Spot only)
max_bid_price                = optional(number, -1)
upgrade_mode                 = optional(string, "Manual")     # Automatic | Manual | Rolling
zones                        = optional(set(string), [])
zone_balance                 = optional(bool, false)
single_placement_group       = optional(bool)
encryption_at_host_enabled   = optional(bool, true)
license_type                 = optional(string)
proximity_placement_group_id  = optional(string)
capacity_reservation_group_id = optional(string)
network_api_version          = optional(string)
source_image_id              = optional(string)
user_data_base64             = optional(string)  # sensitive
extension_operations_enabled = optional(bool)
extensions_time_budget       = optional(string)

# os_disk
os_disk = optional(object({
  caching                   = optional(string, "ReadWrite")    # None | ReadOnly | ReadWrite
  storage_account_type      = optional(string, "Premium_LRS")  # Standard_LRS | StandardSSD_LRS | StandardSSD_ZRS | Premium_LRS | Premium_ZRS
  disk_size_gb              = optional(number)
  disk_encryption_set_id    = optional(string)
  write_accelerator_enabled = optional(bool, false)
  diff_disk_settings        = optional(object({ option = string, placement = optional(string) }))
}))

# source_image_reference (or use source_image_id)
source_image_reference = optional(object({
  publisher = string
  offer     = string
  sku       = string
  version   = string
}))

# data_disk
data_disk = optional(list(object({
  caching                        = string
  storage_account_type           = string
  lun                            = optional(number)
  disk_size_gb                   = optional(number)
  create_option                  = optional(string)
  disk_encryption_set_id         = optional(string)
  write_accelerator_enabled      = optional(bool, false)
  ultra_ssd_disk_iops_read_write = optional(number)
  ultra_ssd_disk_mbps_read_write = optional(number)
})), [])

# os_profile (supply exactly one of linux_configuration / windows_configuration)
os_profile = optional(object({
  custom_data = optional(string)
  linux_configuration = optional(object({
    admin_username                  = string
    admin_password                  = optional(string)
    disable_password_authentication = optional(bool, true)
    computer_name_prefix            = optional(string)
    provision_vm_agent              = optional(bool, true)
    patch_assessment_mode           = optional(string)
    patch_mode                      = optional(string)
    admin_ssh_key                   = optional(list(object({ username = string, public_key = string })), [])
    secret                          = optional(list(object({ key_vault_id = string, certificate = list(object({ url = string })) })), [])
  }))
  windows_configuration = optional(object({
    admin_username              = string
    admin_password              = string
    computer_name_prefix        = optional(string)
    enable_automatic_updates    = optional(bool, true)
    hotpatching_enabled         = optional(bool)
    provision_vm_agent          = optional(bool, true)
    timezone                    = optional(string)
    patch_assessment_mode       = optional(string)
    patch_mode                  = optional(string)
    winrm_listener              = optional(list(object({ protocol = string, certificate_url = optional(string) })), [])
    secret                      = optional(list(object({ key_vault_id = string, certificate = list(object({ store = string, url = string })) })), [])
    additional_unattend_content = optional(list(object({ setting = string, content = string })), [])
  }))
}))

# identity (UserAssigned only)
identity = optional(object({
  type         = optional(string, "UserAssigned")
  identity_ids = set(string)
}))

# network_interface
network_interface = optional(list(object({
  name                          = string
  primary                       = optional(bool)
  enable_accelerated_networking = optional(bool, false)
  enable_ip_forwarding          = optional(bool, false)
  dns_servers                   = optional(list(string), [])
  network_security_group_id     = optional(string)
  auxiliary_mode                = optional(string)
  auxiliary_sku                 = optional(string)
  ip_configuration = list(object({
    name                                         = string
    primary                                      = optional(bool)
    version                                      = optional(string, "IPv4")
    subnet_id                                    = optional(string)
    application_gateway_backend_address_pool_ids = optional(list(string), [])
    application_security_group_ids               = optional(list(string), [])
    load_balancer_backend_address_pool_ids       = optional(list(string), [])
    public_ip_address = optional(list(object({
      name                    = string
      domain_name_label       = optional(string)
      idle_timeout_in_minutes = optional(number)
      public_ip_prefix_id     = optional(string)
      sku_name                = optional(string)
      version                 = optional(string, "IPv4")
      ip_tag                  = optional(list(object({ tag = string, type = string })), [])
    })), [])
  }))
})), [])

# extension
extension = optional(list(object({
  name                                      = string
  publisher                                 = string
  type                                      = string
  type_handler_version                      = string
  auto_upgrade_minor_version_enabled        = optional(bool)
  failure_suppression_enabled               = optional(bool)
  force_extension_execution_on_change       = optional(string)
  extensions_to_provision_after_vm_creation = optional(list(string), [])
  settings                                  = optional(string)
  protected_settings                        = optional(string)
  protected_settings_from_key_vault         = optional(object({ secret_url = string, source_vault_id = string }))
})), [])

# capabilities / health / lifecycle
additional_capabilities   = optional(object({ ultra_ssd_enabled = optional(bool, false) }))
automatic_instance_repair = optional(object({ enabled = bool, action = optional(string), grace_period = optional(string) }))
boot_diagnostics          = optional(object({ storage_account_uri = optional(string) }))
termination_notification  = optional(object({ enabled = bool, timeout = optional(string) }))
rolling_upgrade_policy = optional(object({
  max_batch_instance_percent              = number
  max_unhealthy_instance_percent          = number
  max_unhealthy_upgraded_instance_percent = number
  pause_time_between_batches              = string
  cross_zone_upgrades_enabled             = optional(bool)
  maximum_surge_instances_enabled         = optional(bool)
  prioritize_unhealthy_instances_enabled  = optional(bool)
}))
plan         = optional(object({ name = string, product = string, publisher = string }))
priority_mix = optional(object({ base_regular_count = optional(number), regular_percentage_above_base = optional(number) }))
sku_profile = optional(object({
  allocation_strategy  = string
  vm_sizes             = optional(set(string))
  virtual_machine_size = optional(list(object({ name = string, rank = optional(number) })), [])
}))

# universal tail
tags     = optional(map(string), {})
timeouts = optional(object({ create = optional(string), read = optional(string), update = optional(string), delete = optional(string) }))

🧾 Outputs

Output Description Notes
id The Azure Resource ID of the scale set emitted first
name The scale set name
location Azure region, in the canonical form Azure uses. Read from the resource, not var.location.
unique_id Immutable unique ID, stable across the resource's lifetime
identity_type Managed identity type in effect null when no identity configured
identity_ids User-assigned identity resource IDs attached null when no identity configured
resource_group_name Containing resource group force-new
sku_name The VM size, or Mix not force-new β€” resizing is in place
instances The instance count Computed; reports Azure's value when the input is omitted
platform_fault_domain_count Fault-domain spread force-new; 1 means maximum spreading
zones Availability zones spanned adding is in place, removing replaces
priority Regular or Spot force-new
network_api_version Networking API contract the default refuses the auxiliary fields
upgrade_mode Manual, Automatic or Rolling force-new
instances_are_evictable Derived: priority == "Spot" Azure may reclaim these VMs at 30 seconds' notice
spot_eviction_is_price_triggered Derived: Spot and a bid other than -1 adds a second eviction cause β€” a price movement can empty the set
model_changes_do_not_reach_running_instances Derived: upgrade_mode == "Manual" a patch delivered this way reaches new instances only, indefinitely
automatic_repair_cannot_work Derived: repair configured while extension operations are disabled the feature is accepted, planned and inert
removing_a_zone_replaces_the_scale_set Always true the one-directional force-new nothing in the schema shows
uses_the_deprecated_vm_sizes_argument Derived will not plan against provider 5.0
user_data_is_readable_by_every_process_on_the_instance Derived user data is not a place for a credential
identity_grants_no_permissions Always true attaching an identity is not authorising it

No secret is emitted. admin_password, custom_data, user_data_base64, and extension protected_settings are redacted by the provider.

🧠 Architecture Notes

  • One keystone, thin renderer. The module owns exactly one resource, azurerm_orchestrated_virtual_machine_scale_set.this. Every optional or repeating nested block is rendered with a dynamic block gated on a non-null input, and every optional leaf uses try(x, null), so an omitted input renders as absent rather than as an error.
  • Force-new fields bite hard β€” but the set is smaller than it looks. name, resource_group_name, location, platform_fault_domain_count, priority, eviction_policy, upgrade_mode, zone_balance, extension_operations_enabled, capacity_reservation_group_id and proximity_placement_group_id force replacement, plus zones when a zone is removed and sku_profile when it is removed alongside a sku_name change away from Mix. sku_name, single_placement_group, encryption_at_host_enabled, source_image_id, license_type and network_api_version update in place β€” so resizing the fleet, moving to a patched image and turning on host encryption are all ordinary updates, not rebuilds.
  • User-assigned identity only. Unlike the Uniform scale sets, the Orchestrated resource does not offer a system-assigned identity, so there is no principal_id attribute to emit. Grant roles to the user-assigned identity you attach.
  • Private by default. No public_ip_address block is rendered unless the caller adds one to an ip_configuration. Instances stay reachable only through the VNet, a load balancer, or an application gateway.
  • Secrets never leak from the module. The provider marks admin_password, custom_data, user_data_base64, and extension protected_settings as sensitive; they are redacted from plan output and never emitted as outputs. Prefer SSH keys (Linux) and protected_settings_from_key_vault (extensions) so no secret is expressed at all.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block. That is expected β€” library modules never carry it.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Linux password auth os_profile.linux_configuration.disable_password_authentication = true (SSH keys) set to false and supply admin_password
Host encryption encryption_at_host_enabled = true set to false
Instance public IP none attached add a public_ip_address block to an ip_configuration
Managed identity preferred over embedded credentials omit identity
Data at rest encrypted with a platform-managed key supply disk_encryption_set_id for a customer-managed key
Instance priority priority = "Regular" (on-demand) set to "Spot"
Upgrade mode upgrade_mode = "Manual" (least disruptive) set to "Automatic" or "Rolling"
Secrets in extensions protected_settings_from_key_vault (a reference) supply inline protected_settings

πŸš€ Runbook

# From the module folder β€” offline, no cloud calls:
terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module to a released tag: ?ref=v1.0.0. Never track a branch.
  • This library is plan-only during authoring; a human runs terraform plan / apply from CI against real credentials.
  • The caller supplies the provider "azurerm" { features {} } block and authentication (Azure CLI, managed identity, or OIDC) β€” never the module.

πŸ§ͺ Testing

The offline proof gate below runs without Azure credentials and without a backend:

Gate What it proves
terraform init -backend=false Providers resolve against the ~> 4.0 pin; no backend is configured.
terraform validate The configuration is type-correct against the pinned provider schema β€” every object() typo is caught here. It also fires the provider's own schema checks, but only where values are literals; it does not evaluate this module's validation {} blocks, which are conditions on variables that validate never resolves.
terraform plan with real values What actually exercises this module's checks β€” all 41 of them, offline and without credentials: the five separate name rules, the sku_name format, the instances and priority_mix ranges, the max_bid_price two-value rule, the extensions_time_budget format and its PT15M–PT2H range, every sku_profile rule including the size-family check, both directions of the sku_profile/Mix and rolling_upgrade_policy/upgrade_mode pairings, both ConflictsWith rules, the auxiliary-networking pairing and its API-version dependency, and the two rules the provider defers to its Create function.
terraform fmt -check Canonical formatting.

Only a human-run apply exercises the Azure Resource Manager API: whether the VM size exists in the region, whether quota allows the instance count, whether the fault-domain count is legal there, whether Spot capacity is available, and whether the subscription has registered host encryption. This module is never applyd during authoring.

πŸ’¬ Example Output

$ terraform output
id            = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-web-eastus2/providers/Microsoft.Compute/virtualMachineScaleSets/vmss-web-eastus2"
name          = "vmss-web-eastus2"
unique_id     = "8f2d1c34-6b9a-4c1e-9f7a-2b0d5e6c1a3f"
identity_type = "UserAssigned"
identity_ids  = [
  "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-identity/providers/Microsoft.ManagedIdentity/userAssignedIdentities/id-web",
]

πŸ” Troubleshooting

Symptom Cause Fix
Provider configuration not present / features error on init No caller-side provider "azurerm" { features {} } block Add the provider block with features {} in the root module.
Plan proposes destroy-and-recreate after a small edit You changed a genuinely force-new field (platform_fault_domain_count, priority, eviction_policy, upgrade_mode, zone_balance, extension_operations_enabled, either placement-group ID, or the identity trio) β€” or you removed a zone, which is force-new where adding one is not. Note sku_name, source_image_id, encryption_at_host_enabled and license_type are not force-new and will not cause this. Revert the change, or accept replacement deliberately with a maintenance window.
sku_profile rejected, or demanded, unexpectedly sku_profile and sku_name = "Mix" require each other in both directions Set both or neither.
auxiliary_mode rejected on an otherwise valid network interface network_api_version is at its default of 2020-11-01, at which the provider refuses the auxiliary fields Set a later network_api_version.
Apply fails on zone_balance or priority_mix after a clean plan Both rules live in the provider's Create function, not in a CustomizeDiff This module mirrors both at plan; if you see it from a different module, add zones or set priority = "Spot".
identity.type must be "UserAssigned" Set SystemAssigned on identity.type This resource supports user-assigned identities only; supply identity_ids.
os_profile may set only one of linux_configuration or windows_configuration Both sub-blocks supplied Keep exactly one OS configuration.
Automatic instance repair never triggers No application health signal Add a load-balancer health probe on the referenced backend pool, or an application-health extension.
UltraSSD data disk rejected additional_capabilities.ultra_ssd_enabled not set Set additional_capabilities = { ultra_ssd_enabled = true } and use an AZ-enabled SKU/region.
Spot eviction / bid-price fields ignored priority left as Regular Set priority = "Spot" before eviction_policy, max_bid_price, or priority_mix take effect.
EncryptionAtHost feature error Subscription feature not registered Register the EncryptionAtHost feature, or set encryption_at_host_enabled = false.

πŸ”— Related Docs

πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."