Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure System Center VMM Virtual Machine Instance Terraform Module

Brings a VMM virtual machine under Azure management β€” either deploying a new one from a VMM template or adopting one that already exists (azurerm_system_center_virtual_machine_manager_virtual_machine_instance). Targets hashicorp/azurerm ~> 4.0.

Terraform Provider Module Type Resources Destroy

🧩 Overview

  • πŸ–₯️ Deploys or adopts a VMM virtual machine, so its lifecycle can be driven from Azure.
  • πŸ”΄ A terraform destroy may delete the real virtual machine. The API distinguishes removing the Azure record from deleting the VM off the host; the provider exposes neither switch.
  • ⚠️ Two mutually exclusive modes, and the provider accepts a configuration that picks neither. This module requires exactly one at plan time.
  • πŸ’½ The service injects a disk you never declared. The provider's own fix is ignore_changes, which a module caller cannot write β€” so this module sets it, and says what that costs.
  • 🧩 An extension of an Arc machine, always named default, one per machine β€” hence no name, no location, no tags.
  • πŸ” network_interface and storage_disk edits restart the VM.
  • πŸ”’ An optional guest administrator password, force-new, plaintext in state.

πŸ’‘ Why it matters: This is the module that touches a running workload. Two of its most important behaviours β€” the delete semantics and the injected disk β€” are decided by the service and the language rather than by your configuration, so knowing them in advance is the difference between a routine apply and an outage.

❀️ Support this project

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


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

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;
Loading

🧬 What this module builds

flowchart TB
  modes["READ THIS FIRST: THERE ARE TWO MUTUALLY EXCLUSIVE MODES. Set infrastructure.template_id to DEPLOY a new VM from a VMM template, or infrastructure.inventory_item_id to ADOPT a VM that already exists in VMM."]
  optional["and the provider makes EVERY field inside the REQUIRED infrastructure block OPTIONAL, so a bare infrastructure block passes provider validation and then fails at apply having told you very little"]
  check["so this module requires EXACTLY ONE of the two at plan time, with an error message that explains BOTH modes rather than naming a field"]
  mode2["and mode 2 needs NO guest password at all - the VM already exists and already has its own credentials"]
  destroy["AND A DESTROY MAY DELETE THE REAL VIRTUAL MACHINE. The underlying API distinguishes remove-from-Azure from delete-from-host - the portal calls them Remove from Azure and Delete, PowerShell calls them DeleteMachine and DeleteFromHost."]
  noswitch["BUT THE TERRAFORM PROVIDER EXPOSES NEITHER SWITCH and does not document which behaviour its delete performs. Nothing this module can do makes that safer."]
  nolock["and a caller CANNOT EVEN ADD prevent_destroy, because lifecycle is NOT VALID inside a module block. Use a CanNotDelete management lock and a CI approval gate, and confirm the semantics in your own environment."]
  disk["THE SERVICE INJECTS A DISK YOU NEVER DECLARED: deploying from a VMM template always provisions a virtual disk, IDE bus by default, so without suppression EVERY subsequent plan reports a difference forever."]
  ignore["The provider's own guidance is ignore_changes on storage_disk - but a module caller cannot write a lifecycle block, so THIS MODULE SETS IT. The cost: disks declared here are applied AT CREATION and then NOT reconciled. A later edit produces no plan and no change."]
  emit["so storage_disk_changes_ignored is emitted as a constant true, and declared_storage_disk_names is named for what it is - what was ASKED FOR, not what the VM has now. Need reconciliation? Use the resource directly in a root module."]
  ext["THIS IS AN EXTENSION OF AN ARC MACHINE, always named default, one per machine - which is why there is NO name, NO resource_group_name, NO location and NO tags here. Tag the Arc machine instead."]
  lists["network_interface and storage_disk are ORDERED LISTS, not keyed maps, deliberately: the provider documents an interface's name as THE VMM NETWORK IT ATTACHES TO, not a free choice, so two interfaces on one network legitimately SHARE it and a map keyed on name could not express that."]
  restart["AND UPDATING network_interface OR storage_disk RESTARTS THE VM, per the provider's own notes. Workload-affecting, not metadata."]
  hw["the provider's docs CONTRADICT THEMSELVES on hardware: force-new in one sentence, restarted-on-update in the next. Both cannot be true. READ THE PLAN and believe the plan."]
  secret["operating_system carries the guest's local administrator password, so the whole variable is sensitive - and it is FORCE-NEW, so not a rotation mechanism. uses_admin_password is emitted as presence only, via nonsensitive, so a review can find which VMs put a credential in state."]
  avset["availability-set MEMBERSHIP IS EXPRESSED HERE, not in the availability-set module - which is why that module cannot tell you whether its set has any members. is_in_availability_set reports membership only, never resilience."]
  this["terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance"]
  keystone["azurerm_system_center_virtual_machine_manager_virtual_machine_instance.this"]

  modes -->|"because"| optional
  optional -->|"so"| check
  check -->|"and note"| mode2
  mode2 -->|"validated"| this
  destroy -->|"and"| noswitch
  noswitch -->|"and"| nolock
  nolock -->|"unmitigable"| this
  disk -->|"so"| ignore
  ignore -->|"therefore"| emit
  emit -->|"decided here"| this
  ext -->|"shape"| lists
  lists -->|"and"| restart
  restart -->|"and"| hw
  hw -->|"typing"| this
  secret -->|"posture"| this
  avset -->|"placement"| this
  this -->|"brings the VM under Azure management"| 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 modes,optional,check,mode2,destroy,noswitch,nolock,disk,ignore,emit,ext,lists,restart,hw,secret,avset sib;
Loading

