Creates an availability set in VMM for Arc-enabled virtual machines, so VMs placed in it are kept apart by VMM's placement engine (
azurerm_system_center_virtual_machine_manager_availability_set). Targetshashicorp/azurerm ~> 4.0.
- 🧱 Creates an availability set in VMM — the one module in this family that creates rather than projects.
⚠️ It takes the VMM server's Resource ID, where its three sibling projection modules take an inventory item. That asymmetry is the single most common mistake in this family, and it is caught at plan time here.- 🔗 Membership is not expressed here. A VM joins through the virtual-machine-instance module, so this module cannot place anything in the set — or report whether anything is in it.
- 🔁 Everything except
tagsis force-new, includingname— and here the name is not cosmetic. ⚠️ Renaming detaches every VM in the set, and that detachment does not appear in this module's plan.- 🚫 No claim is made about resilience. A set across a single host separates nothing.
- 🔒 No secret is accepted and none is emitted.
💡 Why it matters: Five inputs, one of which differs from its near-identical siblings in a way that fails only at apply. The other thing worth knowing is what this module cannot tell you: whether the set has members, and whether VMM can honour it.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
bridge["OUT OF BAND, BEFORE ANY TERRAFORM: Microsoft's onboarding script deploys the Arc RESOURCE BRIDGE and creates the CUSTOM LOCATION. No module in this suite does this. It takes up to 30 minutes and needs 32 GB RAM, 4 vCPUs and 100 GB of disk reserved in VMM."]
cred["THE VMM CREDENTIAL: Microsoft REQUIRES a full VMM administrator that is ALSO a local administrator on the VMM server, on every node if VMM is highly available. There is NO scoped VMM role. It is FORCE-NEW, so Terraform has NO rotation path, and it is in state in PLAINTEXT."]
rg["terraform-azurerm-resource-group"]
server["terraform-azurerm-system-center-virtual-machine-manager-server: THE ANCHOR. Registers the VMM server and emits the custom_location_id every sibling needs."]
inv["the inventory-items DATA SOURCE, by inventory_type Cloud, VirtualMachineTemplate, VirtualNetwork or VirtualMachine. This is how you find the ids the projection modules take. VMM objects created moments ago may not be listed yet."]
cloud["terraform-azurerm-system-center-virtual-machine-manager-cloud: PROJECTS an existing VMM placement scope"]
tmpl["terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-template: PROJECTS an existing VMM template"]
vnet["terraform-azurerm-system-center-virtual-machine-manager-virtual-network: PROJECTS an existing VMM VM NETWORK. NOT an Azure virtual network: no address space, no subnets, no peering."]
asym["THE ASYMMETRY THAT CATCHES EVERYONE: the three PROJECTION modules take an INVENTORY ITEM id because their VMM object already exists. The availability set takes the VMM SERVER id, because Azure CREATES it in VMM."]
avset["terraform-azurerm-system-center-virtual-machine-manager-availability-set: the ONE module here that CREATES an object in VMM"]
arc["terraform-azurerm-arc-machine with kind SCVMM: the prerequisite for BOTH modules below, and the thing to put TAGS on, since neither of them has any"]
inst["terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance: an EXTENSION of the Arc machine, always named default, one per machine"]
modes["TWO MUTUALLY EXCLUSIVE MODES: set template_id to DEPLOY a new VM, or inventory_item_id to ADOPT one that exists. The provider makes BOTH optional, so picking NEITHER passes validation and fails at apply. Enforced here at plan time."]
disk["AND THE SERVICE INJECTS A DISK YOU NEVER DECLARED, IDE bus, from the template. The provider's own fix is ignore_changes on storage_disk, but lifecycle is NOT VALID inside a module block, so the CALLER CANNOT WRITE IT. This module sets it: disks apply at creation and are then NOT reconciled."]
destroy["AND A DESTROY MAY DELETE THE REAL VM. The API distinguishes remove-from-Azure from delete-from-host; THE PROVIDER EXPOSES NEITHER SWITCH and documents which it does. prevent_destroy is also unavailable to a module caller. Use a CanNotDelete lock and an approval gate."]
agent["terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance-guest-agent: installs the connected machine agent IN THE GUEST"]
peer["A PEER, NOT A CHILD: it takes the ARC MACHINE id, the same input the instance takes, NOT the instance's id. So NO attribute links them, Terraform CANNOT order them, and you need an explicit depends_on. Exactly one per VM, always named default."]
gcred["A SECOND, SEPARATE SECRET: GUEST OS credentials, owned by a different team from the VMM administrator account. Also force-new, also plaintext in state. Not using this module is the only way to keep them out."]
rp["AND THIS resource ALONE does not auto-register Microsoft.ScVmm: run az provider register --namespace Microsoft.ScVmm"]
bridge -->|"custom_location_id"| server
cred -->|"username and password"| server
rg -->|"resource_group_name, location"| server
server -->|"id"| inv
inv -->|"inventory item ids"| cloud
inv -->|"inventory item ids"| tmpl
inv -->|"inventory item ids"| vnet
server -->|"custom_location_id and location to ALL SIX siblings"| cloud
asym -->|"read this first"| avset
server -->|"id, NOT an inventory item"| avset
cloud -->|"cloud_id, placement"| inst
tmpl -->|"template_id, mode 1"| inst
vnet -->|"virtual_network_id per interface"| inst
avset -->|"id, and MEMBERSHIP is expressed HERE not there"| inst
arc -->|"id as scoped_resource_id"| inst
modes -->|"validated at plan"| inst
disk -->|"decided inside the module"| inst
destroy -->|"unmitigable from here"| inst
arc -->|"the SAME id as scoped_resource_id"| agent
peer -->|"depends_on required"| agent
gcred -->|"username and password"| agent
rp -->|"prerequisite"| agent
inst -.->|"must exist first, but NO reference exists"| agent
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class server keystone;
class cloud,tmpl,vnet,avset,inst,agent me;
class bridge,cred,rg,inv,asym,arc,modes,disk,destroy,peer,gcred,rp sib;
flowchart TB
asym["READ THIS FIRST: THIS MODULE TAKES THE VMM SERVER's RESOURCE ID, not an inventory item id. Its three near-identical siblings - cloud, virtual-machine-template, virtual-network - all take an INVENTORY ITEM in the same position."]
why["and the reason is the whole point of the module: THIS IS THE ONE MODULE IN THE FAMILY THAT CREATES AN OBJECT IN VMM. There is no inventory item to reference, because the availability set does not exist until Azure makes it."]
check["so the provider accepts an inventory item id here and fails at apply, and this module rejects it AT PLAN TIME with an error message that names the distinction - reaching for an inventory-items lookup is the natural mistake after writing the other three"]
flag["and creates_an_object_in_vmm is emitted as a constant true, the deliberate counterpart to the projection modules' projects_an_existing_vmm_object. Read side by side, the pair answers the only question that matters before a destroy: does this remove a RECORD or remove INFRASTRUCTURE?"]
name["EVERY ARGUMENT EXCEPT tags IS FORCE-NEW, INCLUDING name - and here the name is NOT cosmetic, because it is the set's name in VMM too. Renaming destroys the set and creates a new one."]
detach["which DETACHES EVERY VM currently placed in it - and because membership lives on the instance side, THAT DETACHMENT DOES NOT APPEAR IN THIS MODULE'S PLAN"]
member["MEMBERSHIP IS NOT EXPRESSED HERE AT ALL. A VM joins through the virtual-machine-instance module's availability_set_ids input, so this module cannot place anything in the set and cannot report whether anything is in it."]
empty["so an EMPTY SET is a valid, silent outcome: nothing in this module or its plan indicates whether any VM ever joined"]
nores["AND NO CLAIM IS MADE ABOUT RESILIENCE. The set is a placement REQUEST to VMM. Whether VMM can honour it depends on host count and other constraints this module cannot see - a set across a single host separates nothing."]
this["terraform-azurerm-system-center-virtual-machine-manager-availability-set"]
keystone["azurerm_system_center_virtual_machine_manager_availability_set.this"]
asym -->|"because"| why
why -->|"so"| check
check -->|"and"| flag
flag -->|"validated"| this
name -->|"consequence"| detach
detach -->|"lifecycle"| this
member -->|"so"| empty
empty -->|"and"| nores
nores -->|"scope"| this
this -->|"creates the set in VMM"| keystone
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001f3f,color:#fff;
classDef sib fill:#eef2f7,stroke:#b8c4d0,color:#1b1b1b;
class this me;
class keystone keystone;
class asym,why,check,flag,name,detach,member,empty,nores sib;
Resource inventory
| Resource | Count | Notes |
|---|---|---|
azurerm_system_center_virtual_machine_manager_availability_set.this |
1 | The keystone. Creates an object in VMM. |
timeouts block |
0..1 | All four operations. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Azure resource provider | Microsoft.ScVmm, API version 2023-10-07 |
| Provider block | None in this module. The caller configures provider "azurerm", including the mandatory features {} block, and supplies authentication. |
Schema notes that bite — confirmed against the live provider schema and its documentation:
- It takes
system_center_virtual_machine_manager_server_id, not..._server_inventory_item_id. The provider accepts an inventory item ID and fails at apply. - Every argument except
tagsis force-new, includingname. - The name is the set's name in VMM, not just in Azure — this resource creates the set.
- A destroy removes the availability set from the VMM fabric, not just an Azure record.
- Membership is not on this resource. It lives on the VM instance's
system_center_virtual_machine_manager_availability_set_ids. locationmust match the custom location's region, and the mismatch surfaces only at apply.- ARM resource type:
Microsoft.ScVmm/availabilitySets. lifecycleis not valid inside amoduleblock, so a caller cannot addprevent_destroy.
| Operation | Role | Scope |
|---|---|---|
| Creating this resource | Azure Arc SCVMM Administrator | the subscription or resource group holding the VMM server resource |
| Using the set when provisioning a VM (later, possibly a different identity) | Azure Arc SCVMM VM Contributor | the subscription or resource group where VMs are provisioned |
Contributor or Owner at the same scope also work, and are broader than needed.
ℹ️ Creating the set is an administrator action; using it is not. That split is what lets a platform team define placement policy while an app team provisions into it.
- The
Microsoft.ScVmmresource provider registered on the subscription. - The VMM server already registered with Azure and reporting connected.
- Enough hosts in the placement scope to make the set meaningful. VMM's host inventory is invisible to a plan, and a set across a single host separates nothing.
- A resource group in the same region as the custom location.
terraform-azurerm-system-center-virtual-machine-manager-availability-set/
├── providers.tf # required_version + the pinned azurerm provider. No provider block.
├── variables.tf # name, resource_group_name, location, custom_location_id,
│ # system_center_virtual_machine_manager_server_id, tags, timeouts
├── main.tf # the keystone, one resource
├── outputs.tf # id first, then placement, then creates_an_object_in_vmm
├── README.md # this document
├── SCOPE.md # the cross-module contract and the asymmetry rationale
├── LICENSE # MIT
└── .gitignore
provider "azurerm" {
features {}
}
module "vmm_avset_app" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-availability-set.git?ref=v1.0.0"
name = "vmmas-app"
resource_group_name = module.rg.name
# Both from the VMM server module — one source of truth.
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id
# ⚠️ The VMM SERVER's id — NOT an inventory item. See example 1.
system_center_virtual_machine_manager_server_id = module.vmm.id
tags = { tier = "app" }
}ℹ️ The caller configures the provider, its authentication, and the mandatory
features {}block. This module declares none of them.
⚠️ Creating the set is only half the job — a VM has to join it (example 3).
Consumes
| Input | Type | Source module |
|---|---|---|
resource_group_name |
string |
terraform-azurerm-resource-group → name |
location |
string |
the VMM server module → location |
custom_location_id |
string |
the VMM server module → custom_location_id |
system_center_virtual_machine_manager_server_id |
string |
the VMM server module → id (not an inventory item) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The availability set's Resource ID. | the virtual-machine-instance module's system_center_virtual_machine_manager_availability_set_ids |
name |
The set's name — in VMM as well as Azure. Force-new. | review |
resource_group_name / location / custom_location_id |
Placement. Force-new. | review |
system_center_virtual_machine_manager_server_id |
The VMM server. Force-new. | review |
creates_an_object_in_vmm |
Always true. |
destroy-behaviour review |
No secret is accepted and none is emitted.
Values these examples reference but do not create are declared inputs:
variable "scoped_resource_id" {
description = "scoped resource id of an existing resource these examples reference."
type = string
}1 · ⚠️ The input that differs from its siblings
# ✅ THIS module — the VMM SERVER's Resource ID.
system_center_virtual_machine_manager_server_id = module.vmm.id# ✅ Its three siblings — an INVENTORY ITEM id.
system_center_virtual_machine_manager_server_inventory_item_id = local.vmm_clouds["Production Cloud"]# ❌ The natural mistake after writing the other three. Rejected at plan.
system_center_virtual_machine_manager_server_id = local.vmm_clouds["Production Cloud"]Error: Invalid value for variable
system_center_virtual_machine_manager_server_id must be a complete VMM server ID
of the form /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ScVmm/
vmmServers/<name>, MATCHING THAT CAPITALISATION EXACTLY — note
`Microsoft.ScVmm` and `vmmServers`, which almost nothing spells the way you would
guess. The provider parses it with a case-sensitive segment parser. An inventory
item ID is the other common mistake here — that is what the cloud, template and
virtual-network modules take, not this one.
💡 Why the difference exists: the cloud, template and VM network already exist in VMM, so they are addressed by the inventory item that describes them. An availability set does not exist yet — Azure creates it in VMM on your behalf — so there is nothing to look up, and the module takes the server it should be created on.
ℹ️ The provider accepts the wrong ID and fails at apply. This module rejects it at plan time with an error that names the distinction, because in isolation the check is unremarkable and in this family it is the one most likely to save an apply.
💡 If you find yourself reaching for an inventory-items data source here, that is the tell.
2 · 🧱 This module creates; its siblings project
PROJECTION modules (cloud, virtual-machine-template, virtual-network):
the VMM object already exists -> destroy removes the AZURE RECORD only
output: projects_an_existing_vmm_object = true
THIS module:
Azure creates the object in VMM -> destroy removes it FROM THE VMM FABRIC
output: creates_an_object_in_vmm = true
⚠️ A destroy here is not a metadata operation. It removes an availability set from VMM, which changes placement for anything still referencing it.
💡 The two constant outputs exist as a matched pair. Read side by side in a state review they answer the only question that matters before a destroy: does this remove a record, or remove infrastructure?
ℹ️ A
CanNotDeletelock is a reasonable precaution once VMs are placed in the set:
resource "azurerm_management_lock" "avset_no_delete" {
name = "vmmas-app-no-delete"
scope = module.vmm_avset_app.id
lock_level = "CanNotDelete"
notes = "Deleting this set changes placement for the VMs in it"
}
⚠️ A caller cannot addprevent_destroy—lifecycleis not valid inside amoduleblock — so the lock is the available control.
3 · 🔗 Membership is expressed on the VM, not here
module "vmm_avset_app" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-availability-set.git?ref=v1.0.0"
custom_location_id = var.custom_location_id
location = "eastus2"
name = "vmm-avset-app-example"
resource_group_name = "rg-example"
system_center_virtual_machine_manager_server_id = var.system_center_virtual_machine_manager_server_id
}
# FRAGMENT: only the membership argument is shown -- the instance's own required inputs
# (scoped_resource_id, custom_location_id, infrastructure) are in that module's Quick Start.
module "vm_instance_01" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"
# ⚠️ THIS is where a VM joins the set.
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_app.id]
# ...
}ℹ️ The provider places VMs from the instance side. So this module has no membership input, cannot place anything, and cannot report what is in the set.
💡 Why no membership input here? Adding one would mean either a second write path to the same relationship — two places that can disagree — or a synthetic input that does not reflect the API. This suite documents the split instead.
⚠️ An empty set is a valid, silent outcome. Creating the set and forgetting to place anything in it produces no error, no warning and no plan difference. Checkis_in_availability_seton the VM modules, not here.
ℹ️ Membership is a list on the VM, so one VM can belong to more than one set.
4 · A set that separates two VMs
module "vmm_avset_web" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-availability-set.git?ref=v1.0.0"
name = "vmmas-web"
resource_group_name = module.rg.name
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id
system_center_virtual_machine_manager_server_id = module.vmm.id
}
module "vm_web_01" {
# ...
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_web.id]
}
module "vm_web_02" {
# ...
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_web.id]
}💡 The point of the set is the second VM. One VM in a set achieves nothing; the set is a request that VMM keep its members apart.
⚠️ And VMM has to be able to honour it. A placement scope with one host cannot separate anything, and this module cannot see the host inventory. Nothing here validates or reports that.
ℹ️ Which is why no output on this module — and no wording in this document — describes the set as delivering availability.
5 · 🔁 The name is not cosmetic
Force-new: name resource_group_name location custom_location_id
system_center_virtual_machine_manager_server_id
Not force-new: tags
⚠️ Unlike the projection modules, this module'snameis the object's name in VMM. The projections name an Azure record; this names a real availability set.
🔴 So renaming it destroys the set and creates a new one — which detaches every VM currently in it. And because membership lives on the instance side, that detachment does not appear in this module's plan. You will see one resource replaced; you will not see the VMs that lost their placement guarantee.
💡 Decide the name before the first apply, and treat a rename as a placement change requiring a review of every VM that references the set.
ℹ️ VMM naming conventions are worth following here, because this name shows up in VMM's own tooling, not just in Azure.
6 · Several sets for a tiered application
locals {
tiers = ["web", "app", "db"]
}
module "vmm_avset" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-availability-set.git?ref=v1.0.0"
for_each = toset(local.tiers)
name = "vmmas-${each.key}"
resource_group_name = module.rg.name
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id
system_center_virtual_machine_manager_server_id = module.vmm.id
tags = { tier = each.key }
}💡 One set per tier is the usual shape: separating web servers from each other is what protects the tier, and separating a web server from a database server is not what a set is for.
ℹ️
for_eachover a set of strings keeps the keys stable — adding a fourth tier does not disturb the first three.
⚠️ Each set is force-new onname, so"vmmas-${each.key}"means renaming a tier destroys and recreates that set (example 5).
7 · `location` and `custom_location_id` come from one place
# ✅
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id# ⚠️ Works until someone changes one and not the other.
location = "eastus"
custom_location_id = "/subscriptions/.../customLocations/cl-vmm-dc1"ℹ️ The region must match the custom location's region, and the mismatch is only detected at apply.
💡 The VMM server module emits both precisely so a composition states them once. Six modules in this family need the same pair.
⚠️ custom_location_idis shape-validated — it must be aMicrosoft.ExtendedLocation/customLocationsResource ID. A resource-bridge ID is the usual substitute and the provider accepts it before failing at apply.
8 · Tags reach the Azure record
tags = {
tier = "app"
owner = "platform-virtualization"
}ℹ️ The only non-force-new field on this resource — so tags are the one thing here that can be corrected without a replacement.
⚠️ They tag the Azure resource. They do not reach VMM, and they do not reach the VMs placed in the set.
💡 Two of the seven modules in this family have no
tagsat all — the VM instance and the guest agent. Tag their Arc machines instead.
9 · What a review should assert
output "avset_posture" {
value = {
id = module.vmm_avset_app.id
name = module.vmm_avset_app.name # the VMM name too
creates = module.vmm_avset_app.creates_an_object_in_vmm # always true
server = module.vmm_avset_app.system_center_virtual_machine_manager_server_id
}
}
# ⚠️ Membership is asserted on the VMs, not here -- these are the two instance modules from the
# example above, each carrying this set's id.
# FRAGMENT: the instances' own required inputs are in that module's Quick Start.
module "vm_web_01" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_web.id]
}
# FRAGMENT: as above.
module "vm_web_02" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_web.id]
}
output "avset_members" {
value = {
web_01 = module.vm_web_01.is_in_availability_set
web_02 = module.vm_web_02.is_in_availability_set
}
}ℹ️
createsis a constanttrue, and it is in the outputs so that "what does a destroy of this actually remove?" has a documented answer.
⚠️ The second output is the important one. This module cannot tell you whether the set has members; only the VM modules can. A set with no members is the failure mode that produces no error.
💡 Even both outputs together do not establish resilience — that depends on VMM's host count and placement rules (example 4).
10 · Importing a set created from the portal
terraform import 'module.vmm_avset_app.azurerm_system_center_virtual_machine_manager_availability_set.this' \
"/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-vmm/providers/Microsoft.ScVmm/availabilitySets/vmmas-app"
⚠️ Checknameandsystem_center_virtual_machine_manager_server_idfirst after importing. Both are force-new, so a mismatch proposes a replacement — which would detach every VM in the set (example 5).
💡 Confirm an empty plan before adding anything else. An import that plans a replacement on an availability set with live members is the worst combination in this family.
ℹ️ Import does not tell you what is in the set. Check the VM modules'
is_in_availability_setoutputs, or VMM itself.
11 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
data "azurerm_client_config" "current" {}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-vmm-dc1"
location = "eastus"
}
module "kv" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
name = "kv-vmm-dc1"
resource_group_name = module.rg.name
location = module.rg.location
tenant_id = data.azurerm_client_config.current.tenant_id
}
data "azurerm_key_vault_secret" "vmm_password" {
name = "vmm-arc-svc-password"
key_vault_id = module.kv.id
}
# ── The anchor ────────────────────────────────────────────────────────────────
module "vmm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-server.git?ref=v1.0.0"
name = "vmm-contoso-dc1"
resource_group_name = module.rg.name
location = module.rg.location
custom_location_id = var.custom_location_id
fqdn = "vmm.contoso.com"
username = var.vmm_admin_username
password = data.azurerm_key_vault_secret.vmm_password.value
}
# ── Projections take an INVENTORY ITEM… ───────────────────────────────────────
data "azurerm_system_center_virtual_machine_manager_inventory_items" "clouds" {
inventory_type = "Cloud"
system_center_virtual_machine_manager_server_id = module.vmm.id
}
locals {
vmm_clouds = { for i in data.azurerm_system_center_virtual_machine_manager_inventory_items.clouds.inventory_items : i.name => i.id }
}
module "vmm_cloud_prod" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-cloud.git?ref=v1.0.0"
name = "vmmcloud-prod"
resource_group_name = module.rg.name
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id
system_center_virtual_machine_manager_server_inventory_item_id = local.vmm_clouds["Production Cloud"]
}
# ── …while THIS module takes the SERVER id (example 1) ────────────────────────
module "vmm_avset" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-availability-set.git?ref=v1.0.0"
for_each = toset(["web", "app"])
name = "vmmas-${each.key}"
resource_group_name = module.rg.name
location = module.vmm.location
custom_location_id = module.vmm.custom_location_id
system_center_virtual_machine_manager_server_id = module.vmm.id
tags = { tier = each.key }
}
# ── Two VMs, so the set has something to separate (example 4) ─────────────────
module "vm_arc" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-arc-machine.git?ref=v1.0.0"
for_each = toset(["vm-web-01", "vm-web-02"])
name = each.key
resource_group_name = module.rg.name
location = module.rg.location
kind = "SCVMM"
tags = { tier = "web" }
}
module "vm_web" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"
for_each = toset(["vm-web-01", "vm-web-02"])
scoped_resource_id = module.vm_arc[each.key].id
custom_location_id = module.vmm.custom_location_id
infrastructure = {
system_center_virtual_machine_manager_template_id = var.vmm_template_id
system_center_virtual_machine_manager_cloud_id = module.vmm_cloud_prod.id
system_center_virtual_machine_manager_virtual_machine_server_id = module.vmm.id
checkpoint_type = "Production"
}
hardware = { cpu_count = 2, memory_in_mb = 4096 }
# ⚠️ MEMBERSHIP IS EXPRESSED HERE, not in the availability-set module.
system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset["web"].id]
timeouts = { create = "2h" }
}
resource "azurerm_management_lock" "avset_no_delete" {
for_each = module.vmm_avset
name = "${each.value.name}-no-delete"
scope = each.value.id
lock_level = "CanNotDelete"
notes = "Deleting this set changes placement for the VMs in it"
}
output "avset_posture" {
value = {
for k, m in module.vmm_avset : k => {
id = m.id
name = m.name
creates = m.creates_an_object_in_vmm # always true
}
}
}
# ⚠️ The membership check that the availability-set module cannot answer.
output "avset_members" {
value = { for k, m in module.vm_web : k => m.is_in_availability_set }
}🔒 What the composition gets right: the server ID here and an inventory item ID on the projection, both regional values wired from the anchor,
CanNotDeletelocks over sets with live members, two VMs so the set separates something, and a membership output taken from the VM modules rather than assumed.
⚠️ What no plan will tell you: whether the placement scope has more than one host, whether VMM can honour the separation alongside its other constraints, or whether every VM you expected actually joined.
💡 The membership output is the one to keep. A set with no members produces no error anywhere (example 3).
12 · Everything this module deliberately does not do
NOT in scope, and why:
membership expressed on the VM instance; two write paths to one
relationship would be able to disagree
VMM placement rules fabric configuration; not readable or writable here
host inventory invisible to a plan, so no validation can depend on it
a resilience claim would require the two above
the VMM object's
prior existence there is none — this module creates it (example 2)
💡 The pattern across this family: where a constraint cannot be checked at plan time, it is documented and no fake validation is written for it. A check that looked authoritative and was not would be worse than none.
ℹ️ What is validated: the server ID's shape (example 1), the custom location's shape, and non-empty names. All three catch real, common mistakes.
⚠️ What is documented but unenforceable: host count, VMM placement behaviour, and whether the set has members. Nothing in this module pretends otherwise.
| Input | Type | Default | Notes |
|---|---|---|---|
name |
string |
— | Required. Force-new. |
resource_group_name |
string |
— | Required. Force-new. |
location |
string |
— | Required. Force-new. Must match the custom location's region. |
custom_location_id |
string |
— | Required. Force-new. Shape-validated. |
system_center_virtual_machine_manager_server_id |
string |
— | Required. Force-new. |
tags |
map(string) |
{} |
The only non-force-new field. |
timeouts |
object(...) |
null |
All four operations. |
Full schemas
variable "system_center_virtual_machine_manager_server_id" {
type = string
# The check that exists because of the FAMILY, not because of the field: three
# near-identical siblings take an inventory item in this position.
validation {
condition = can(regex("^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft[.]ScVmm/vmmServers/[^/]+$", var.system_center_virtual_machine_manager_server_id))
error_message = "system_center_virtual_machine_manager_server_id must be a complete VMM server ID ... MATCHING THAT CAPITALISATION EXACTLY ... An inventory item ID is the other common mistake here …"
}
}
variable "custom_location_id" {
type = string
validation {
condition = can(regex("(?i)/providers/Microsoft\\.ExtendedLocation/customLocations/", var.custom_location_id))
error_message = "custom_location_id must be a Microsoft.ExtendedLocation/customLocations Resource ID."
}
}| Output | Description | Kind |
|---|---|---|
id |
The Resource ID of the availability set | Passthrough |
name |
The name of the availability set | Passthrough |
resource_group_name |
The resource group holding the availability set | Passthrough |
location |
The region of the availability set | Passthrough |
custom_location_id |
The custom location backing the VMM environment | Passthrough |
system_center_virtual_machine_manager_server_id |
The VMM server this set belongs to | Passthrough |
creates_an_object_in_vmm |
Always true, and emitted as the counterpart to the projection modules' projects_an_existing_vmm_object: destroying this resource removes an availability set that Azure created in VMM, not just an Azure representation |
Constant |
every_argument_except_tags_is_force_new |
Always true | Constant |
creates_the_object_in_vmm_rather_than_projecting_one |
Always true, and it is the opposite of the three sibling modules in this family | Constant |
requires_a_healthy_arc_resource_bridge |
Always true, and it is the dependency most likely to make an apply fail for reasons that look unrelated | Constant |
No secret is accepted and none is emitted. Membership is not an output here — it is on the VM modules.
-
The server-ID validation exists because of the family, not because of the field. In isolation, a shape check on one Resource ID is unremarkable. In a family where three near-identical siblings take a different ID in the same position, it is the check most likely to save an apply — so its error message names the distinction rather than restating the pattern.
-
creates_an_object_in_vmmis a constanttrue, designed as half of a pair. The three projection modules emitprojects_an_existing_vmm_object. Read side by side in a state review, the pair answers the question that matters before a destroy: does this remove a record, or remove infrastructure? -
The
namefield is called out as non-cosmetic because the projection modules trained the opposite intuition. Here the name reaches VMM, and a rename detaches every member — a consequence that does not appear in this module's plan, because membership lives elsewhere. -
Membership is deliberately absent. The provider places VMs from the instance side. A membership input here would be either a second write path to one relationship — two places able to disagree — or a synthetic input that does not reflect the API. The split is documented instead, in both directions: this module says membership is elsewhere, and the VM module says the set cannot report its members.
-
An empty set is acknowledged as a silent failure mode. Creating a set and forgetting to place anything in it produces no error and no plan difference, so the review pattern points at the VM modules'
is_in_availability_setrather than at anything here. -
No claim is made about resilience, in the code or the prose. The set is a placement request to VMM; whether VMM can honour it depends on host count and other constraints this module cannot see. A set across a single host separates nothing, so no output is named or described as though it guaranteed availability.
-
Where a constraint cannot be checked, no fake validation is written. Host count, VMM placement behaviour and membership are all documented as unenforceable. A check that looked authoritative and was not would be worse than none.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Wrong-ID pastes | an inventory item ID rejected at plan, with the family difference named | — |
| Destroy semantics | creates_an_object_in_vmm emitted; a management lock recommended |
destroy anyway, knowingly |
| Rename surprise | the VMM-name consequence and the invisible detachment documented | rename, knowingly |
| Membership confusion | documented in both directions; the review pattern points at the VM modules | — |
| Resilience honesty | no output or wording claims availability | — |
| Region mismatch | both regional values wired from the VMM server module in every example | hard-code literals, knowingly |
| Custom-location pastes | resource-bridge and resource-group IDs rejected at plan | — |
| Unenforceable constraints | documented, with no fake validation written | — |
| Secrets | none accepted, none emitted | — |
- Before the first apply: settle the name — it reaches VMM and a rename detaches members.
- Before assuming the set works: check the VMs'
is_in_availability_set, not this module. - Before claiming resilience: confirm the placement scope has more than one host.
- Before a destroy: this removes an object from VMM, not just an Azure record.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the source to a tag —
?ref=v1.0.0— never a branch. - Plan-only from here. A human applies from CI.
⚠️ A destroy removes the availability set from VMM. Use aCanNotDeletelock once VMs are placed in it;prevent_destroyis not available to a module caller.⚠️ A rename is a replacement, and it detaches every member without showing them in the plan.- ℹ️ Only
tagsupdates in place. - ℹ️ Creating the set reaches through to VMM, so allow more time than a pure control-plane operation.
- ℹ️ Membership lives on the VM instance module. Creating the set is half the job.
terraform validate and terraform fmt -check are the offline gate. They confirm:
system_center_virtual_machine_manager_server_idis a completevmmServers/<name>ID, anchored at both ends and matched case-sensitively (Microsoft.ScVmm,vmmServers) — an inventory item ID is rejected, and so is a lower-cased one, which is what the provider's own parser does;custom_location_idis acustomLocationsResource ID;name,resource_group_nameandlocationare not blank;- the module declares no
providerblock.
💡 These were proved by evaluating the conditions in
terraform consoleinside the module — which does fire root-module variable validations, unliketerraform validateon a calling configuration. An inventory item ID in the server field fails; the VMM server module'sidpasses.
What only plan and apply exercise:
- whether the resource group, custom location and VMM server exist;
- whether the region matches the custom location's;
- whether VMM accepts the set.
What no Terraform command checks at any stage:
- whether the placement scope contains more than one host — and therefore whether the set can separate anything;
- whether VMM can honour the separation alongside its other placement constraints;
- whether any VM ever joined the set — membership is on the VM instance resource;
- whether the name follows your VMM naming conventions, which matters because it reaches VMM.
Outputs:
creates_an_object_in_vmm = true
custom_location_id = "/subscriptions/00000000-.../providers/Microsoft.ExtendedLocation/customLocations/cl-vmm-dc1"
id = "/subscriptions/00000000-.../resourceGroups/rg-vmm-dc1/providers/Microsoft.ScVmm/availabilitySets/vmmas-app"
location = "eastus"
name = "vmmas-app"
resource_group_name = "rg-vmm-dc1"
system_center_virtual_machine_manager_server_id = "/subscriptions/00000000-.../providers/Microsoft.ScVmm/vmmServers/vmm-contoso-dc1"
⚠️ creates_an_object_in_vmm = trueis the destroy warning, in the outputs (example 2).
⚠️ Note what is not here: members. No output on this module can tell you whether the set contains anything (example 3).
💡
system_center_virtual_machine_manager_server_idrunning from/subscriptions/…to/vmmServers/vmm-contoso-dc1— complete, with nothing appended, and capitalised exactly so — is what a correct value looks like (example 1).
| Symptom | Cause | Fix |
|---|---|---|
| Plan rejects the server ID | An inventory item ID was supplied. | Use the VMM server module's id (example 1). |
| Apply fails saying the parent was not found | The right shape, wrong VMM server. | Wire it from the VMM server module. |
Plan rejects custom_location_id |
A resource-bridge or resource-group ID was supplied. | Use the customLocations ID (example 7). |
| Apply fails on the region | location does not match the custom location's region. |
Wire both from the VMM server module (example 7). |
| The set exists but nothing is in it | Membership is expressed on the VM. | Set ..._availability_set_ids on the VM module (example 3). |
| Expected this module to report members | It cannot — placement is on the instance side. | Check the VMs' is_in_availability_set (example 9). |
| VMs in the set landed on the same host | VMM could not honour the separation. | A fabric question — check host count and placement rules (example 4). |
| A rename destroyed the set | name is force-new and reaches VMM. |
Expected; treat a rename as a placement change (example 5). |
| VMs lost their placement guarantee after a rename | Detachment does not appear in this module's plan. | Review every VM referencing the set (example 5). |
Wanted prevent_destroy |
lifecycle is not valid inside a module block. |
Use a CanNotDelete lock (example 2). |
| An imported resource proposes a replacement | name or the server ID differs. |
Correct the configuration first (example 10). |
| Apply fails with the provider not registered | Microsoft.ScVmm is not registered. |
az provider register --namespace Microsoft.ScVmm. |
azurerm_system_center_virtual_machine_manager_availability_set— provider documentation.azurerm_system_center_virtual_machine_manager_virtual_machine_instance— where membership is expressed.- Overview of Azure Arc-enabled SCVMM — what the service does.
- Support matrix for Arc-enabled SCVMM — the RBAC role table.
- VMM availability sets — what VMM does with a set, and what it needs to honour one.
azurerm_management_lock— the destroy protection a module caller can actually apply.- Sibling modules:
terraform-azurerm-system-center-virtual-machine-manager-server,...-cloud,...-virtual-machine-template,...-virtual-network,...-virtual-machine-instance,...-virtual-machine-instance-guest-agent. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."