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). Targetshashicorp/azurerm ~> 4.0.
- π₯οΈ Deploys or adopts a VMM virtual machine, so its lifecycle can be driven from Azure.
- π΄ A
terraform destroymay 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 noname, nolocation, notags. - π
network_interfaceandstorage_diskedits 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.
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
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;
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. |
| 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.
infrastructureis 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.
lifecycleis not valid inside amoduleblock, which blocks bothprevent_destroyand the provider's ownignore_changesremedy.- The documentation contradicts itself on
hardwareβ force-new in one sentence, restart-on-update in the next. network_interfaceandstorage_diskupdates restart the VM.operating_systemis force-new in full, includingadmin_password.vhd_typeisDynamicorFixedβ while the address-type fields areDynamicorStatic.- Ranges:
cpu_count1β64; memory fields 32β1048576 MB;bus0β3;lun0β63. system_center_virtual_machine_manager_availability_set_idsis a list, and this is where membership lives.- No
name,resource_group_name,locationortagsβ the instance is alwaysdefault, one per Arc machine. - Import path:
.../Microsoft.HybridCompute/machines/<machine>/providers/Microsoft.ScVmm/virtualMachineInstances/default.
| 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 Usergrants on the projected cloud, template and network are what make self-service possible without handing over administration of the VMM connection.
- The
Microsoft.ScVmmresource 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 isscoped_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.
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
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.
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.
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, noresource_group_name, nolocationand notagsβ all of those belong toterraform-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 nofor_eachover 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"]
}
β οΈ infrastructureis a required block whose every field the provider marks optional. So this is accepted by the provider:
infrastructure = {} # β rejected by THIS module at plan timeError: 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_systemout entirely and keep a secret out of state.
βΉοΈ
deployment_modeis 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_hostargument to set, and none for this module to expose.
β οΈ And a caller cannot addprevent_destroy, becauselifecycleis not valid inside amoduleblock. 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_modematters here: destroying anadopted-existing-vmrisks 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 astorage_diskdifference β forever. CI drift checks fail permanently, and nobody reads plans that are always dirty.
π΄ A caller of a module cannot write that
lifecycleblock. 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_instancedirectly in your root module, where you can write your ownlifecycleblock and choose differently.
βΉοΈ
storage_disk_changes_ignoredis emitted as a constanttrue, anddeclared_storage_disk_namesis 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_typeisDynamicorFixed. The address-type fields onnetwork_interfaceareDynamicorStatic. WritingStaticfor 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:
bus0β3,lun0β63,disk_size_gba 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_idis 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
nameas 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 onnamecould 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_idpoints 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 changinghardwareforces 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_count1β64, memory fields 32β1048576 MB, anddynamic_memory_min_in_mbmust not exceeddynamic_memory_max_in_mbβ a cross-field check that is safe here because both fields live in the same variable.
βΉοΈ
limit_cpu_for_migration_enabledis 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_systemvariable is marked sensitive because it carries the guest's local administrator password. Socomputer_nameis sensitive too, and the outputs unwrap it withnonsensitive().
β οΈ Plaintext in state.sensitive = trueredacts 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_passwordis 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 aMicrosoft.ScVmm/availabilitySetsResource ID, and duplicates are rejected.
β οΈ is_in_availability_setreports 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 = trueinto 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.
β οΈ Disabledis 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.
βΉοΈ
hardwareandnetwork_interfacecan 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 == truemeans this VM put a guest password in state, anddisks == trueis always true β treat it as a reminder that Terraform is not reconciling disks.
π°
cpuandmemory_mbare the cost and the licensing position. Both are null when left to the template, which is itself worth flagging in a review.
β οΈ avsetis 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_typeset explicitly rather than defaulted, aCanNotDeletelock 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.
| 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. template_id / inventory_item_id, enforced. |
hardware |
object({...}) |
null |
Ranges validated. |
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. |
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
}| 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.
-
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_destroyeither, becauselifecycleis not valid inside amoduleblock. 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 β hencestorage_disk_changes_ignored. -
declared_storage_disk_namesis named for what it is. With reconciliation suppressed, an output that looked like current state would be actively misleading. -
network_interfaceandstorage_diskare ordered lists, departing from this suite's keyed-map default. The provider documents an interface'snameas 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
hardwaredocumentation 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_modeis 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_setreports 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_passwordis presence only, unwrapped withnonsensitive(). Sensitivity is contagious, so every value read fromoperating_systemis sensitive; the outputs unwrap the non-secret facts individually. -
checkpoint_typeis validated but not defaulted, and is emitted. The right value depends on what the workload tolerates during a checkpoint.Disabledis 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'sscoped_resource_idcomes from.
| 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_interfaceorstorage_disk: read the plan; two of the three restart the VM and the third may replace it. - Before reporting on resilience:
is_in_availability_setis not an assurance.
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
CanNotDeletelock on the Arc machine and an approval gate on any destroy plan. β οΈ Read every plan.network_interfaceandstorage_diskedits restart the VM;hardwaremay 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.createaccordingly and do not cancel. - βΉοΈ Tags, name and location belong to the Arc machine, not here.
terraform validate and terraform fmt -check are the offline gate. They confirm:
scoped_resource_idis an Arc machine ID β an ID ending/virtualMachineInstances/defaultis rejected;custom_location_idis acustomLocationsResource ID;infrastructurenames exactly one VM source;checkpoint_type, when set, is one of the four documented values;cpu_countis 1β64 and whole; memory fields are 32β1048576 and whole;dynamic_memory_min_in_mbdoes not exceeddynamic_memory_max_in_mb;- every
network_interfacehas a non-emptyname, and its address-type fields areDynamicorStatic; storage_diskbusis 0β3,lunis 0β63,disk_size_gbis positive and whole,bus_typeisIDE/SCSI,vhd_typeisDynamic/Fixed;- availability-set IDs are
availabilitySetsIDs with no duplicates; - no output reads the guest password;
- 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. A bareinfrastructure = {}, a VM-instance ID inscoped_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 onenamepass, 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_typematches what the workload can tolerate.
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 = trueis always true. Read it as "Terraform is not reconciling this VM's disks", not as a setting you chose.
β οΈ is_in_availability_set = truemeans membership, not resilience (example 9).
π
uses_admin_password = truesays a guest credential is in state. Nothing here reveals it.
π‘
scoped_resource_idis in the outputs because the guest-agent module needs exactly that value β notid.
| 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). |
azurerm_system_center_virtual_machine_manager_virtual_machine_instanceβ provider documentation, including theignore_changesguidance and thehardwarenote.azurerm_arc_machineβ the prerequisite resource; usekind = "SCVMM".- Create a VM on VMM using Azure Arc β the roles an app team needs.
- Delete a VMM-managed VM through Arc β the two deletes described in example 3.
Remove-AzScVmmVMβ where-DeleteMachineand-DeleteFromHostare documented.- Support matrix for Arc-enabled SCVMM β the RBAC role table.
- Sensitive values in Terraform state β why marking an input sensitive is not encryption.
- Sibling modules:
terraform-azurerm-system-center-virtual-machine-manager-server,...-cloud,...-virtual-machine-template,...-virtual-network,...-availability-set,...-virtual-machine-instance-guest-agent. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."