Resource inventory

Resource Count Notes
azurerm_system_center_virtual_machine_manager_virtual_machine_instance.this 1 The keystone. Always named default.
infrastructure block 1 Required β€” and every field inside it is optional to the provider.
hardware block 0..1 CPU and memory. Documentation contradicts itself on force-new.
operating_system block 0..1 πŸ”’ Carries the guest administrator password. Force-new.
network_interface blocks 0..n Ordered list. Updates restart the VM.
storage_disk blocks 0..n Ordered list. Drift suppressed β€” see example 4.
lifecycle { ignore_changes } 1 Set by the module, because a caller cannot.

βœ… Provider / Versions

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:

  • The delete has no caller-selectable semantics. The API distinguishes removing the Azure resource from deleting the VM off the host; the provider exposes neither and documents which it performs.
  • infrastructure is required and every field inside it is optional. A bare block satisfies the provider.
  • The service provisions a template disk you did not declare, IDE bus by default.
  • lifecycle is not valid inside a module block, which blocks both prevent_destroy and the provider's own ignore_changes remedy.
  • The documentation contradicts itself on hardware β€” force-new in one sentence, restart-on-update in the next.
  • network_interface and storage_disk updates restart the VM.
  • operating_system is force-new in full, including admin_password.
  • vhd_type is Dynamic or Fixed β€” while the address-type fields are Dynamic or Static.
  • Ranges: cpu_count 1–64; memory fields 32–1048576 MB; bus 0–3; lun 0–63.
  • system_center_virtual_machine_manager_availability_set_ids is a list, and this is where membership lives.
  • No name, resource_group_name, location or tags β€” the instance is always default, one per Arc machine.
  • Import path: .../Microsoft.HybridCompute/machines/<machine>/providers/Microsoft.ScVmm/virtualMachineInstances/default.

πŸ”‘ Required Azure RBAC Roles / Permissions

Operation Role Scope
Provisioning the VM Azure Arc SCVMM VM Contributor the subscription or resource group where the VM is provisioned
Using the cloud, template and VM network Azure Arc SCVMM Private Cloud User the subscription or resource group containing those resources, or the resources themselves
Subsequent VM operations Azure Arc SCVMM VM Contributor the resource group containing the VM, or the VM itself
Reading the secret holding the guest administrator password, when one is set Key Vault Secrets User the secret, or the vault

Contributor or Owner at the same scope also work, and are broader than needed.

ℹ️ This is the module an application team is normally granted. The Private Cloud User grants on the projected cloud, template and network are what make self-service possible without handing over administration of the VMM connection.

Azure Prerequisites

  • The Microsoft.ScVmm resource provider registered on the subscription.
  • The VMM server registered with Azure and reporting connected, and the resource bridge running.
  • An Arc machine created with kind = "SCVMM" β€” its ID is scoped_resource_id.
  • Mode 1 (deploy): a projected VMM cloud and template, and capacity in the VMM fabric to satisfy the request. Neither the template's suitability nor the fabric's capacity is visible to a plan.
  • Mode 2 (adopt): the VM already existing in VMM and visible in the inventory.
  • A projected VMM VM network per interface.
  • Time. Deploying a new VM runs through the bridge into VMM; the provider's own create default is an hour.

πŸ“ Module Structure

terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance/
β”œβ”€β”€ providers.tf   # required_version + the pinned azurerm provider. No provider block.
β”œβ”€β”€ variables.tf   # scoped_resource_id, custom_location_id, infrastructure (required),
β”‚                  # hardware, operating_system (sensitive), network_interface,
β”‚                  # storage_disk, availability_set_ids, timeouts. No tags.
β”œβ”€β”€ main.tf        # the keystone, its five blocks, and the ignore_changes decision
β”œβ”€β”€ outputs.tf     # id, deployment_mode, sizing, and the drift-suppression flags
β”œβ”€β”€ README.md      # this document
β”œβ”€β”€ SCOPE.md       # the cross-module contract, incl. the delete-semantics finding
β”œβ”€β”€ LICENSE        # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "vm_arc" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-arc-machine.git?ref=v1.0.0"

  name                = "vm-app-01"
  resource_group_name = module.rg.name
  location            = module.rg.location
  kind                = "SCVMM"

  tags = { app = "billing" } # ⚠️ tags go HERE β€” this module has none
}

# πŸ”’ The guest administrator password, from a vault.
data "azurerm_key_vault_secret" "vm_admin" {
  name         = "vm-app-01-admin"
  key_vault_id = module.kv.id
}

module "vm_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"

  scoped_resource_id = module.vm_arc.id
  custom_location_id = module.vmm.custom_location_id

  # MODE 1 β€” deploy a new VM. Exactly one source is required; see example 2.
  infrastructure = {
    system_center_virtual_machine_manager_template_id               = module.vmm_template_ws2022.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    = 4
    memory_in_mb = 8192
  }

  operating_system = {
    computer_name  = "vmapp01"
    admin_password = data.azurerm_key_vault_secret.vm_admin.value
  }

  network_interface = [{
    name               = "VLAN-Prod"
    virtual_network_id = module.vmm_network_prod.id
    ipv4_address_type  = "Dynamic"
  }]
}

ℹ️ The caller configures the provider, its authentication, and the mandatory features {} block. This module declares none of them.

