Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure Maintenance Assignment (Virtual Machine) Terraform Module

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.

Terraform azurerm Module Version Type Resources


🧩 Overview

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.

❀️ Support this project

If this module saves you time, consider supporting the work:


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

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

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.

🧬 What this module builds

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

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).

βœ… Provider / Versions

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, and location all 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 update timeout. This module's timeouts object carries create, read, and delete only. Passing update is 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 name input β€” the service derives the assignment name. There is no tags support 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's scope and visibility. A configuration built with an InGuestPatch scope behaves very differently from a Host one, 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.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • 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/read on the configuration being assigned.
  • Microsoft.Compute/virtualMachines/read on 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.

Azure Prerequisites

  • 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.Maintenance resource 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.

πŸ“ Module Structure

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

βš™οΈ Quick Start

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.

⚠️ location must be the VM's region, and every field is force-new β€” changing any of them replaces the assignment.

πŸ”Œ Cross-Module Contract

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

πŸ“š Example Library

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 location from 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 no update timeout. Passing one is a plan error, which is why this module's timeouts object 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.

πŸ“₯ Inputs

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.

🧾 Outputs

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.

🧠 Architecture Notes

  • Fully immutable, so no update timeout. All three fields force replacement, which is why timeouts carries only create, read, and delete. Including update would fail terraform validate against 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 why configurationAssignments/write is 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 scope must 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 timeouts only β€” a deliberate omission, not an oversight.

🧱 Design Principles

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

πŸš€ Runbook

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 / apply from CI against real credentials.
  • The caller supplies provider "azurerm" { features {} }, auth, and the target subscription.
  • Apply a change to maintenance_configuration_id outside an active maintenance window, since the replacement briefly leaves the VM unassigned.

πŸ§ͺ Testing

The offline proof gate β€” no cloud, no backend:

  • terraform init -backend=false resolves the pinned azurerm ~> 4.0 provider.
  • terraform validate proves the configuration is type-correct against the provider schema, including that timeouts has no update member.
  • terraform fmt -check enforces 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.

πŸ’¬ Example Output

$ 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"

πŸ” Troubleshooting

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.

πŸ”— Related Docs


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