Opts one Virtual Machine into a Maintenance Configuration's schedule, built for
hashicorp/azurerm ~> 4.0. The configuration owns the window and the reboot behaviour; this module decides which machine joins it.
This module manages a single Azure Maintenance Assignment for a Virtual Machine (azurerm_maintenance_assignment_virtual_machine):
- π Binds one Maintenance Configuration to one Virtual Machine, which is what actually brings that machine under the configuration's schedule.
- ποΈ Owns membership only. The window, recurrence, reboot setting, and patch classifications all live on the configuration β this record just says "this VM participates".
- π§ Is fully immutable: all three fields force replacement, and the provider exposes no update operation.
- π― Names the machine explicitly, which is the auditable alternative to the query-based
terraform-azurerm-maintenance-assignment-dynamic-scope.
π‘ Why it matters: A maintenance configuration can reboot machines. Which machines it touches is therefore a change worth reviewing, and an explicit per-VM assignment makes that list a diff rather than the outcome of a query. When you do want the query, the dynamic-scope sibling exists β but then the filter becomes the thing to review.
If this module saves you time, consider supporting the work:
- β Star the repository on GitHub
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
MC["terraform-azurerm-maintenance-configuration"]
LVM["terraform-azurerm-linux-virtual-machine"]
WVM["terraform-azurerm-windows-virtual-machine"]
DS["terraform-azurerm-maintenance-assignment-dynamic-scope"]
ASSIGN["terraform-azurerm-maintenance-assignment-virtual-machine"]
TARGET["Virtual Machine opted into the maintenance window"]
MC -->|"id feeds maintenance_configuration_id"| ASSIGN
LVM -->|"id feeds virtual_machine_id"| ASSIGN
WVM -->|"id feeds virtual_machine_id"| ASSIGN
MC -->|"same configuration, query-matched instead"| DS
ASSIGN -->|"opts one named machine into the window"| TARGET
DS -->|"opts every matching machine into the window"| TARGET
classDef self fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001d33,color:#fff;
classDef neutral fill:#eef2f6,stroke:#b9c4d0,color:#1b2733;
class ASSIGN self;
class TARGET keystone;
class MC,LVM,WVM,DS neutral;
The configuration is upstream of both assignment styles. This module names a machine; the dynamic-scope sibling matches machines by query. Scale sets use terraform-azurerm-maintenance-assignment-virtual-machine-scale-set, and Dedicated Hosts use terraform-azurerm-maintenance-assignment-dedicated-host.
flowchart TB
subgraph IN["Inputs"]
V["virtual_machine_id (force-new)"]
M["maintenance_configuration_id (force-new)"]
L["location (force-new)"]
end
THIS["azurerm_maintenance_assignment_virtual_machine.this"]
subgraph BLK["Nested blocks"]
B1["timeouts (create / read / delete only)"]
end
subgraph OUT["Outputs"]
O1["id (nests under the VM)"]
O2["virtual_machine_id"]
O3["maintenance_configuration_id / location"]
end
V --> THIS
M --> THIS
L --> THIS
THIS --> B1
THIS -->|"emits"| O1
THIS --> O2
THIS --> O3
classDef self fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#001d33,color:#fff;
classDef neutral fill:#eef2f6,stroke:#b9c4d0,color:#1b2733;
class THIS keystone;
class V,M,L,B1,O1,O2,O3 neutral;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_maintenance_assignment_virtual_machine.this |
1 | The keystone assignment record binding one configuration to one VM. |
Nested blocks rendered from typed inputs: timeouts (single, with create / read / delete only β this resource has no update operation).
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None β the caller configures provider "azurerm" { features {} }, auth, and the subscription |
| API provider | Microsoft.Maintenance |
Schema notes that bite (verified against the live provider schema):
- π Every field is force-new.
virtual_machine_id,maintenance_configuration_id, andlocationall replace the assignment. Re-pointing a VM at a different configuration is a destroy-and-create, not an update. - β±οΈ Because nothing is updatable, the provider exposes no
updatetimeout. This module'stimeoutsobject carriescreate,read, anddeleteonly. Passingupdateis not an error -- Terraform's object-type conversion silently discards the undeclared key, so the value is dropped with no diagnostic at all. - π The resource ID nests under the Virtual Machine, not under the Maintenance Configuration:
β¦/virtualMachines/<vm>/providers/Microsoft.Maintenance/configurationAssignments/<name>. That is also why permission is needed at the VM's scope. - π« This resource type has no
nameinput β the service derives the assignment name. There is notagssupport either; neither is a module variable. - π One assignment binds one configuration to one VM. Assigning a second configuration to the same VM means a second instance of this module.
β οΈ Whether the assignment actually patches anything depends entirely on the configuration'sscopeandvisibility. A configuration built with anInGuestPatchscope behaves very differently from aHostone, and a scope mismatch is rejected at apply rather than at plan.- π§ For in-guest patching, the VM additionally needs its patch mode set appropriately (for example
AutomaticByPlatform) on the VM resource itself β that is a VM-module concern, not this one's.
Microsoft.Maintenance/configurationAssignments/write(plus/read,/delete) at the Virtual Machine's scope β the assignment is a child of the VM, so permission is needed there rather than on the configuration.Microsoft.Maintenance/maintenanceConfigurations/readon the configuration being assigned.Microsoft.Compute/virtualMachines/readon the target VM.- Both are carried by the built-in Contributor on the resource group, or by a custom role granting those actions.
- No data-plane or key permissions are needed; the assignment carries no secrets.
- An existing Maintenance Configuration, with a scope appropriate to virtual machines β create it with
terraform-azurerm-maintenance-configuration. - An existing Virtual Machine, in the region passed as
location. - The
Microsoft.Maintenanceresource provider registered on the target subscription. - For in-guest patching: the VM's patch mode configured to accept platform-orchestrated updates.
- The caller configures
provider "azurerm" { features {} }, auth, and the subscription; the module declares none of these.
terraform-azurerm-maintenance-assignment-virtual-machine/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # virtual_machine_id, maintenance_configuration_id, location; timeouts tail (no update)
βββ main.tf # keystone azurerm_maintenance_assignment_virtual_machine.this; dynamic timeouts
βββ outputs.tf # id first, then virtual_machine_id / maintenance_configuration_id / location
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT, Copyright (c) 2026 Casey Wood
βββ .gitignore # canonical library ignore set
provider "azurerm" {
features {}
# auth + subscription configured here (az login, OIDC, managed identity, or a service principal)
}
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}βΉοΈ The caller owns the provider:
features {}, authentication, and which subscription the provider points at are all root-module concerns. The module never declares them.
β οΈ locationmust be the VM's region, and every field is force-new β changing any of them replaces the assignment.
Consumes
| Input | Type | Typical source |
|---|---|---|
virtual_machine_id |
string |
terraform-azurerm-linux-virtual-machine / terraform-azurerm-windows-virtual-machine (id) |
maintenance_configuration_id |
string |
terraform-azurerm-maintenance-configuration (id) |
location |
string |
the VM module's location output |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Maintenance Assignment Resource ID (first) | references / audit |
virtual_machine_id |
The VM opted into the window | audit, terraform-azurerm-role-assignments scope |
maintenance_configuration_id |
The assigned configuration | audit |
location |
Region of the assignment record | composition wiring |
The examples below take machine and configuration IDs as inputs, because this module assigns an existing VM to an existing configuration and builds neither. Example 13 wires the real modules end to end.
variable "vm_ids" {
description = "Map of a stable key to the Resource ID of an existing virtual machine."
type = map(string)
}
variable "maintenance_configuration_ids" {
description = "Map of a stable key to the Resource ID of an existing maintenance configuration."
type = map(string)
}1 Β· Minimal call
All three fields are required, so the minimal call is the whole call.
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = "eastus"
}βΉοΈ What this actually does to the VM is decided by the configuration, not here.
2 Β· Sourcing location from the resource group rather than hard-coding it
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}π‘ Deriving
locationfrom the VM removes a whole class of apply-time failure, since the two must match.
3 Β· A Linux VM under an in-guest patch configuration
module "maintenance_config" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-configuration.git?ref=v1.0.0"
name = "mc-linux-monthly"
resource_group_name = module.rg.name
location = module.rg.location
scope = "InGuestPatch"
}
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = var.vm_ids["linux01"]
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}
β οΈ In-guest patching also needs the VM's own patch mode set to accept platform orchestration. Assigning the configuration is necessary but not sufficient.
4 Β· A Windows VM under the same configuration
module "windows_vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = var.vm_ids["windows01"]
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}π‘ One configuration can serve many machines across both operating systems β each machine is its own assignment.
5 Β· Several VMs with `for_each`
locals {
patched_vms = {
web01 = var.vm_ids["web01"]
web02 = var.vm_ids["web02"]
api01 = var.vm_ids["api01"]
}
}
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
for_each = local.patched_vms
virtual_machine_id = each.value
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}π‘ This is the explicit-membership pattern: the set of patched machines is a map in code, so adding or removing one is a visible diff.
6 Β· Different configurations for different tiers
locals {
vm_windows = {
web01 = var.vm_ids["web01"]
web02 = var.vm_ids["web02"]
}
vm_databases = {
db01 = var.vm_ids["db01"]
}
}
# Front end patches early in the window.
module "web_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
for_each = local.vm_windows
virtual_machine_id = each.value
maintenance_configuration_id = var.maintenance_configuration_ids["early"]
location = module.rg.location
}
# Databases patch in a later, longer window.
module "db_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
for_each = local.vm_databases
virtual_machine_id = each.value
maintenance_configuration_id = var.maintenance_configuration_ids["late"]
location = module.rg.location
}π‘ Staggering windows by tier is the usual reason to have several configurations, and the assignment is where a machine picks its tier.
7 Β· Two configurations on one VM
# A VM may carry more than one assignment β for example a host-scope
# configuration and an in-guest patch configuration.
module "vm_host_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = var.maintenance_configuration_ids["host"]
location = module.rg.location
}
module "vm_guest_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = var.maintenance_configuration_ids["guest"]
location = module.rg.location
}βΉοΈ One instance per configuration. The resource models a single binding, so two bindings are two instances.
8 Β· Explicit timeouts (no `update` β the schema has none)
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
timeouts = {
create = "30m"
read = "5m"
delete = "30m"
}
}
β οΈ Every field is force-new, so the provider exposes noupdatetimeout. Passing one is a plan error, which is why this module'stimeoutsobject omits it.
9 Β· Moving a VM to a different configuration is a replacement
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = var.maintenance_configuration_ids["new_window"] # was var.maintenance_configuration_ids["old_window"]
location = module.rg.location
}
β οΈ This destroys the old assignment and creates a new one. There is a brief moment where the VM belongs to neither window β schedule the change outside a maintenance window.
10 Β· Removing a VM from patching
variable "vm_under_maintenance_control" {
description = "Set false to take this VM out of the managed maintenance window."
type = bool
default = true
}
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
count = var.vm_under_maintenance_control ? 1 : 0
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}
β οΈ Removing the assignment does not make the VM unpatched β it returns the machine to whatever default platform behaviour applies. "Not in a managed window" is not the same as "never restarted".
11 Β· Explicit assignment versus a dynamic scope
# Option A β name each machine (this module). Membership is a reviewable list.
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
for_each = { web01 = var.vm_ids["web01"], web02 = var.vm_ids["web02"] }
virtual_machine_id = each.value
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}
# Option B β match by query (the sibling). New machines join automatically.
module "dynamic_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-dynamic-scope.git?ref=v1.0.0"
name = "prod-linux-monthly"
maintenance_configuration_id = module.maintenance_config.id
filter = {
resource_groups = [module.rg.name]
os_types = ["Linux"]
}
}π The trade-off is real: explicit assignments never surprise you but need a change per machine; a dynamic scope scales but means a new VM can join a rebooting window without a Terraform change. Pick deliberately.
12 Β· Auditing which machines are assigned
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
for_each = { web01 = var.vm_ids["web01"], web02 = var.vm_ids["web02"] }
virtual_machine_id = each.value
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
}
output "maintenance_membership" {
description = "Which machines are in the managed window, for the change record."
value = {
for k, m in module.vm_maintenance : k => {
assignment_id = m.id
vm_id = m.virtual_machine_id
configuration = m.maintenance_configuration_id
}
}
}π‘ Because membership is explicit, the output is the audit list β no query needed.
13 Β· ποΈ End-to-end composition
A resource group, a network, a Linux VM, a maintenance configuration with a real window, this assignment, and diagnostics.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-compute-prod"
location = "eastus"
}
module "vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-compute-prod"
resource_group_name = module.rg.name
location = module.rg.location
address_space = ["10.70.0.0/16"]
subnets = {
app = { address_prefixes = ["10.70.1.0/24"] }
}
}
module "nic" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface.git?ref=v1.0.0"
name = "nic-app-01"
resource_group_name = module.rg.name
location = module.rg.location
ip_configurations = [{
name = "internal"
subnet_id = module.vnet.subnet_ids["app"]
}]
}
module "vm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-linux-virtual-machine.git?ref=v1.0.0"
name = "vm-app-01"
resource_group_name = module.rg.name
location = module.rg.location
size = "Standard_D2s_v5"
admin_username = "azureadmin"
network_interface_ids = [module.nic.id]
}
module "maintenance_config" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-configuration.git?ref=v1.0.0"
name = "mc-prod-monthly"
resource_group_name = module.rg.name
location = module.rg.location
# The window, recurrence, and reboot behaviour all live here.
scope = "InGuestPatch"
window = {
start_date_time = "2026-08-02 02:00"
duration = "03:55"
time_zone = "Eastern Standard Time"
recur_every = "1Month Second Sunday"
}
}
# Membership: this VM joins that window.
module "vm_maintenance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-maintenance-assignment-virtual-machine.git?ref=v1.0.0"
virtual_machine_id = module.vm.id
maintenance_configuration_id = module.maintenance_config.id
location = module.rg.location
timeouts = { create = "30m", delete = "30m" }
}
module "law" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"
name = "law-compute-prod"
resource_group_name = module.rg.name
location = module.rg.location
}
output "maintenance_membership" {
value = {
assignment_id = module.vm_maintenance.id
vm_id = module.vm_maintenance.virtual_machine_id
configuration = module.vm_maintenance.maintenance_configuration_id
}
}π Note the division of ownership: the configuration decides when and whether it reboots; this assignment decides who. Reviewing a change to either is reviewing a different question, which is why they are separate modules.
Required
| Name | Type | Description |
|---|---|---|
virtual_machine_id |
string |
Resource ID of the VM to enrol. Force-new. |
maintenance_configuration_id |
string |
Resource ID of the Maintenance Configuration to assign. Force-new. |
location |
string |
Region of the assignment record; must match the VM's region. Force-new. |
Optional
| Name | Type | Default | Description |
|---|---|---|---|
timeouts |
object |
null |
create/read/delete durations β no update. |
Full object() schemas
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
delete = optional(string)
})
default = null
}All three inputs are plain required strings β there is no enum to constrain and no nested structure to type. The timeouts object omits update because the resource has no update operation.
| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the Maintenance Assignment | Passthrough |
virtual_machine_id |
The Resource ID of the Virtual Machine opted into the maintenance window | Passthrough |
maintenance_configuration_id |
The Resource ID of the assigned Maintenance Configuration, which owns the schedule and reboot behaviour | Passthrough |
location |
The Azure region of the assignment record, normalized to the provider's lower-case, space-free form | Passthrough |
assignment_name |
The name of the assignment record: the last segment of its Resource ID | Derived |
target_resource_type |
The ARM resource type this module assigns a maintenance configuration to | Derived |
subscription_id |
The subscription GUID the Virtual Machine, and therefore the assignment record, lives in | Derived |
virtual_machine_name |
The name of the Virtual Machine opted into the maintenance window | Derived |
virtual_machine_resource_group_name |
The resource group of the Virtual Machine | Derived |
maintenance_configuration_name |
The name of the assigned Maintenance Configuration | Derived |
maintenance_configuration_resource_group_name |
The resource group of the assigned Maintenance Configuration | Derived |
maintenance_configuration_subscription_id |
The subscription GUID of the assigned Maintenance Configuration | Derived |
is_cross_subscription_assignment |
True when the Maintenance Configuration lives in a different subscription from the Virtual Machine | Derived |
is_cross_resource_group_assignment |
True when the Maintenance Configuration lives in a different resource group from the Virtual Machine | Derived |
assignment_name_is_the_configuration_name |
Constant true | Constant |
same_named_configurations_collide_on_this_target |
Constant true | Constant |
overlapping_windows_trigger_only_one_configuration |
Constant true | Constant |
location_is_never_returned_by_the_api |
Constant true | Constant |
every_argument_forces_replacement |
Constant true | Constant |
configuration_id_casing_is_diff_suppressed |
Constant true | Constant |
virtual_machine_id_casing_forces_replacement |
Constant true | Constant |
create_is_retried_while_the_target_is_pending |
Constant true | Constant |
create_timeout_is_the_retry_budget |
Constant true | Constant |
destroy_silently_ends_maintenance_coverage |
Constant true | Constant |
rbac_derives_from_the_target_scope |
Constant true | Constant |
configuration_scope_is_not_verified_at_plan |
Constant true | Constant |
guest_scope_requires_automatic_by_platform_patch_mode |
Constant true | Constant |
an_existing_assignment_blocks_create |
Constant true | Constant |
There is no name output β this resource type has no name attribute. No output is sensitive.
- Fully immutable, so no update timeout. All three fields force replacement, which is why
timeoutscarries onlycreate,read, anddelete. Includingupdatewould failterraform validateagainst the real schema. - Replacement has a gap. Moving a VM between configurations destroys the old assignment before creating the new one, so there is a moment when the machine belongs to no managed window. Schedule that change outside a window.
- The ID nests under the VM.
β¦/virtualMachines/<vm>/providers/Microsoft.Maintenance/configurationAssignments/<name>β the assignment is the VM's child, which is whyconfigurationAssignments/writeis needed at the VM's scope rather than on the configuration. - Ownership is split on purpose. The configuration owns the schedule and the reboot behaviour; this module owns membership. Keeping them separate means "when do we patch?" and "what do we patch?" are different reviews.
- Assignment is necessary but not always sufficient. For in-guest patching the VM's own patch mode must also accept platform orchestration, and the configuration's
scopemust suit virtual machines. Neither is checkable at plan time, so both surface at apply or β worse β as a window that quietly does nothing. - Removing the assignment is not the same as opting out of all maintenance. It returns the machine to default platform behaviour rather than guaranteeing it is never restarted.
- No name, no tags. This ARM resource supports neither, so the universal tail here is
timeoutsonly β a deliberate omission, not an oversight.
| Concern | Secure default (empty call) | Opt-out (caller types it) |
|---|---|---|
| Membership | exactly one named VM per instance | add instances deliberately, or use the dynamic-scope sibling |
| Schedule and reboot behaviour | not decided here β owned by the configuration | set on terraform-azurerm-maintenance-configuration |
| Blast radius | one machine, stated explicitly | a dynamic scope trades that for automatic enrolment |
| Auditability | membership is a diff in code | not permitted to be implicit within this module |
| Mutability | none β every change is a replacement | not permitted |
| Secrets | none accepted, none emitted | not permitted |
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never a branch. - This is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. - The caller supplies
provider "azurerm" { features {} }, auth, and the target subscription. - Apply a change to
maintenance_configuration_idoutside an active maintenance window, since the replacement briefly leaves the VM unassigned.
The offline proof gate β no cloud, no backend:
terraform init -backend=falseresolves the pinnedazurerm ~> 4.0provider.terraform validateproves the configuration is type-correct against the provider schema, including thattimeoutshas noupdatemember.terraform fmt -checkenforces canonical formatting.
What only terraform plan / apply (run by a human, from CI) exercises: whether the configuration's scope suits virtual machines, whether location matches the VM's region, Microsoft.Maintenance provider registration, and RBAC at the VM's scope. And what no Terraform run proves: whether patches actually install, which depends on the VM's own patch mode and on the window being long enough.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-compute-prod/providers/Microsoft.Compute/virtualMachines/vm-app-01/providers/Microsoft.Maintenance/configurationAssignments/mc-prod-monthly"
location = "eastus"
maintenance_configuration_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-compute-prod/providers/Microsoft.Maintenance/maintenanceConfigurations/mc-prod-monthly"
virtual_machine_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-compute-prod/providers/Microsoft.Compute/virtualMachines/vm-app-01"| Symptom | Cause | Fix |
|---|---|---|
An update timeout you set is silently ignored, with no error |
This module's timeouts is a typed object with no update attribute, and Terraform's object-type conversion discards undeclared keys without raising anything. The Unsupported argument: update diagnostic exists, but only inside a resource block's own timeouts -- which a module caller never writes. |
Do not set update; there is no update operation to time out. Nothing will warn you, so check the key is absent rather than relying on a plan error. |
| Assignment replaces on a small edit | Every field is force-new | Expected; plan the recreate outside a maintenance window. |
| Apply fails on region | location is not the VM's region |
Wire location from the VM module's location output. |
| Apply fails: configuration scope mismatch | The configuration's scope does not suit virtual machines |
Use a VM-appropriate scope on the configuration, e.g. InGuestPatch. |
| Assignment exists but nothing is ever patched | The VM's own patch mode does not accept platform orchestration | Set the VM's patch mode (e.g. AutomaticByPlatform) on the VM module. |
| Window passes with no updates installed | Window too short, or no applicable updates | Lengthen the configuration's duration; confirm updates are actually pending. |
| Authorization error | Missing configurationAssignments/write at the VM scope |
Grant Contributor on the resource group, or a custom role with the assignment actions. |
| Removed the assignment and the VM still rebooted | Removal returns the VM to default platform behaviour | Managed windows control when; they are not a guarantee of never restarting. |
| Want a scale set instead | This resource only accepts a VM ID | Use terraform-azurerm-maintenance-assignment-virtual-machine-scale-set. |
- azurerm_maintenance_assignment_virtual_machine (provider registry)
- Microsoft Learn β Azure Update Manager maintenance configurations
- Sibling module:
terraform-azurerm-maintenance-configuration - Sibling module:
terraform-azurerm-maintenance-assignment-virtual-machine-scale-set - Sibling module:
terraform-azurerm-maintenance-assignment-dedicated-host - Sibling module:
terraform-azurerm-maintenance-assignment-dynamic-scope - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."