⚠️ Read examples 3 and 4 before your first apply β€” they cover the delete semantics and the injected disk.

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
scoped_resource_id string terraform-azurerm-arc-machine β†’ id, created with kind = "SCVMM"
custom_location_id string the VMM server module β†’ custom_location_id
infrastructure.…_cloud_id string the cloud module β†’ id
infrastructure.…_template_id string the virtual-machine-template module β†’ id (mode 1)
infrastructure.…_inventory_item_id string the inventory-items data source, inventory_type = "VirtualMachine" (mode 2)
infrastructure.…_virtual_machine_server_id string the VMM server module β†’ id
network_interface[].virtual_network_id string the virtual-network module β†’ id
system_center_virtual_machine_manager_availability_set_ids list(string) the availability-set module β†’ id
operating_system.admin_password string a secret store read at plan time

Emits

Output Description Consumed by
id The VM instance Resource ID, always ending /virtualMachineInstances/default. diagnostics, RBAC, imports
scoped_resource_id The Arc machine. the guest-agent module β€” it needs this, not id
deployment_mode deployed-from-template or adopted-existing-vm. destroy-behaviour review
checkpoint_type The checkpoint type, or null. data-protection review
cloud_id / template_id Placement and source. review
cpu_count / memory_in_mb Sizing. cost review
network_interface_names The VMM networks attached, in order. review
system_center_virtual_machine_manager_availability_set_ids Membership. resilience review
is_in_availability_set Derived β€” membership, not resilience. resilience review
computer_name The guest computer name. review
uses_admin_password Derived, presence only. security review
storage_disk_changes_ignored Always true. change planning
declared_storage_disk_names What was asked for at creation. review

No output emits any part of operating_system.admin_password.

πŸ“š Example Library

Values these examples reference but do not create are declared inputs:

variable "custom_location_id" {
  description = "custom location id of an existing resource these examples reference."
  type        = string
}

variable "system_center_virtual_machine_manager_server_id" {
  description = "system center virtual machine manager server id of an existing resource these examples reference."
  type        = string
}

The examples below reference existing resources by ID or name rather than creating them; this module owns only its own resource. Those references are declared inputs:

variable "vm_arc_adopted_id" {
  description = "id of an existing vm arc adopted that these examples reference but do not create."
  type        = string
}

variable "vmm_network_backup_id" {
  description = "id of an existing vmm network backup that these examples reference but do not create."
  type        = string
}
1 · 🧩 What this module is, and why it has no name or tags
The Resource ID this module produces:

  /subscriptions/.../resourceGroups/rg/providers/Microsoft.HybridCompute/machines/vm-app-01
    /providers/Microsoft.ScVmm/virtualMachineInstances/default
                                                       ^^^^^^^
                                              always "default", one per machine

ℹ️ This resource is an extension of an Arc machine, not a resource of its own. That is why the module takes no name, no resource_group_name, no location and no tags β€” all of those belong to terraform-azurerm-arc-machine.

module "vm_arc" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-arc-machine.git?ref=v1.0.0"

  name                = "vm-app-01"
  resource_group_name = module.rg.name
  location            = module.rg.location
  kind                = "SCVMM" # ⚠️ required for this pairing
  tags                = { app = "billing", env = "prod" }
}

πŸ’‘ Put your tags on the Arc machine. Cost allocation, ownership and policy all key off it, and it is the resource that actually appears in the Azure VM inventory.

⚠️ Because there is one instance per Arc machine, two VMs need two Arc machines. There is no for_each over VMs inside this module.

2 · ⚠️ Two modes, and the provider accepts neither
# MODE 1 β€” DEPLOY a new VM. The service builds it in VMM from the template.
infrastructure = {
  system_center_virtual_machine_manager_template_id = module.vmm_template_ws2022.id
  system_center_virtual_machine_manager_cloud_id    = module.vmm_cloud_prod.id
}
# MODE 2 β€” ADOPT a VM that already exists in VMM. Nothing is built.
infrastructure = {
  system_center_virtual_machine_manager_inventory_item_id = local.vmm_vms["APP-01"]
}

⚠️ infrastructure is a required block whose every field the provider marks optional. So this is accepted by the provider:

infrastructure = {} # ❌ rejected by THIS module at plan time
Error: Invalid value for variable

  infrastructure must state exactly ONE source for the VM: set
  system_center_virtual_machine_manager_template_id to DEPLOY a new VM from a
  VMM template, or system_center_virtual_machine_manager_inventory_item_id to
  ADOPT a VM that already exists in VMM. Setting neither is accepted by the
  provider and fails at apply; setting both is contradictory.

πŸ’‘ The error explains both modes rather than naming a missing field, because someone hitting it usually does not yet know there are two.

πŸ”’ Mode 2 needs no guest password at all β€” the VM already exists and already has its own credentials. If you are adopting, leave operating_system out entirely and keep a secret out of state.

ℹ️ deployment_mode is emitted so a review can see which mode produced a given VM without re-reading the inputs. It changes what a destroy costs (example 3).

3 Β· πŸ”΄ A destroy may delete the real virtual machine
The underlying API has TWO deletes:

  remove the Azure resource, KEEP the on-premises VM   (portal: "Remove from Azure",
                                                        PowerShell: -DeleteMachine)
  delete the VM FROM THE VMM HOST                      (portal: "Delete",
                                                        PowerShell: -DeleteFromHost)

πŸ”΄ The Terraform provider exposes neither switch, and does not document which behaviour its delete performs. There is no delete_from_host argument to set, and none for this module to expose.

⚠️ And a caller cannot add prevent_destroy, because lifecycle is not valid inside a module block. So the usual Terraform safeguard is unavailable.

What you can actually do:

# A lock the provider cannot bypass.
resource "azurerm_management_lock" "vm_no_delete" {
  name       = "vm-app-01-no-delete"
  scope      = module.vm_arc.id
  lock_level = "CanNotDelete"
  notes      = "Arc-enabled VMM VM; a Terraform destroy may delete the on-premises VM"
}

