Creates the container that a virtual machine's restore points live in — scoped to one machine, holding nothing itself — targeting
hashicorp/azurerm ~> 4.0.
- 📦 Creates the restore point collection for exactly one virtual machine.
- 🔗 Emits the
idthe sibling restore-point module needs — it is the parent, created first. - 🏷️ Carries
tags, the only argument that updates in place, and the only Terraform-managed place to attach a cost label. - 🚩 Reports where the machine actually is, since the collection need not share its resource group.
- ⛔ Names the cascade no plan shows: destroying the collection destroys every restore point inside it, irreversibly.
💡 Why it matters: a collection is metadata — it stores nothing and costs nothing. Everything valuable is in the restore points inside it, and everything dangerous follows from that: four of its five arguments are force-new, a replacement destroys the container first, and a restore point captures a moment that cannot be re-taken.
If this module saved you time:
- ⭐ Star the repository — it genuinely helps other people find it.
- 💼 Connect on LinkedIn — linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee — buymeacoffee.com/microsoftexpert
flowchart TB
VM["terraform-azurerm-linux-virtual-machine or -windows-virtual-machine"]
RC["terraform-azurerm-virtual-machine-restore-point-collection"]
RP["terraform-azurerm-virtual-machine-restore-point"]
DISK["terraform-azurerm-managed-disk -- excluded when unsupported"]
RESTORE["restoring -- create disks, then a NEW machine. NOT a Terraform operation"]
VM -->|"id to source_virtual_machine_id -- ONE machine per collection"| RC
RC -->|"id to virtual_machine_restore_point_collection_id"| RP
DISK -.->|"id to excluded_disks -- the documented remedy, not an optimisation"| RP
RP -.->|"produces recovery MATERIAL only"| RESTORE
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002B4A,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class RC self
class VM keystone
class RP,DISK,RESTORE ext
The collection sits between the machine and its restore points, and the direction matters: it is created first, it names the machine, and the restore-point module references it. The dashed edge to restoring is dashed because restoring is not a Terraform operation at all.
flowchart TB
IN1["name -- the provider validates NOTHING"]
IN2["resource_group_name / location"]
IN3["source_virtual_machine_id -- ONE machine, force-new"]
IN4["tags -- the ONLY in-place update"]
RES["azurerm_virtual_machine_restore_point_collection.this"]
OUT1["id -- consumed as the restore point parent"]
OUT2["source_virtual_machine_name / spans_resource_groups"]
OUT3["posture flags -- 500 per machine, 10000 per region"]
OUT4["destroying this destroys every restore point inside"]
IN1 --> RES
IN2 --> RES
IN3 --> RES
IN4 --> RES
RES --> OUT1
RES --> OUT2
RES --> OUT3
RES --> OUT4
classDef self fill:#0078D4,stroke:#004578,color:#ffffff
classDef ext fill:#F3F2F1,stroke:#8A8886,color:#201F1E
class RES self
class IN1,IN2,IN3,IN4,OUT1,OUT2,OUT3,OUT4 ext
| Resource | Name | Cardinality |
|---|---|---|
azurerm_virtual_machine_restore_point_collection |
this |
single |
One resource, five arguments, and only tags updates in place.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, including authentication |
- 🔴 Destroying the collection destroys every restore point in it. Four of five arguments are force-new, and a replacement destroys before it creates. There is no soft delete.
- 🔴
namecarries NO validation whatsoever — not a length limit, not a character rule, not even a non-empty check. The sibling restore-point resource has the same non-rule. - 🟠 Only
tagsupdates in place. The provider retrieves the collection, replaces the tags and writes it back; it sends nothing else. - 🟠 The 500-restore-point limit is per MACHINE, not per collection. Adding collections does not raise it.
- 🟠 10,000 collections per subscription per region is the other documented ceiling.
- 🟠 Moving the source virtual machine breaks this. The collection's source reference is not updated by a resource group or subscription move, so new restore points fail against the old identity.
- 🟡 One collection protects exactly one machine — including each member of a Flexible-orchestration scale set, separately.
- 🟡 Uniform-orchestration scale sets are not supported at all, and the ID looks the same.
- 🟡
instantAccessis not exposed by the provider's resource, though Azure's collection has it. - 🟡 No lock is taken on the source machine — which matters for the child, where concurrent creates are unsupported.
- ✅ An import guard exists, and all four timeouts are honoured — the child module carries only three.
Least-privilege, scoped to the resource group holding the collection.
| Permission | Scope | Why |
|---|---|---|
Microsoft.Compute/restorePointCollections/read |
the resource group | Refresh, and the create's import check. |
Microsoft.Compute/restorePointCollections/write |
the resource group | Create and update. |
Microsoft.Compute/restorePointCollections/delete |
the resource group | Destroy — and this takes the restore points with it. |
Microsoft.Compute/virtualMachines/read |
the source machine | The collection references it, possibly across resource groups. |
Virtual Machine Contributor at both scopes covers all four.
🔒 Delete on this collection is delete on every recovery point in it. A role assignment that looks like it only permits managing a container permits destroying the recovery material inside, irreversibly — no soft delete, no recycle bin, and a restore point cannot be recreated because it captured a moment that has passed.
⚠️ The snapshots are the machine's data. This resource accepts and emits no credential, but the restore points it contains hold point-in-time copies of every captured managed disk. The access control that matters is on this collection's resource group — which may not be the machine's.
- The
Microsoft.Computeresource provider registered in the target subscription. - An existing virtual machine using managed disks. Unmanaged disks are unsupported.
- A resource group and region. The collection should be in the machine's region; nothing checks that.
- Not a Uniform-orchestration scale set — unsupported, and indistinguishable from a supported machine by Resource ID.
- Quota headroom: 10,000 collections per subscription per region, 500 restore points per machine.
terraform-azurerm-virtual-machine-restore-point-collection/
├── providers.tf # required_version >= 1.12.0, azurerm ~> 4.0, no provider block
├── variables.tf # 6 inputs, 4 validations -- the placement, the source machine, the tags
├── main.tf # one keystone `this`; a dynamic timeouts block
├── outputs.tf # 28 outputs -- id first, then the source machine, then the cascade
├── README.md # this file
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
provider "azurerm" {
features {}
}
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}Creating the collection captures nothing. The restore points inside it do that.
| Input | Type | Source |
|---|---|---|
name |
string |
the caller |
resource_group_name |
string |
terraform-azurerm-resource-group → name |
location |
string |
terraform-azurerm-resource-group → location |
source_virtual_machine_id |
string |
terraform-azurerm-linux-virtual-machine → id |
tags |
map(string) |
the caller |
timeouts |
object(...) |
the caller — four keys |
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the collection | the restore-point module's virtual_machine_restore_point_collection_id |
source_virtual_machine_name |
The protected machine, parsed | reporting |
spans_resource_groups |
Whether collection and machine are co-located | review |
| the posture flags | the cascade, the per-machine limits, the move hazard | change review |
1 · The smallest real call
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}ℹ️ Four required arguments. The collection is metadata — it stores nothing and costs nothing until restore points are created inside it.
2 · Wiring it to the restore-point module
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
module "nightly_restore_point" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point.git?ref=v1.0.0"
name = "rp-2026-08-13"
virtual_machine_restore_point_collection_id = module.restore_point_collection.id
}💡 The collection is the parent. It names the machine; the restore point does not, and takes no virtual machine argument at all.
⚠️ Referencing the collection'sidgives Terraform the ordering for free — nodepends_onneeded.
3 · The collection in a different resource group from the machine
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = var.backup_resource_group_name # the backup team's group
location = var.backup_resource_group_location
source_virtual_machine_id = module.app_vm.id # the application team's group
}
output "placement" {
value = {
collection_group = module.restore_point_collection.resource_group_name
machine_group = module.restore_point_collection.source_resource_group_name
crosses = module.restore_point_collection.spans_resource_groups
}
}💡 Legitimate and common — separating the recovery material from the workload is often the point.
🔒 It also separates who can read the snapshots from who can read the machine. A reviewer looking at the application's resource group sees no sign that point-in-time copies of its disks exist elsewhere.
spans_resource_groupsreports it.
4 · Tagging for cost, because the restore points cannot be tagged
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
tags = {
environment = "production"
cost-centre = "platform"
retention-days = "30"
}
}ℹ️
tagsis the only argument that updates in place — and the only Terraform-managed label available for attributing snapshot cost, because the restore points inside expose no tags of their own.
💡 The
retention-daystag above is a label, not a mechanism. Nothing in this module or the child enforces retention; that is the caller's automation.
5 · One collection per machine, across a fleet
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
for_each = var.protected_virtual_machine_ids # map of name => vm id
name = "rpc-${each.key}"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = each.value
tags = module.rg.tags
}ℹ️ A collection protects exactly one machine.
source_virtual_machine_idis singular and force-new, so a fleet means a collection each.
⚠️ That includes the individual members of a Flexible-orchestration scale set and of an availability set — Microsoft documents restore points as created per instance. A Uniform-orchestration scale set is not supported at all, and its members' IDs look no different here.
6 · Why the force-new arguments deserve care
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01-v2" # <-- renaming is a REPLACEMENT
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
⚠️ A replacement destroys the collection first — taking every restore point in it. Those are recovery points for moments that have passed; nothing can recreate them. Renaming a collection is not a cosmetic change.
💡
force_new_fieldslists all four, anddestroying_this_destroys_every_restore_point_inside_itis the output to read beside it.
7 · Reading the limits before automating
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
output "limits_to_design_around" {
value = {
per_machine = module.restore_point_collection.the_five_hundred_restore_point_limit_is_per_machine_not_per_collection
per_region = module.restore_point_collection.ten_thousand_collections_per_subscription_per_region
holds_nothing = module.restore_point_collection.the_collection_holds_nothing_and_the_restore_points_hold_everything
}
}
⚠️ The 500-restore-point ceiling is per machine, irrespective of how many collections they are spread across. A design that creates a collection per backup does not raise the ceiling — it only approaches the 10,000-collection one faster.
💡 Reuse one collection per machine and manage retention inside it. That is what the shape is for.
8 · The move that breaks everything, later
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
output "before_you_move_the_vm" {
value = module.restore_point_collection.moving_the_source_virtual_machine_breaks_this
}
⚠️ Do not move the source machine between resource groups or subscriptions while restore points exist. Microsoft states the collection's source reference is not updated by the move, so the Resource Manager IDs stop matching: new restore point creation fails against the old identity, and the existing points refer to a machine that is no longer at that path.
💡 Clean up the collection before the move, and create a new one afterwards. There is no repair.
9 · What NOT to do
# WRONG -- a scale set ID where a virtual machine belongs.
source_virtual_machine_id = var.app_scale_set_idℹ️ Refused at
terraform plan, offline and without credentials. The check is anchored to.../virtualMachines/<vm>. Restore points are created per instance — and a Uniform-orchestration scale set is unsupported outright.
# WRONG -- a disk, or the collection's own ID.
source_virtual_machine_id = var.data_disk_idℹ️ Also refused, for the same anchoring reason.
# ACCEPTED BY THE PROVIDER, refused here -- an empty name.
name = ""
⚠️ The provider applies no validation tonameat all — not a length limit, not a character rule, not even a non-empty check. This module adds only the non-empty check, because an empty name cannot form a Resource ID. Nothing further is invented, because there is no documented rule to mirror.
10 · Instant Access, and what this module cannot do
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
output "not_managed_here" {
value = module.restore_point_collection.instant_access_is_not_exposed_by_this_provider_resource
}ℹ️ Azure's restore point collection carries an
instantAccessproperty, which shortens the time to restore a disk. The provider's resource does not expose it, so a collection created here has whatever default Azure applies, and enabling or disabling it is an out-of-band change this module will neither make nor detect.
⚠️ That means an operator can change it in the portal and no plan will ever notice.
11 · Reading the posture outputs before applying
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
output "collection_posture" {
value = {
holds_nothing = module.restore_point_collection.the_collection_holds_nothing_and_the_restore_points_hold_everything
destroy_cascades = module.restore_point_collection.destroying_this_destroys_every_restore_point_inside_it
one_machine_only = module.restore_point_collection.one_collection_protects_exactly_one_virtual_machine
uniform_unsupported = module.restore_point_collection.uniform_orchestration_scale_sets_are_not_supported
no_lock = module.restore_point_collection.the_provider_takes_no_lock_on_the_source_virtual_machine
tags_only_update = module.restore_point_collection.only_tags_change_in_place
force_new = module.restore_point_collection.force_new_fields
}
}🔒 Every one of these except
force_newis a constanttrue. They are outputs rather than prose because each is consequential, invisible in state, and inferable from nothing else in the plan — and because an output can be surfaced in a review pipeline, where a README cannot.
12 · Serialising restore points, which nothing else will
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
}
module "restore_point_first" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point.git?ref=v1.0.0"
name = "rp-full"
virtual_machine_restore_point_collection_id = module.restore_point_collection.id
}
# Chained deliberately -- Azure does not support concurrent creation for one
# machine, and neither resource takes a lock.
module "restore_point_second" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point.git?ref=v1.0.0"
name = "rp-crash"
virtual_machine_restore_point_collection_id = module.restore_point_collection.id
crash_consistency_mode_enabled = true
depends_on = [module.restore_point_first]
}
⚠️ depends_onhere is load-bearing, not decorative. Microsoft states that concurrent creation of restore points for the same machine is unsupported, andthe_provider_takes_no_lock_on_the_source_virtual_machineistrue— Terraform would otherwise start both at once.
⚠️ Azure also throttles creation to 3 per hour per machine, and the two consistency modes have different minimum intervals. Two in one apply is fine; a fleet-wide loop is not.
13 · 🏗️ End-to-end composition
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-app-eastus"
location = "eastus"
tags = {
environment = "production"
workload = "line-of-business-app"
}
}
module "app_vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-app-eastus"
resource_group_name = module.rg.name
location = module.rg.location
address_space = ["10.60.0.0/16"]
subnets = {
snet-app = {
address_prefixes = ["10.60.1.0/24"]
}
}
tags = module.rg.tags
}
module "app_nic" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-interface.git?ref=v1.0.0"
name = "nic-app01"
resource_group_name = module.rg.name
location = module.rg.location
ip_configurations = {
internal = {
subnet_id = module.app_vnet.subnet_ids["snet-app"]
private_ip_address_allocation = "Dynamic"
}
}
tags = module.rg.tags
}
module "app_vm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-linux-virtual-machine.git?ref=v1.0.0"
name = "vm-app01"
resource_group_name = module.rg.name
location = module.rg.location
size = "Standard_D2s_v5"
admin_username = "azureadmin"
network_interface_ids = [module.app_nic.id]
tags = module.rg.tags
}
# The container. Metadata only -- it stores nothing.
module "restore_point_collection" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point-collection.git?ref=v1.0.0"
name = "rpc-app01"
resource_group_name = module.rg.name
location = module.rg.location
source_virtual_machine_id = module.app_vm.id
tags = merge(module.rg.tags, {
contains = "point-in-time-disk-snapshots"
})
}
# The capture. This is what stores data and accrues cost.
module "app_restore_point" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-machine-restore-point.git?ref=v1.0.0"
name = "rp-2026-08-13T0400Z"
virtual_machine_restore_point_collection_id = module.restore_point_collection.id
}
output "protection" {
value = {
machine = module.app_vm.id
collection = module.restore_point_collection.id
restore_point = module.app_restore_point.id
consistency = module.app_restore_point.requested_consistency_mode
crosses_groups = module.restore_point_collection.spans_resource_groups
destroy_cascades = module.restore_point_collection.destroying_this_destroys_every_restore_point_inside_it
}
}💡 Terraform derives the ordering from the Resource ID references — the restore point waits for the collection, which waits for the machine.
⚠️ consistencyreports(omitted -- Azure defaults to ApplicationConsistent)here, which is the stronger of the two modes and the right default. It is a choice, not an absence.
🔒 The
containstag is deliberate. The collection's resource group now holds point-in-time copies of the machine's disks, and a tag is the cheapest way to make that visible to whoever reviews the group later.
Required: name, resource_group_name, location, source_virtual_machine_id
Optional: tags, timeouts
This module carries the universal tags tail; the sibling restore-point module does not, because the
provider exposes tags here and not there.
Full input schemas
variable "name" {
type = string
# REQUIRED, FORCE-NEW. The provider applies NO validation to this field at
# all. This module adds only a non-empty check.
}
variable "resource_group_name" {
type = string
# REQUIRED, FORCE-NEW. Need not match the source machine's.
}
variable "location" {
type = string
# REQUIRED, FORCE-NEW. Deliberately NOT validated against a region list.
}
variable "source_virtual_machine_id" {
type = string
# REQUIRED, FORCE-NEW. Anchored to .../virtualMachines/<vm>, so a scale set
# is refused. ONE machine per collection.
}
variable "tags" {
type = map(string)
default = {}
# The ONLY argument that updates in place.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
# All four honoured. The CHILD module carries only three.
}| Output | Type | Notes |
|---|---|---|
id |
string |
.../restorePointCollections/<name> — the restore-point module's parent |
name |
string |
As configured; force-new |
resource_group_name / location |
string |
The collection's own placement |
source_virtual_machine_id / source_virtual_machine_name |
string |
The protected machine |
source_resource_group_name / source_subscription_id |
string |
Where that machine is |
tags |
map(string) |
The only in-place-updatable argument |
spans_resource_groups |
bool |
Whether collection and machine are co-located |
tag_count |
number |
How many tags are applied |
the_collection_holds_nothing_and_the_restore_points_hold_everything |
bool |
Constant true |
destroying_this_destroys_every_restore_point_inside_it |
bool |
Constant true |
the_five_hundred_restore_point_limit_is_per_machine_not_per_collection |
bool |
Constant true |
ten_thousand_collections_per_subscription_per_region |
bool |
Constant true |
moving_the_source_virtual_machine_breaks_this |
bool |
Constant true |
one_collection_protects_exactly_one_virtual_machine |
bool |
Constant true |
uniform_orchestration_scale_sets_are_not_supported |
bool |
Constant true |
instant_access_is_not_exposed_by_this_provider_resource |
bool |
Constant true |
only_tags_change_in_place |
bool |
Constant true |
the_provider_takes_no_lock_on_the_source_virtual_machine |
bool |
Constant true |
an_import_guard_exists_on_this_resource |
bool |
Constant true |
a_deleted_collection_disappears_from_state_without_an_error |
bool |
Constant true |
all_four_timeouts_are_honoured_here |
bool |
Constant true |
force_new_fields |
list(string) |
Everything except tags |
fields_that_can_change_after_creation |
list(string) |
tags only |
fields_azure_returns_on_read |
list(string) |
Where drift is detectable |
no_secret_is_accepted_or_emitted_by_this_module |
bool |
Constant true — read the caveat |
The collection holds nothing. It names a virtual machine and groups restore points; the point-in-time managed-disk snapshots, and the storage charges, belong to the restore points inside it. An empty collection stores nothing and costs nothing. That is worth understanding first, because everything else here follows from it — including why the destroy is so much more dangerous than the create.
Destroying the collection destroys the recovery points. Four of the five arguments are force-new, and a replacement destroys before it creates. A restore point captured a moment that has passed and cannot be recreated; there is no soft delete and no recycle bin. Renaming a collection, moving it between resource groups, or changing its region are all, in effect, "delete every recovery point for this machine".
The limits are counted where you would not expect. The 500-restore-point ceiling is per virtual machine, irrespective of how many collections the points are spread across — so a design that creates a collection per backup does not raise it, and approaches the 10,000-collections-per-subscription-per-region ceiling faster. Reuse one collection per machine.
Moving the machine breaks this, silently and later. Microsoft states that a machine with restore points should not be moved between resource groups or subscriptions: the collection's source reference is not updated, so the Resource Manager IDs stop matching. New restore point creation then fails against the old identity, and the existing points refer to a machine that is not at that path. Clean up the collection before the move.
Nothing here is validated by the provider. name carries no rule of any kind — not length, not
characters, not even non-empty. This module adds the non-empty check alone, because an empty name cannot form
a Resource ID, and adds nothing more because there is no documented rule to mirror. The sibling restore-point
resource has exactly the same non-rule, which is the only thing the two names agree on.
Only tags change in place. The provider retrieves the collection, replaces the tags and writes it back — nothing else. That makes tags the only Terraform-managed label available for attributing snapshot cost, since the restore points expose none of their own.
No lock, and it matters downstream. Neither this resource nor the child takes an internal lock, while Microsoft states that concurrent creation of restore points for one machine is unsupported. A configuration that creates several restore points must serialise them itself — see example 12.
Instant Access is invisible here. Azure's collection carries an instantAccess property; the provider's
resource does not expose it. A collection created here takes whatever default Azure applies, and someone
toggling it in the portal will never show up in a plan.
lifecycle is not valid inside a module block. A caller cannot add prevent_destroy here — which
matters more than usual given the cascade. Where accidental destruction is a real risk, apply a
CanNotDelete management lock to the resource group, noting that a lock prevents deletion but not
replacement, and four of five arguments force replacement.
| Concern | This module's position | What the caller must type to change it |
|---|---|---|
| Parent/child direction | The collection is the parent — created first, referenced by the restore point | — |
| The destroy cascade | Emitted as its own output, separate from force_new_fields, because readers act on the consequence not the mechanism |
— |
name |
Non-empty only — the provider validates nothing, and inventing a rule would reject legal names | — |
source_virtual_machine_id |
Anchored to virtualMachines, so a scale set is refused |
— |
location |
Not validated against a region list — Azure's region set changes | — |
| Service limits | Emitted as constant outputs, never enforced — none is derivable from a single resource | — |
spans_subscriptions |
Deliberately absent — the collection's own subscription comes from the provider and cannot be read | — |
tags |
Present, because the provider exposes it here — and it is the only cost label available | — |
🔒 There is no security toggle here. The posture is who can delete this collection and who can read the resource group its snapshots land in.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module by tag — ?ref=v1.0.0 — never a branch.
Plan-only: this repository performs no cloud apply. A human applies from CI — and for this resource, reads the destroy plan carefully, because a replacement is not recoverable.
What terraform validate covers, offline and without credentials:
name,resource_group_nameandlocationare non-empty and carry no leading or trailing whitespace — each is sent to Azure exactly as written, and the provider validatesnamenot at all;nameis at most 80 characters and is not a pasted Resource ID (two separate checks, so each failure gets its own message);resource_group_nameis at most 90 characters and does not end in a period;source_virtual_machine_idis avirtualMachinesResource ID with no surrounding whitespace;tagsholds at most 50 entries, with keys up to 512 characters and values up to 256.- The anchored
source_virtual_machine_idcheck — a scale set, a disk and a collection ID are all refused.
What only terraform plan exercises (credentials required):
- That the resource group and the source machine exist.
- The import guard, if a collection of this name already exists in the group.
What only terraform apply reveals:
- Whether the machine uses managed disks, and whether it is a Uniform-orchestration scale set member.
- Whether the subscription is at its collection quota.
What nothing reveals at all:
- Whether
instantAccesshas been toggled out of band. - Whether someone is about to move the source machine.
ℹ️
terraform consolefires root-module variable validations, unliketerraform validateon a calling configuration. It is the honest offline harness for the first list and for both derived flags.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app-eastus/providers/Microsoft.Compute/restorePointCollections/rpc-app01"
name = "rpc-app01"
resource_group_name = "rg-app-eastus"
location = "eastus"
source_virtual_machine_name = "vm-app01"
source_resource_group_name = "rg-app-eastus"
spans_resource_groups = false
tag_count = 3
force_new_fields = [
"name",
"resource_group_name",
"location",
"source_virtual_machine_id",
]
fields_that_can_change_after_creation = [
"tags",
]
destroying_this_destroys_every_restore_point_inside_it = true
ℹ️
fields_that_can_change_after_creationholding onlytagsis the whole risk profile of this resource in one line: everything else is a replacement, and a replacement is a cascade.
| Symptom | Cause | Fix |
|---|---|---|
name must not be empty or whitespace |
A blank name | The provider validates this field not at all; this module adds only the non-empty check |
source_virtual_machine_id must be a virtual machine Resource ID |
A scale set, a disk, or a collection ID | The check is anchored to virtualMachines/<vm>. Restore points are per instance |
| Apply fails with an already-exists error | A collection of this name is already in the resource group | The create carries an import guard. terraform import it, or choose another name |
| Apply fails on the source machine | It uses unmanaged disks, or it is a Uniform-orchestration scale set member | Neither is supported, and neither is visible from a Resource ID |
| Apply fails at the subscription level | 10,000 collections per subscription per region reached | Reuse one collection per machine rather than one per backup |
| Restore point creation fails though the collection is fine | The machine already has 500 restore points, across all collections | The ceiling is per machine. Delete older points |
| New restore points fail after a resource-group move | The collection's source reference still points at the old ID | Not repairable. Delete the collection and create a new one |
| A rename destroyed the recovery points | name is force-new, and a replacement destroys first |
Expected and irreversible. Read force_new_fields before editing |
| The collection reappears as a create on every plan | It was removed outside Terraform; the read marks it gone silently | Applying recreates an empty collection — the restore points are gone |
| Instant Access changed and no plan noticed | The provider does not expose instantAccess |
Out-of-band by design. Manage it outside Terraform or not at all |
| Two restore points failed when applied together | Concurrent creation for one machine is unsupported, and nothing locks | Chain them with depends_on — see example 12 |
azurerm_virtual_machine_restore_point_collection— provider reference- Overview of Virtual Machine restore points — consistency modes, incremental behaviour and the limitations
- Support matrix for VM restore points — the 500-per-machine and 10,000-per-region ceilings, disk type support and throttling
- Sibling modules:
terraform-azurerm-virtual-machine-restore-point,terraform-azurerm-linux-virtual-machine,terraform-azurerm-windows-virtual-machine,terraform-azurerm-managed-disk,terraform-azurerm-resource-group,terraform-azurerm-recovery-services-vault - This module's
SCOPE.md— the cross-module contract
💙 "Infrastructure as Code should be standardized, consistent, and secure."