πŸ’‘ Plus a CI approval gate on any plan containing a destroy of this resource type, and verify the behaviour in a non-production environment before you need to rely on it. This document will not guess on your behalf.

⚠️ deployment_mode matters here: destroying an adopted-existing-vm risks deleting a VM that Terraform never created.

4 Β· πŸ’½ The disk you never declared, and the trade this module makes
The provider's own example carries this comment:

  lifecycle {
    // Service API always provisions a virtual disk with bus type IDE per
    // Virtual Machine Template by default, so it has to be ignored
    ignore_changes = [storage_disk]
  }

⚠️ Without suppression, every plan after the first reports a storage_disk difference β€” forever. CI drift checks fail permanently, and nobody reads plans that are always dirty.

πŸ”΄ A caller of a module cannot write that lifecycle block. So the choice has to be made inside the module, and this module makes it: ignore_changes = [storage_disk].

The cost, stated plainly:

storage_disk = [{ name = "data0", disk_size_gb = 128 }]
# Applied AT CREATION. βœ…

storage_disk = [{ name = "data0", disk_size_gb = 256 }] # later edit
# ⚠️ Produces NO plan and NO change. Terraform has stopped reconciling disks.

πŸ’‘ If reconciling disks with Terraform is a requirement, declare azurerm_system_center_virtual_machine_manager_virtual_machine_instance directly in your root module, where you can write your own lifecycle block and choose differently.

ℹ️ storage_disk_changes_ignored is emitted as a constant true, and declared_storage_disk_names is deliberately named for what it is β€” what was asked for at creation, not what the VM has now.

5 Β· Disks: ranges, and the enum near-miss
storage_disk = [
  { name = "os", template_disk_id = local.template_os_disk_id },
  { name = "data0", disk_size_gb = 256, bus = 0, bus_type = "SCSI", lun = 0, vhd_type = "Fixed" },
  { name = "logs", disk_size_gb = 128, bus = 0, bus_type = "SCSI", lun = 1, vhd_type = "Dynamic", storage_qos_policy_name = "Silver" },
]

⚠️ vhd_type is Dynamic or Fixed. The address-type fields on network_interface are Dynamic or Static. Writing Static for a disk is the natural mistake, and this module rejects it at plan:

storage_disk bus_type must be IDE or SCSI, and vhd_type must be Dynamic or
Fixed. Note vhd_type is Dynamic/Fixed β€” not Dynamic/Static, which is what the
address-type fields use.

ℹ️ Validated ranges, from the provider's documented limits: bus 0–3, lun 0–63, disk_size_gb a positive whole number.

ℹ️ An ordered list, not a keyed map: bus and LUN are the identity, and order is what the service preserves.

⚠️ template_disk_id is force-new. And remember example 4 β€” these are applied at creation and then not reconciled.

6 Β· Network interfaces, and why this one is a list
network_interface = [
  { name = "VLAN-Prod", virtual_network_id = module.vmm_network_prod.id, ipv4_address_type = "Dynamic" },
  { name = "VLAN-Prod", virtual_network_id = module.vmm_network_prod.id, ipv4_address_type = "Static" },
  { name = "VLAN-Backup", virtual_network_id = var.vmm_network_backup_id },
]

ℹ️ The provider documents name as the VMM virtual network the interface connects to β€” not a free-choice interface name. So two interfaces on the same network legitimately share it, which is why this is an ordered list rather than this suite's usual keyed map: a map keyed on name could not express the first two entries above.

πŸ’‘ Order matters to the guest, and a list preserves it.

⚠️ Updating this block restarts the VM, per the provider's own note. Adding a second NIC is not a metadata edit.

ℹ️ virtual_network_id points at the projected Azure resource for the same network. When both are supplied they should refer to the same thing; the provider does not reconcile them.

⚠️ ipv4_address_type = "Static" means the address is pinned by VMM. It is not the same as configuring an address inside the guest.

7 Β· Hardware, and a documentation contradiction
hardware = {
  cpu_count                = 8
  memory_in_mb             = 16384
  dynamic_memory_min_in_mb = 8192
  dynamic_memory_max_in_mb = 32768
}

⚠️ The provider's documentation contradicts itself here. It states that changing hardware forces a new resource, and immediately notes that the resource is restarted while updating it. Both cannot be true.

πŸ’‘ Read the plan and believe the plan. An edit that turns out to be a replacement on a production VM is not something to discover during the apply. This module does not pick a side in the documentation's argument, because guessing would be worse than saying so.

ℹ️ Validated ranges: cpu_count 1–64, memory fields 32–1048576 MB, and dynamic_memory_min_in_mb must not exceed dynamic_memory_max_in_mb β€” a cross-field check that is safe here because both fields live in the same variable.

ℹ️ limit_cpu_for_migration_enabled is left unset. It trades CPU features for live-migration mobility, and which matters is a workload property the module cannot know β€” so no default is invented.

8 Β· πŸ”’ The guest administrator password
operating_system = {
  computer_name  = "vmapp01"
  admin_password = data.azurerm_key_vault_secret.vm_admin.value
}

πŸ”’ The whole operating_system variable is marked sensitive because it carries the guest's local administrator password. So computer_name is sensitive too, and the outputs unwrap it with nonsensitive().

⚠️ Plaintext in state. sensitive = true redacts plan output and does nothing to the state file.

⚠️ Force-new in full, including the password β€” so this is not a rotation mechanism. Rotate the guest's local administrator password inside the guest or with configuration management, not by editing this value, which would replace the VM.

πŸ’‘ Adopting an existing VM (mode 2) needs none of this. The cleanest way to keep a guest credential out of state is not to supply one.

ℹ️ uses_admin_password is emitted as presence only, so a state review can find which VMs contributed a credential without exposing any.

9 Β· Availability sets: membership lives 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
}

module "vm_instance" {
  # ...and THIS is where a VM joins it
  system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_app.id]
}

ℹ️ The availability-set module creates the set and cannot place anything in it. That is why it cannot report whether its set has any members β€” and why an empty set is a silent, valid outcome.

⚠️ Validated: every entry must be a Microsoft.ScVmm/availabilitySets Resource ID, and duplicates are rejected.

⚠️ is_in_availability_set reports membership only. Whether the set delivers resilience depends on VMM's host count and placement rules β€” a set across a single host separates nothing, and this module cannot see the fabric.

πŸ’‘ So do not let is_in_availability_set = true into a resilience report unqualified.

10 Β· `checkpoint_type` is a data-protection setting
infrastructure = {
  # ...
  checkpoint_type = "Production"      # βœ… application-consistent
  # checkpoint_type = "ProductionOnly"
  # checkpoint_type = "Standard"
  # checkpoint_type = "Disabled"      # ⚠️ no checkpoints for this VM
}

ℹ️ Validated against the provider's four documented values: Disabled, Production, ProductionOnly, Standard. Leave it unset to take the service default.

⚠️ Disabled is a data-protection decision, not a performance tuning knob. It is emitted as an output precisely so a review can find the VMs that have no checkpoints.

πŸ’‘ This module does not default it. The right value depends on what the workload can tolerate during a checkpoint, which is not something a module can infer β€” but leaving it invisible would be worse, hence the output.

11 Β· Adopting an existing VMM virtual machine
data "azurerm_system_center_virtual_machine_manager_inventory_items" "vms" {
  inventory_type                                  = "VirtualMachine"
  system_center_virtual_machine_manager_server_id = module.vmm.id
}

locals {
  vmm_vms = {
    for i in data.azurerm_system_center_virtual_machine_manager_inventory_items.vms.inventory_items :
    i.name => i.id
  }
}

module "vm_instance_adopted" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"

  scoped_resource_id = var.vm_arc_adopted_id
  custom_location_id = module.vmm.custom_location_id

  infrastructure = {
    system_center_virtual_machine_manager_inventory_item_id         = local.vmm_vms["LEGACY-APP-01"]
    system_center_virtual_machine_manager_virtual_machine_server_id = module.vmm.id
  }

  # No operating_system block β€” the guest already exists. πŸ”’ No secret in state.
  # No template, no cloud: nothing is being built.
}

πŸ’‘ Match on name, not position. inventory_items[0] is fragile β€” the order is the service's.

⚠️ Adoption is where the destroy semantics matter most (example 3): destroying this record risks deleting a VM that Terraform never created and that predates your configuration. Lock it.

ℹ️ hardware and network_interface can still be supplied when adopting, but remember both restart the VM β€” which on an adopted production workload is a change window, not a plan.

12 Β· What a review should assert
output "vm_posture" {
  value = {
    mode        = module.vm_instance.deployment_mode              # adopted? then lock it
    checkpoints = module.vm_instance.checkpoint_type              # "Disabled" needs a reason
    secret      = module.vm_instance.uses_admin_password          # is a credential in state?
    disks       = module.vm_instance.storage_disk_changes_ignored # always true β€” know it
    avset       = module.vm_instance.is_in_availability_set       # membership, not resilience
    cpu         = module.vm_instance.cpu_count
    memory_mb   = module.vm_instance.memory_in_mb
  }
}

πŸ”’ Four rules worth encoding: mode == "adopted-existing-vm" should imply a management lock, checkpoints == "Disabled" needs a documented reason, secret == true means this VM put a guest password in state, and disks == true is always true β€” treat it as a reminder that Terraform is not reconciling disks.

πŸ’° cpu and memory_mb are the cost and the licensing position. Both are null when left to the template, which is itself worth flagging in a review.

⚠️ avset is not an assurance (example 9).

13 Β· πŸ—οΈ 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"
}

# ── The VMM connection (its README covers the credential) ─────────────────────
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
}

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
}

# πŸ”’ A SEPARATE secret from the VMM administrator credential β€” different owner.
data "azurerm_key_vault_secret" "vm_admin" {
  name         = "vm-app-01-admin"
  key_vault_id = module.kv.id
}

# ── Discovery, matched on name ────────────────────────────────────────────────
data "azurerm_system_center_virtual_machine_manager_inventory_items" "clouds" {
  inventory_type                                  = "Cloud"
  system_center_virtual_machine_manager_server_id = module.vmm.id
}

data "azurerm_system_center_virtual_machine_manager_inventory_items" "templates" {
  inventory_type                                  = "VirtualMachineTemplate"
  system_center_virtual_machine_manager_server_id = module.vmm.id
}

data "azurerm_system_center_virtual_machine_manager_inventory_items" "networks" {
  inventory_type                                  = "VirtualNetwork"
  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 }
  vmm_templates = { for i in data.azurerm_system_center_virtual_machine_manager_inventory_items.templates.inventory_items : i.name => i.id }
  vmm_networks  = { for i in data.azurerm_system_center_virtual_machine_manager_inventory_items.networks.inventory_items : i.name => i.id }
}

# ── Projections ───────────────────────────────────────────────────────────────
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"]
}

module "vmm_template_ws2022" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-template.git?ref=v1.0.0"

  name                                                           = "vmmtmpl-ws2022"
  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_templates["WS2022-Standard"]
}

module "vmm_network_prod" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-network.git?ref=v1.0.0"

  name                                                           = "vmmnet-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_networks["VLAN-Prod"]
}

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
  location                                        = module.vmm.location
  custom_location_id                              = module.vmm.custom_location_id
  system_center_virtual_machine_manager_server_id = module.vmm.id # the SERVER id
}

# ── The Arc machine: where name, location and TAGS live ───────────────────────
module "vm_arc" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-arc-machine.git?ref=v1.0.0"

  name                = "vm-app-01"
  resource_group_name = module.rg.name
  location            = module.rg.location
  kind                = "SCVMM"

  tags = { app = "billing", env = "prod", owner = "platform" }
}

# ── The VM ────────────────────────────────────────────────────────────────────
module "vm_instance" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-system-center-virtual-machine-manager-virtual-machine-instance.git?ref=v1.0.0"

  scoped_resource_id = module.vm_arc.id
  custom_location_id = module.vmm.custom_location_id

  # MODE 1: deploy. Exactly one source, validated at plan (example 2).
  infrastructure = {
    system_center_virtual_machine_manager_template_id               = module.vmm_template_ws2022.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" # not Disabled
  }

  hardware = {
    cpu_count    = 4
    memory_in_mb = 8192
  }

  operating_system = {
    computer_name  = "vmapp01"
    admin_password = data.azurerm_key_vault_secret.vm_admin.value
  }

  network_interface = [{
    name               = "VLAN-Prod"
    virtual_network_id = module.vmm_network_prod.id
    ipv4_address_type  = "Dynamic"
  }]

  storage_disk = [
    { name = "data0", disk_size_gb = 256, bus = 0, bus_type = "SCSI", lun = 0, vhd_type = "Fixed" },
  ]
  # ⚠️ Applied at creation, then NOT reconciled (example 4).

  system_center_virtual_machine_manager_availability_set_ids = [module.vmm_avset_app.id]

  timeouts = { create = "2h" }
}

# ⚠️ The only real protection against the delete ambiguity (example 3).
resource "azurerm_management_lock" "vm_no_delete" {
  name       = "vm-app-01-no-delete"
  scope      = module.vm_arc.id
  lock_level = "CanNotDelete"
  notes      = "A Terraform destroy here may delete the on-premises VM"
}

# ── Self-service, granted on the projections rather than the connection ───────
module "app_team_access" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.vmm_cloud_prod.id

  role_assignments = {
    app_team = {
      principal_id         = var.app_team_group_object_id
      role_definition_name = "Azure Arc SCVMM Private Cloud User"
      principal_type       = "Group"
      description          = "Place VMs on this VMM cloud without administering the VMM connection"
    }
  }
}

output "vm_posture" {
  value = {
    mode        = module.vm_instance.deployment_mode
    checkpoints = module.vm_instance.checkpoint_type
    secret      = module.vm_instance.uses_admin_password
    disks       = module.vm_instance.storage_disk_changes_ignored
    avset       = module.vm_instance.is_in_availability_set
    cpu         = module.vm_instance.cpu_count
    memory_mb   = module.vm_instance.memory_in_mb
  }
  # πŸ”’ No part of the guest password appears here, and none can.
}

πŸ”’ What the composition gets right: two separate secrets from Key Vault with different owners, tags on the Arc machine where they belong, checkpoint_type set explicitly rather than defaulted, a CanNotDelete lock over the delete ambiguity, a generous create timeout, and the app team granted on the projected cloud rather than on the VMM connection.

⚠️ What no plan will tell you: whether the VMM fabric has capacity for the request, whether the template's OS and disk layout suit the workload, whether the availability set spans more than one host, or which delete the provider will perform.

πŸ’‘ If the VM already exists in VMM, adopt it (example 11) and skip the guest password entirely.

πŸ“₯ Inputs

Input Type Default Notes
scoped_resource_id string β€” Required. Force-new. The Arc machine ID, shape-validated.
custom_location_id string β€” Required. Force-new. Shape-validated.
infrastructure object({...}) β€” Required. ⚠️ Exactly one of template_id / inventory_item_id, enforced.
hardware object({...}) null Ranges validated. ⚠️ Documentation contradicts itself on force-new.
operating_system object({...}) null sensitive. πŸ”’ Guest admin password. Force-new.
network_interface list(object({...})) [] Ordered list. Updates restart the VM.
storage_disk list(object({...})) [] Ordered list. ⚠️ Drift suppressed by the module.
system_center_virtual_machine_manager_availability_set_ids list(string) [] Shape- and duplicate-validated.
timeouts object(...) null All four. Create can take hours.

There is no tags variable β€” the provider exposes none on this resource.

Full schemas
variable "infrastructure" {
  type = object({
    checkpoint_type                                                = optional(string)
    system_center_virtual_machine_manager_cloud_id                 = optional(string)
    system_center_virtual_machine_manager_inventory_item_id         = optional(string)
    system_center_virtual_machine_manager_template_id               = optional(string)
    system_center_virtual_machine_manager_virtual_machine_server_id = optional(string)
  })

  # The guardrail: a bare infrastructure {} satisfies the PROVIDER.
  validation {
    condition = (
      (try(var.infrastructure.system_center_virtual_machine_manager_template_id, null) != null ? 1 : 0) +
      (try(var.infrastructure.system_center_virtual_machine_manager_inventory_item_id, null) != null ? 1 : 0)
    ) == 1
    error_message = "infrastructure must state exactly ONE source for the VM: …"
  }
  # ...plus checkpoint_type ∈ {Disabled, Production, ProductionOnly, Standard}
}

variable "storage_disk" {
  type = list(object({
    name                    = optional(string)
    disk_size_gb            = optional(number)
    bus                     = optional(number) # 0-3
    bus_type                = optional(string) # IDE | SCSI
    lun                     = optional(number) # 0-63
    vhd_type                = optional(string) # Dynamic | Fixed  (NOT Static)
    storage_qos_policy_name = optional(string)
    template_disk_id        = optional(string) # force-new
  }))
  default = []
  # ⚠️ main.tf sets lifecycle { ignore_changes = [storage_disk] } β€” see example 4.
}

variable "operating_system" {
  type = object({
    computer_name  = optional(string)
    admin_password = optional(string)
  })
  default   = null
  sensitive = true # the guest's local administrator password
}

🧾 Outputs

Output Description Sensitive
id The VM instance Resource ID. no
scoped_resource_id The Arc machine β€” what the guest-agent module needs. no
deployment_mode deployed-from-template or adopted-existing-vm. no
checkpoint_type The checkpoint type, or null. no
cloud_id / template_id Placement and source. no
cpu_count / memory_in_mb Sizing, or null when left to the template. no
network_interface_names VMM networks attached, in order. no
system_center_virtual_machine_manager_availability_set_ids Membership. no
is_in_availability_set Derived β€” membership, not resilience. no
computer_name Guest computer name, unwrapped from the sensitive object. no
uses_admin_password Derived, presence only. no
storage_disk_changes_ignored Always true. no
declared_storage_disk_names What was asked for, not what the VM has now. no

πŸ”’ No output emits any part of the guest administrator password.

🧠 Architecture Notes

  • The delete ambiguity leads the documentation because nothing in the module can fix it. The API has two deletes β€” remove-from-Azure and delete-from-host β€” and the provider exposes neither, nor documents which it performs. A caller cannot add prevent_destroy either, because lifecycle is not valid inside a module block. So the honest response is to state the ambiguity, point at a management lock and an approval gate as the real mitigations, and decline to guess which behaviour applies.

  • The two modes are enforced rather than documented. A required block whose every field the provider marks optional is exactly the case this suite's "make the type the contract" rule exists for: the provider takes an empty block, so the check belongs at plan time. The error message explains both modes, because someone hitting it usually does not know there are two.

  • ignore_changes = [storage_disk] is set inside the module, and the cost is stated in five places. The alternative is a permanent difference on every plan that a caller has no way to suppress. Where the language prevents a caller from making a decision, this suite's practice is to make it, name it, and emit it β€” hence storage_disk_changes_ignored.

  • declared_storage_disk_names is named for what it is. With reconciliation suppressed, an output that looked like current state would be actively misleading.

  • network_interface and storage_disk are ordered lists, departing from this suite's keyed-map default. The provider documents an interface's name as the VMM network it attaches to rather than a free-choice identifier, so two interfaces on one network legitimately share it and a keyed map could not express that. Order also matters to the guest; for disks, bus and LUN are the identity.

  • The hardware documentation contradiction is reported, not resolved. Force-new in one sentence and restart-on-update in the next cannot both hold, and the module has no way to determine which. Telling a reader to trust the plan is more useful than picking a side confidently.

  • deployment_mode is derived and emitted because one resource type means two different things, and the difference determines what a destroy costs. Adoption is the case where the delete ambiguity bites hardest.

  • is_in_availability_set reports membership and says so. Whether a set delivers resilience depends on VMM host count and placement rules the module cannot see, so nothing here is described as though it guaranteed availability.

  • uses_admin_password is presence only, unwrapped with nonsensitive(). Sensitivity is contagious, so every value read from operating_system is sensitive; the outputs unwrap the non-secret facts individually.

  • checkpoint_type is validated but not defaulted, and is emitted. The right value depends on what the workload tolerates during a checkpoint. Disabled is a data-protection posture, and leaving it invisible would be worse than leaving it unset.

  • No tags, and that is pointed at the Arc machine rather than left as an absence. Cost allocation and ownership key off the machine, which is also where the module's scoped_resource_id comes from.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Ambiguous destroy disclosed in six sections; lock + approval gate recommended destroy anyway, knowingly
Mode confusion a source is required at plan time, with both modes explained β€”
Avoiding a guest secret adoption named as the credential-free path deploy from a template and supply one
Guest password sensitive, state exposure and force-new disclosed; presence emitted β€”
Permanent plan noise storage_disk drift suppressed, and the cost emitted use the resource directly in a root module
Data protection checkpoint_type validated and emitted; Disabled made visible set Disabled, with a reason
Resilience honesty is_in_availability_set caveated as membership only β€”
Wrong-ID pastes the VM-instance ID rejected in scoped_resource_id β€”
Enum near-misses Static rejected for vhd_type, with the reason named β€”
Nonsense sizing out-of-range CPU, memory, bus and LUN rejected at plan β€”
Secrets in outputs no output reads the guest password β€”
  • Before the first apply: decide how this resource is protected from destruction (example 3).
  • Before deploying: could you adopt instead, and keep a guest password out of state?
  • Before editing hardware, network_interface or storage_disk: read the plan; two of the three restart the VM and the third may replace it.
  • Before reporting on resilience: is_in_availability_set is not an assurance.

πŸš€ Runbook

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 may delete the on-premises virtual machine. Put a CanNotDelete lock on the Arc machine and an approval gate on any destroy plan.
  • ⚠️ Read every plan. network_interface and storage_disk edits restart the VM; hardware may replace it.
  • ℹ️ Disk edits after creation do nothing β€” Terraform has stopped reconciling them (example 4).
  • ⏳ Deploying a new VM can take an hour or more. Set timeouts.create accordingly and do not cancel.
  • ℹ️ Tags, name and location belong to the Arc machine, not here.

πŸ§ͺ Testing

terraform validate and terraform fmt -check are the offline gate. They confirm:

  • scoped_resource_id is an Arc machine ID β€” an ID ending /virtualMachineInstances/default is rejected;
  • custom_location_id is a customLocations Resource ID;
  • infrastructure names exactly one VM source;
  • checkpoint_type, when set, is one of the four documented values;
  • cpu_count is 1–64 and whole; memory fields are 32–1048576 and whole; dynamic_memory_min_in_mb does not exceed dynamic_memory_max_in_mb;
  • every network_interface has a non-empty name, and its address-type fields are Dynamic or Static;
  • storage_disk bus is 0–3, lun is 0–63, disk_size_gb is positive and whole, bus_type is IDE/SCSI, vhd_type is Dynamic/Fixed;
  • availability-set IDs are availabilitySets IDs with no duplicates;
  • no output reads the guest password;
  • the module declares no provider block.

πŸ’‘ These were proved by evaluating the conditions in terraform console inside the module β€” which does fire root-module variable validations, unlike terraform validate on a calling configuration. A bare infrastructure = {}, a VM-instance ID in scoped_resource_id, vhd_type = "Static", bus = 9, an inverted dynamic-memory pair and a cloud ID in the availability-set list all fail. Two interfaces sharing one name pass, which is the point of the list model.

What only plan and apply exercise:

  • whether the Arc machine, cloud, template and VM network exist;
  • whether the VMM fabric can satisfy the request.

What no Terraform command checks at any stage:

  • which delete the provider performs β€” the central open question of this module;
  • whether the template's OS, disk layout and hardware profile suit the workload;
  • whether the availability set spans more than one host, and therefore whether it separates anything;
  • whether the state backend protects the guest password it may now hold;
  • whether checkpoint_type matches what the workload can tolerate.

πŸ’¬ Example Output

Outputs:

checkpoint_type                                            = "Production"
cloud_id                                                   = "/subscriptions/00000000-.../providers/Microsoft.ScVmm/clouds/vmmcloud-prod"
computer_name                                              = "vmapp01"
cpu_count                                                  = 4
declared_storage_disk_names                                = ["data0"]
deployment_mode                                            = "deployed-from-template"
id                                                         = "/subscriptions/00000000-.../providers/Microsoft.HybridCompute/machines/vm-app-01/providers/Microsoft.ScVmm/virtualMachineInstances/default"
is_in_availability_set                                     = true
memory_in_mb                                               = 8192
network_interface_names                                    = ["VLAN-Prod"]
scoped_resource_id                                         = "/subscriptions/00000000-.../providers/Microsoft.HybridCompute/machines/vm-app-01"
storage_disk_changes_ignored                               = true
system_center_virtual_machine_manager_availability_set_ids  = ["/subscriptions/00000000-.../providers/Microsoft.ScVmm/availabilitySets/vmmas-app"]
template_id                                                = "/subscriptions/00000000-.../providers/Microsoft.ScVmm/virtualMachineTemplates/vmmtmpl-ws2022"
uses_admin_password                                        = true

⚠️ storage_disk_changes_ignored = true is always true. Read it as "Terraform is not reconciling this VM's disks", not as a setting you chose.

⚠️ is_in_availability_set = true means membership, not resilience (example 9).

πŸ”’ uses_admin_password = true says a guest credential is in state. Nothing here reveals it.

πŸ’‘ scoped_resource_id is in the outputs because the guest-agent module needs exactly that value β€” not id.

πŸ” Troubleshooting

Symptom Cause Fix
Plan rejects scoped_resource_id The VM instance's own ID was passed instead of the Arc machine's. Use module.vm_arc.id (example 1).
Plan rejects infrastructure Neither or both VM sources were set. Exactly one (example 2).
Apply fails with an unhelpful error and no source set Only possible without this module β€” the provider accepts a bare block. Set one source (example 2).
A destroy deleted the on-premises VM The provider exposes no remove-from-Azure switch. Prevent it: lock plus approval gate (example 3).
Wanted prevent_destroy lifecycle is not valid inside a module block. Use a CanNotDelete management lock (example 3).
Every plan shows a storage_disk difference Would happen without suppression; this module suppresses it. Nothing to do (example 4).
Edited a disk and nothing happened Disk drift is ignored, by design. Use the resource directly if you need reconciliation (example 4).
Plan rejects vhd_type = "Static" The disk enum is Dynamic/Fixed; Static belongs to address types. Fixed or Dynamic (example 5).
Plan rejects a bus or lun Out of the documented range. bus 0–3, lun 0–63 (example 5).
Wanted to key network_interface by name Two interfaces may share a VMM network name. It is an ordered list, deliberately (example 6).
The VM restarted after a small edit network_interface and storage_disk updates restart it. Expected; plan it as a change window (example 6).
A hardware edit showed a replacement The provider's docs contradict themselves here. Believe the plan (example 7).
Plan rejects a dynamic-memory pair min exceeds max. Correct the pair (example 7).
A guest password edit showed a replacement operating_system is force-new in full. Rotate inside the guest (example 8).
Error: Output refers to sensitive values when wrapping this module A value read from operating_system was re-exported. Unwrap per element with nonsensitive().
Plan rejects an availability-set ID Not an availabilitySets ID, or a duplicate. Use the availability-set module's id (example 9).
An inventory lookup returns nothing Sync has not completed, or the VM does not exist. Wait and re-plan; match on name (example 11).
The apply has run for an hour Deploying through the bridge into VMM. Expected. Raise timeouts.create; do not cancel.
Wanted to tag this resource The provider exposes no tags. Tag the Arc machine (example 1).

πŸ”— Related Docs

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

Releases

Packages

Contributors

Languages