Defines how often on-premises Hyper-V machines replicate to Azure and how long their recovery points are kept, targeting
hashicorp/azurerm ~> 4.0.
- 🕒 Manages
azurerm_site_recovery_hyperv_replication_policy— the retention window, the application-consistent snapshot frequency and the replication interval for Hyper-V-to-Azure replication. - 🔴 Three Terraform resource types write this one Azure record type. The Azure-to-Azure and VMware policies use the same client and the same Resource ID constructor, so a shared name in a shared vault is one object.
- ✏️ This resource can be edited. It declares an Update function, so its three numeric arguments change in place and
timeoutscarries four keys. - 🚫 The cross-field rule its Azure-to-Azure sibling enforces is absent here — no documentation note, no plan-time check and no comparison in either the create or the update body. This module reports the condition instead of inventing the constraint.
- ⏱️
replication_interval_in_secondsis a two-value set: 30 or 300. Nothing else can be expressed, and a value set outside Terraform is reverted on the next apply. - 🔗 Takes the vault by ID where its Azure-to-Azure counterpart takes it by name plus a resource group — and still emits a path output byte-identical to that module's.
💡 Why it matters: the three policy resources in this service look unrelated by name and are interchangeable at the API. The retention argument here is in hours with a ceiling of 24; on the sibling it is in minutes with a ceiling of 525600. A value copied between the two configurations is wrong by a factor of sixty in one direction and by two orders of magnitude in the other, and neither the plan nor the apply says so.
If this module saved you time:
- ⭐ Star the repository — it helps others find it.
- 💼 Connect on LinkedIn — linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee — buymeacoffee.com/microsoftexpert
This module is terraform-azurerm-site-recovery-hyperv-replication-policy, one of the fourteen blue nodes — all fourteen authored site_recovery_* modules share this diagram. The three dark nodes are not modules: each is a single ARM resource type that several of the blue modules write -- two of the three by three resource types each, the remaining one by two. Any two modules writing one of them can silently collide.
flowchart TB
RG["terraform-azurerm-resource-group"]
VAULT["terraform-azurerm-recovery-services-vault"]
FABRIC["terraform-azurerm-site-recovery-fabric"]
HVS["terraform-azurerm-site-recovery-services-vault-hyperv-site"]
PC["terraform-azurerm-site-recovery-protection-container"]
POLICY["terraform-azurerm-site-recovery-replication-policy"]
HVPOL["terraform-azurerm-site-recovery-hyperv-replication-policy"]
VMPOL["terraform-azurerm-site-recovery-vmware-replication-policy"]
PCM["terraform-azurerm-site-recovery-protection-container-mapping"]
HVASSOC["terraform-azurerm-site-recovery-hyperv-replication-policy-association"]
VMASSOC["terraform-azurerm-site-recovery-vmware-replication-policy-association"]
NM["terraform-azurerm-site-recovery-network-mapping"]
HVNM["terraform-azurerm-site-recovery-hyperv-network-mapping"]
RVM["terraform-azurerm-site-recovery-replicated-vm"]
RRP["terraform-azurerm-site-recovery-replication-recovery-plan"]
ARMF["ONE ARM type, TWO resource types: vaults replicationFabrics. Same name plus same vault means one object, silently"]
ARMP["ONE ARM type, THREE resource types: vaults replicationPolicies. A2A, Hyper-V and VMware policies are interchangeable at the API"]
ARMM["ONE ARM type, THREE resource types: replicationProtectionContainerMappings. The hardest group to assert on"]
VM["terraform-azurerm-linux-virtual-machine or windows-virtual-machine: wire its id, NOT its virtual_machine_id"]
VNET["terraform-azurerm-virtual-network: the failover network, the test network and the source network"]
RUNBOOK["an Azure Automation runbook: a plan action names it by id and it runs with the automation account's own permissions. NOT bounded by vault RBAC"]
VMWVM["terraform-azurerm-site-recovery-vmware-replicated-vm"]
HOSTS["the Hyper-V hosts themselves, registered with a five-day vault key. NO azurerm resource does this"]
APPL["the VMware replication appliance, deployed on-premises and registered against the vault. NO azurerm resource does this either"]
VMM["the System Center VMM server, registered by installing the Site Recovery Provider on it. NO azurerm resource does this either"]
VCENTER["the vCenter machines and the credentials stored on the appliance. source_vm_name and physical_server_credential_name are FRIENDLY names matched at apply time, and that credential is root or admin ON the source machine"]
RG -->|"resource_group_name and location"| VAULT
VAULT -->|"name, NOT id, so there is no dependency edge from a literal"| FABRIC
VAULT -->|"id, NOT name: this sibling takes the vault ID instead"| HVS
VAULT -->|"name, and NO fabric: a policy belongs to the vault"| POLICY
VAULT -->|"id, a second convention on the same parent"| HVPOL
VAULT -->|"id, and the association takes the SAME vault argument"| VMPOL
VAULT -->|"id: BOTH the fabric and the container are discovered under it at apply time"| VMASSOC
VAULT -->|"name plus resource group, a third convention again"| RVM
VAULT -->|"id: the ONLY Resource ID this module resolves anything from"| HVNM
VAULT -->|"id, and BOTH fabrics as ids: a fourth convention"| RRP
FABRIC -->|"hardcoded instance type: an Azure fabric"| ARMF
HVS -->|"hardcoded instance type: HyperVSite, absent from Swagger"| ARMF
POLICY -->|"hardcoded A2A payload"| ARMP
HVPOL -->|"hardcoded Hyper-V to Azure payload"| ARMP
VMPOL -->|"hardcoded InMageRcm payload: the MODERNIZED VMware provider"| ARMP
PCM -->|"container taken by name and by id"| ARMM
HVASSOC -->|"container discovered under the fabric at apply time"| ARMM
VMASSOC -->|"fabric AND container both discovered under the vault"| ARMM
FABRIC -->|"name, so a Hyper-V site's name is equally legal here"| PC
FABRIC -->|"name, for BOTH sides: ARM names, not friendly names"| NM
FABRIC -->|"id, for BOTH sides, and neither is compared to the vault"| RRP
PC -->|"name for the source side, id for the target side"| PCM
POLICY -->|"id, where the container beside it is taken by name"| PCM
HVPOL -->|"id, the only consumer of a Hyper-V policy"| HVASSOC
HVS -->|"id, and the protection container is discovered from it"| HVASSOC
VMPOL -->|"id, the only consumer of a VMware policy"| VMASSOC
VNET -->|"id, twice. Only the SOURCE id is parsed by the provider"| NM
VNET -->|"id, the target only. The SOURCE network is a VMM friendly name"| HVNM
FABRIC -->|"source fabric by NAME, target fabric by ID: one resource, two conventions"| RVM
PC -->|"source container by NAME, target container by ID"| RVM
POLICY -->|"id, and a Hyper-V or VMware policy id would also be accepted"| RVM
VM -->|"the machine being protected"| RVM
VNET -->|"the failover network and the test network, both Optional AND Computed"| RVM
PCM -->|"makes a container pair eligible before any item can replicate"| RVM
NM -->|"decides where an Azure-to-Azure failover lands"| RVM
HVNM -->|"decides where a VMM Hyper-V failover lands, for every machine on the source network"| VMM
RVM -->|"ids, into ORDERED boot groups: group 2 starts only after group 1 has finished"| RRP
VMWVM -->|"ids, into the same ORDERED boot groups as the Azure-to-Azure items"| RRP
VAULT -->|"id ALONE, a fifth convention: the fabric AND the container are discovered from it, and there must be exactly ONE container"| VMWVM
VMPOL -->|"id, its second consumer: the policy this VMware item replicates under"| VMWVM
VNET -->|"the failover network and the test network. test_network_id is read back and NEVER updated"| VMWVM
VMASSOC -->|"makes the discovered container eligible before any VMware machine can replicate"| VMWVM
RUNBOOK -->|"id, from a plan pre-action or post-action"| RRP
APPL -->|"appliance_name is its FRIENDLY name, matched against the fabric process servers. Microsoft: it cannot be changed once set"| VMWVM
VCENTER -->|"source_vm_name and physical_server_credential_name, both matched by name against records the appliance discovered"| VMWVM
HOSTS -->|"register into the site out of band, invisible to Terraform"| HVS
HOSTS -->|"registration is what creates the container this record needs"| HVASSOC
APPL -->|"required before any VMware machine can be enrolled"| VMASSOC
VMM -->|"the fabric AND the source network are found under it by FRIENDLY NAME at apply time"| HVNM
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002438,color:#ffffff
classDef sibling fill:#F3F6F9,stroke:#8A9BA8,color:#1B1F23
class FABRIC,HVS,PC,POLICY,HVPOL,VMPOL,PCM,HVASSOC,VMASSOC,NM,HVNM,RVM,RRP,VMWVM me
class ARMF,ARMP,ARMM keystone
class RG,VAULT,VM,VNET,RUNBOOK,VCENTER,HOSTS,APPL,VMM sibling
The Hyper-V branch reads bottom-up: hosts are registered out of band, a Hyper-V site represents them, this policy states the replication settings, and the association binds the two. Nothing replicates until all four exist.
flowchart TB
IN_NAME["name: force-new, and shared with TWO other policy resource types in one Azure namespace"]
IN_VAULT["recovery_vault_id: a vault Resource ID, split into four values. Anchored, because a Hyper-V site's ID extends it"]
IN_RET["recovery_point_retention_in_hours: zero to 24. Zero is legal and means no history"]
IN_FREQ["application_consistent_snapshot_frequency_in_hours: zero to 12. Zero disables app-consistent points"]
IN_INT["replication_interval_in_seconds: 30 or 300, and nothing else is expressible"]
THIS["azurerm_site_recovery_hyperv_replication_policy.this"]
NORULE["NO cross-field rule here. Both sibling policy resources document and enforce one; this resource documents nothing and checks nothing"]
OUT_PATH["vault and policy paths, comparable with all three policy modules and both associations"]
OUT_POSTURE["posture: no history, crash-consistent only, frequency above the window, or its exact boundary"]
OUT_CONST["seven constants: one shared record type, a silent read, an unenforced pairing, an in-place retention cut"]
IN_NAME --> THIS
IN_VAULT --> THIS
IN_RET --> THIS
IN_FREQ --> THIS
IN_INT --> THIS
IN_FREQ -->|"reported, not rejected: Microsoft says should, not must"| NORULE
IN_RET -->|"carries no rules of its own"| NORULE
IN_NAME --> OUT_PATH
IN_VAULT --> OUT_PATH
IN_RET --> OUT_POSTURE
IN_FREQ --> OUT_POSTURE
IN_INT --> OUT_POSTURE
THIS --> OUT_CONST
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef keystone fill:#004578,stroke:#002438,color:#ffffff
classDef sibling fill:#F3F6F9,stroke:#8A9BA8,color:#1B1F23
class THIS keystone
class NORULE me
class IN_NAME,IN_VAULT,IN_RET,IN_FREQ,IN_INT,OUT_PATH,OUT_POSTURE,OUT_CONST sibling
| Resource | Cardinality | Note |
|---|---|---|
azurerm_site_recovery_hyperv_replication_policy |
single (this) |
The keystone. No child or relationship records. |
A standalone module: one keystone resource, no for_each children. Five arguments plus timeouts, three of which are editable in place.
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 (validated against 4.81.0) |
| Provider block | None here. The caller configures provider "azurerm" { features {} }, auth and subscription. |
| ARM API version | Microsoft.RecoveryServices 2024-04-01 |
tags |
Not supported — this resource has no tags and no location. Tag the vault instead. |
Schema notes that bite
- 🔴
azurerm_site_recovery_replication_policyandazurerm_site_recovery_vmware_replication_policywrite the same Azure record type. Same client, same ID constructor; the three differ only in the provider-specific payload. A name collision in one vault is silent in every direction. - 🔴 The read sets the three numeric attributes only inside a type assertion, with no
else. A record of either other replication type read through this resource leaves all three at zero, and Terraform proposes to change them on every plan with nothing explaining why. ⚠️ The documented cross-field rule does not exist on this resource. The sibling documents "application_consistent_snapshot_frequency_in_minutes must be less than or equal to recovery_point_retention_in_minutes" and enforces it. Here the documentation page carries zero~>/!>/->note blocks, there is noCustomizeDiff, and neither body compares the two.⚠️ Force-new:nameandrecovery_vault_idonly. The three numeric arguments carry noForceNewand are updated in place.⚠️ The retention ceiling is 24 and the unit is hours.IntBetween(0, 24), against the sibling'sIntBetween(0, 525600)in minutes.⚠️ replication_interval_in_secondsisIntInSlice([]int{30, 300}). Two values, both documented.⚠️ nameis validated only byStringIsNotEmpty, which accepts a whitespace-only string.recovery_vault_idgets a real type-specific validator,vaults.ValidateVaultID.⚠️ The existence check references the API's HTTP 400 — but not for a missing record (Azure/azure-rest-api-specsissue 12759). It guards with the service's ownwasBadRequestWithNotExisthelper, which returns true only when the response is a 400 and the first service-error detail code is exactlySubscriptionIdNotRegisteredWithSrs— a subscription not registered with Site Recovery, not a missing record. The helper also requires a legacyautorest.DetailedError, and every caller in this service uses ago-azure-sdkclient with nogo-autorestimport, so on the evidence in this service the branch is unreachable. Treat the tolerance as nominal and expect a first apply to fail while looking for the policy.⚠️ There is noSchemaVersionand no state upgrader.
| Principal | Scope | Requirement |
|---|---|---|
| The principal running Terraform | The vault or its resource group | Site Recovery Contributor — it carries Microsoft.RecoveryServices/vaults/replicationPolicies/* |
| Alternatively | The same scope | Contributor or Owner |
Whoever can write this policy can shorten a recovery window in place. Narrowing recovery_point_retention_in_hours is an update, not a replacement: no resource is destroyed, nothing in the plan flags data loss, and recovery points outside the new window are not recoverable afterwards. That belongs here rather than only in Architecture Notes, because this table is what a reviewer reads before granting write access.
Site Recovery Contributor cannot create or delete the vault itself, per Microsoft's description of the role. Site Recovery Operator cannot register new infrastructure and Site Recovery Reader is read-only.
Backup Contributor is the wrong role and it is an easy mistake, because a Recovery Services vault serves both Azure Backup and Site Recovery. It grants nothing over replicationPolicies.
Plan access is not credential access. This module reads and holds no secret, and no output is sensitive.
Microsoft.RecoveryServicesregistered in the subscription.- An existing Recovery Services vault, referenced here by Resource ID.
- A name unique within that vault across ALL THREE policy resource types — nothing enforces it across configurations.
- An understanding that this policy protects nothing on its own. A Hyper-V topology also needs a Hyper-V site, an association binding the two, and hosts registered out of band.
terraform-azurerm-site-recovery-hyperv-replication-policy/
├── providers.tf # required_version and the pinned azurerm; no provider block
├── variables.tf # 6 variables, 9 validations
├── main.tf # the keystone `this`, plus the vault parse and the reported posture
├── outputs.tf # 27 outputs: 6 passthrough, 14 derived, 7 constant
├── README.md # this file
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT
└── .gitignore
module "dr_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-dr-prod"
location = "eastus"
}
module "dr_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-dr-prod"
resource_group_name = module.dr_rg.name
location = module.dr_rg.location
sku = "Standard"
}
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}ℹ️ The caller configures the provider, its authentication and its
features {}block. This module declares none of those.
Consumes
| Input | Type | Source module |
|---|---|---|
recovery_vault_id |
string |
terraform-azurerm-recovery-services-vault output id |
name |
string |
caller |
recovery_point_retention_in_hours |
number |
caller |
application_consistent_snapshot_frequency_in_hours |
number |
caller |
replication_interval_in_seconds |
number |
caller |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The policy's Resource ID (first) | terraform-azurerm-site-recovery-hyperv-replication-policy-association as policy_id |
policy_path_within_the_subscription |
The plan-time path | check blocks across all three policy modules |
vault_path_within_the_subscription |
The parent vault's path | check blocks asserting one topology, one vault |
vault_name |
The vault's name, parsed out of the ID | the siblings in this family that take a vault by name |
recovery_point_retention_in_minutes |
The window converted | comparison against the Azure-to-Azure policy module |
| seven constants | The facts that produce no error when they bite | documentation, check blocks |
1 · The smallest real call
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}ℹ️ Every argument is required. There is no empty call to make safe, and no argument here is an exposure control — this policy governs retention and frequency, not access.
2 · Microsoft's own tutorial values
Microsoft's Hyper-V-to-Azure tutorial selects a five-minute copy frequency, two hours of retention and an hourly application-consistent snapshot.
module "hyperv_policy_tutorial" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-tutorial"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 2
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}💡 Two hours of retention with hourly application-consistent points means exactly two such points exist at any time.
application_consistent_points_within_the_retention_windowreports that number.
3 · The 30-second replication interval
module "hyperv_policy_tight" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-30s"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 30
}
⚠️ This buys a tighter recovery point objective, not more recovery points. Microsoft documents a crash-consistent recovery point every five minutes on a schedule that cannot be configured, so a 30-second replication interval moves data more often while the number of points to choose from is unchanged.replication_interval_is_shorter_than_the_crash_consistent_point_intervalreports the distinction.
4 · Crash-consistent points only
module "hyperv_policy_no_vss" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-crash-only"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 12
application_consistent_snapshot_frequency_in_hours = 0
replication_interval_in_seconds = 300
}💡 Zero disables application-consistent snapshots, and Microsoft's documented default for a new policy is exactly that. It is the right choice where no VSS-aware application needs quiescing — application-consistent snapshots are the most involved and, per Microsoft, affect the performance of applications running on a replicated machine.
5 · No recovery point history at all
module "hyperv_policy_latest_only" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-latest-only"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 0
application_consistent_snapshot_frequency_in_hours = 0
replication_interval_in_seconds = 30
}
⚠️ Zero retention is inside the provider's range and means a failover can use only the latest available point. There is no error at any stage, and a0in a plan reads as a default rather than as a decision — which is whyretains_no_recovery_point_historyexists.
6 · The combination Microsoft recommends against, which nothing rejects
module "hyperv_policy_inverted" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-inverted"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 2
application_consistent_snapshot_frequency_in_hours = 12
replication_interval_in_seconds = 30
}
⚠️ Both values are inside their published ranges, so this applies cleanly. Microsoft's guidance is that the frequency "should always be less than you set for retaining recovery points" — a recommendation, not a limit, which is why this module reports it rather than rejecting it.
Assert on it where the recommendation matters:
check "application_consistent_frequency_fits_the_window" {
assert {
condition = !module.hyperv_policy_inverted.application_consistent_frequency_exceeds_the_retention_window
error_message = "The application-consistent snapshot frequency is longer than the retention window, so no application-consistent point survives to be used."
}
}7 · The rule this resource does NOT have, and the sibling that does
The Azure-to-Azure policy resource enforces the pairing; this one does not. Both configurations are legal, and only one of them fails.
# REJECTED at plan by terraform-azurerm-site-recovery-replication-policy, which carries the documented rule.
module "a2a_policy_rejected" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-replication-policy.git?ref=v1.0.0"
name = "a2a-policy"
recovery_vault_name = module.dr_vault.name
resource_group_name = module.dr_rg.name
recovery_point_retention_in_minutes = 60
application_consistent_snapshot_frequency_in_minutes = 120
}
# ACCEPTED here, because this resource publishes no such rule.
module "hyperv_policy_accepted" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 1
application_consistent_snapshot_frequency_in_hours = 2
replication_interval_in_seconds = 300
}💡 The asymmetry is deliberate on both sides. Each module mirrors what its own resource actually enforces, rather than making the two look consistent by inventing a constraint on one of them.
8 · Asserting the three policy resource types do not collide
This is the hazard with no validation available: two Terraform resource types, one Azure record type, no visibility across configurations.
module "a2a_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-replication-policy.git?ref=v1.0.0"
name = "a2a-policy-24h"
recovery_vault_name = module.dr_vault.name
resource_group_name = module.dr_rg.name
recovery_point_retention_in_minutes = 1440
application_consistent_snapshot_frequency_in_minutes = 60
}
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}
check "the_two_policies_are_distinct_records" {
assert {
condition = (
module.a2a_policy.policy_path_within_the_subscription
!= module.hyperv_policy.policy_path_within_the_subscription
)
error_message = "Both policy modules resolve to the same Azure record. Both will plan, the second apply wins, and neither reports a conflict."
}
}🔒 The two modules reach that value from different arguments — one from a vault name plus a resource group, the other from a vault Resource ID — and produce byte-identical strings. That is what makes the comparison sound rather than coincidental.
9 · Editing the retention window in place
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
# Was 24. Reducing it is an in-place update: no replacement appears in the plan.
recovery_point_retention_in_hours = 1
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}
⚠️ Recovery points outside the new window are not recoverable afterwards, and the plan shows a one-line numeric diff.lifecycleis not valid inside amoduleblock, so a caller cannot addignore_changeshere — the controls are review of the plan and aCanNotDeletemanagement lock on the vault, which prevents deletion rather than editing.
10 · Rejecting the vault-versus-site transposition
# WRONG: this is the Hyper-V site's ID, which the association wants -- not the vault's.
module "hyperv_policy_bad" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy"
recovery_vault_id = module.hyperv_site.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}🔒 Caught at plan by two rules. The site's ID extends a valid vault path, so an unanchored check would accept it and this module would then derive a vault name with four unread trailing segments instead of raising an error. The second rule names the transposition and says which module's output belongs where.
11 · Wiring the sibling that wants a vault NAME
This module receives an ID and re-emits the parts, so a composition does not need both forms from the vault module.
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}
# This sibling takes the vault BY NAME. Its value comes out of the ID above.
module "hyperv_container" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-protection-container.git?ref=v1.0.0"
name = "hyperv-container"
recovery_fabric_name = module.hyperv_site.name
recovery_vault_name = module.hyperv_policy.vault_name
resource_group_name = module.hyperv_policy.vault_resource_group_name
}
⚠️ Read the constantthe_protection_container_is_discovered_at_apply_time_and_exactly_one_must_existon the association module before adding a container to a Hyper-V site. A second container in one site breaks the association permanently.
12 · Several policies at scale, keyed by tier
locals {
hyperv_tiers = {
database = { retention = 24, frequency = 1, interval = 30 }
app = { retention = 12, frequency = 4, interval = 300 }
file = { retention = 4, frequency = 0, interval = 300 }
}
}
module "hyperv_policy_per_tier" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
for_each = local.hyperv_tiers
name = "hyperv-policy-${each.key}"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = each.value.retention
application_consistent_snapshot_frequency_in_hours = each.value.frequency
replication_interval_in_seconds = each.value.interval
}
output "hyperv_policy_paths" {
value = { for k, m in module.hyperv_policy_per_tier : k => m.policy_path_within_the_subscription }
}💡 The file tier disables application-consistent snapshots, which Microsoft recommends for non-database workloads. The database tier pairs the 30-second interval with hourly application-consistent points, which is the heaviest combination this resource can express.
13 · Reporting rather than asserting, for a plan review
output "hyperv_policy_posture" {
value = {
retention_hours = module.hyperv_policy.recovery_point_retention_in_hours
retention_minutes = module.hyperv_policy.recovery_point_retention_in_minutes
app_consistent_points = module.hyperv_policy.application_consistent_points_within_the_retention_window
crash_consistent_only = module.hyperv_policy.only_crash_consistent_recovery_points_are_taken
no_history = module.hyperv_policy.retains_no_recovery_point_history
frequency_exceeds_window = module.hyperv_policy.application_consistent_frequency_exceeds_the_retention_window
frequency_equals_window = module.hyperv_policy.frequency_equals_the_whole_retention_window
at_the_thirty_second_floor = module.hyperv_policy.replicates_at_the_thirty_second_floor
}
}💡
retention_minutesis in the list for one reason: it is the only value directly comparable against the Azure-to-Azure policy module, whose argument is in minutes. Reading two policies in one vault without it invites a factor-of-sixty mistake.
14 · Timeouts, and the key this resource does have
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
timeouts = {
create = "45m"
update = "45m"
}
}ℹ️ All four keys exist here, so
updateis accepted. On the three-key modules in this family — the fabric, the Hyper-V site, the protection container, both mappings and the association — the sameupdatekey is discarded silently, with no error, no warning and no plan diff. A four-keytimeoutsblock is the reliable way to tell from the schema alone that a resource has an update path.
15 · 🏗️ End-to-end composition — a Hyper-V topology
locals {
platform_tags = {
workload = "disaster-recovery"
managed = "terraform"
}
}
module "dr_rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-dr-prod"
location = "eastus"
tags = local.platform_tags
}
module "dr_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"
name = "rsv-dr-prod"
resource_group_name = module.dr_rg.name
location = module.dr_rg.location
sku = "Standard"
tags = local.platform_tags
}
# The site that represents the on-premises Hyper-V hosts. The hosts themselves are registered out of band.
module "hyperv_site" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-services-vault-hyperv-site.git?ref=v1.0.0"
name = "hyperv-site-hq"
recovery_vault_id = module.dr_vault.id
}
module "hyperv_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy.git?ref=v1.0.0"
name = "hyperv-policy-24h"
recovery_vault_id = module.dr_vault.id
recovery_point_retention_in_hours = 24
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
}
# The record that makes the policy applicable to the site.
module "hyperv_association" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-site-recovery-hyperv-replication-policy-association.git?ref=v1.0.0"
name = "hyperv-association"
hyperv_site_id = module.hyperv_site.id
policy_id = module.hyperv_policy.id
}
check "one_vault_holds_the_whole_topology" {
assert {
condition = (
module.hyperv_policy.vault_path_within_the_subscription
== module.hyperv_site.vault_path_within_the_subscription
)
error_message = "The policy and the Hyper-V site are in different vaults. Nothing in the provider checks this and the association's apply will fail against the API."
}
}
check "the_association_binds_the_policy_this_composition_declared" {
assert {
condition = (
module.hyperv_association.policy_path_within_the_subscription
== module.hyperv_policy.policy_path_within_the_subscription
)
error_message = "The association is bound to a policy other than the one declared here."
}
}
output "the_topology_is_still_incomplete" {
description = "Deliberately named for what is missing. All four records exist and nothing replicates yet."
value = module.hyperv_policy.a_policy_with_no_association_protects_nothing
}🔒 This composition is deliberately incomplete, and the closing output says so. Four Azure records exist and no machine is protected: the Hyper-V hosts are registered by installing an agent and using a vault registration key, which no
azurermresource performs — and this provider has no Hyper-V replicated-item resource at all.azurerm_site_recovery_replicated_vm, owned byterraform-azurerm-site-recovery-replicated-vm, models the AZURE-TO-AZURE scenario only.
| Group | Variables |
|---|---|
| Identity | name, recovery_vault_id |
| Recovery points | recovery_point_retention_in_hours, application_consistent_snapshot_frequency_in_hours |
| Replication | replication_interval_in_seconds |
| Universal tail | timeouts (no tags — the resource has none) |
Full input schemas
| Variable | Type | Default | Notes |
|---|---|---|---|
name |
string |
— | Force-new. Two rules: blank, and not a Resource ID. Shares a namespace with the A2A policy resource. |
recovery_vault_id |
string |
— | Force-new. Two rules: an anchored vault-ID shape, and a rejection of a /replicationFabrics/ path. |
recovery_point_retention_in_hours |
number |
— | IntBetween(0, 24). No module rule. Zero is legal. Updatable in place. |
application_consistent_snapshot_frequency_in_hours |
number |
— | IntBetween(0, 12). No module rule. Zero disables application-consistent snapshots. Updatable in place. |
replication_interval_in_seconds |
number |
— | 30 or 300. One module rule, for wording: it names the unit and the likely mistake. Updatable in place. |
timeouts |
object({ create, read, update, delete }) |
{} → 30m / 5m / 30m / 30m |
Four keys, because an Update function exists. Four format rules. |
Nine validations. Two on name, two on recovery_vault_id, one on replication_interval_in_seconds, four on timeouts. The two numeric arguments carry none, deliberately — their provider ranges are exact and state both bounds in their own messages, and the relationship between them is reported rather than enforced.
27 outputs: 6 passthrough, 14 derived, 7 constant. None is sensitive; this module accepts and emits no secret.
| Output | Description |
|---|---|
id |
The policy's Resource ID. Consumed by the association as policy_id. |
name |
The policy's name. |
recovery_vault_id |
The vault, as configured. |
recovery_point_retention_in_hours |
The retention window in the resource's own unit. |
application_consistent_snapshot_frequency_in_hours |
The frequency in the resource's own unit. |
replication_interval_in_seconds |
30 or 300, as configured. |
vault_subscription_id |
Parsed from the vault ID. The subscription the record is created in. |
vault_resource_group_name |
Parsed from the vault ID. |
vault_name |
Parsed from the vault ID, for wiring the siblings that take a vault by name. |
vault_path_within_the_subscription |
Comparable across every authored site-recovery module. |
policy_path_within_the_subscription |
Comparable across all three policy modules, both associations and the replicated-VM module. |
recovery_point_retention_in_minutes |
The window converted, for comparison with the A2A policy module. |
retains_no_recovery_point_history |
True at zero retention. |
application_consistent_snapshots_are_disabled |
True at zero frequency. |
only_crash_consistent_recovery_points_are_taken |
The same condition, named for what still happens. |
application_consistent_frequency_exceeds_the_retention_window |
The state Microsoft recommends against and nothing rejects. |
application_consistent_points_within_the_retention_window |
How many fit, floored. Zero when disabled. |
frequency_equals_the_whole_retention_window |
The boundary of the recommendation. |
replicates_at_the_thirty_second_floor |
True at 30 rather than 300. |
replication_interval_is_shorter_than_the_crash_consistent_point_interval |
Separates replication frequency from recovery point frequency. |
two_terraform_resource_types_write_this_one_azure_record_type |
Constant. The collision with the A2A policy resource. |
the_read_adopts_a_policy_of_the_other_replication_type_without_reporting_it |
Constant. The type assertion with no else. |
the_provider_enforces_no_relationship_between_the_two_recovery_point_settings |
Constant. The rule the sibling has and this one does not. |
narrowing_the_retention_window_is_an_in_place_update_that_discards_recovery_points |
Constant. |
the_crash_consistent_recovery_point_interval_is_not_configurable |
Constant. Five minutes, per Microsoft. |
a_policy_with_no_association_protects_nothing |
Constant. What a green apply does and does not mean. |
whether_a_first_apply_is_refused_depends_on_caller_provider_configuration |
Constant. Plus the 400 tolerance tally across the service. |
The record type is shared, and that is the fact to design around. azurerm_site_recovery_replication_policy and this resource both write a Microsoft.RecoveryServices/vaults/replicationPolicies record. The provider resolves both through the same ReplicationPolicies client and constructs both Resource IDs with the same constructor; the only difference is the provider-specific payload inside the create call. Two modules cannot see each other's arguments, so no validation reaches this — policy_path_within_the_subscription and a check block do.
And the read makes the collision quiet in the other direction. The read sets the three numeric attributes only inside a type assertion for the Hyper-V-to-Azure policy detail, with no else branch. A record of the other type read through this resource leaves all three at their Go zero values, so Terraform proposes changing them on every plan with nothing explaining why. azurerm_site_recovery_fabric, facing the equivalent situation, errors instead and names the Go type it expected. Within one service, one resource tells you and another does not.
The cross-field rule is absent, and the absence is the finding. On the A2A resource the provider documents that the application-consistent frequency must not exceed the retention, enforces it in both bodies, and catches the zero corner at plan time. Here the documentation page carries no note blocks at all, there is no CustomizeDiff, and neither body compares the two numbers. Microsoft's own guidance is a recommendation — "should always be less" — and this suite does not enforce what the provider has not published, so the state is reported through application_consistent_frequency_exceeds_the_retention_window and frequency_equals_the_whole_retention_window. The two policy modules in this library therefore differ on purpose.
Mutability is the security-relevant property here, not immutability. Most of this family cannot be edited at all. This resource can: the three numeric arguments update in place, so narrowing a retention window is a one-line diff with no replacement in the plan and no signal that recovery points are being discarded.
Units are the quiet trap. Retention here is hours with a ceiling of 24; on the sibling it is minutes with a ceiling of 525600. A 24 means a day here and 24 minutes there. recovery_point_retention_in_minutes exists so the two can be compared without arithmetic.
Identity is derived from one argument. The vault arrives as a single Resource ID and the module splits it for the subscription, the resource group and the vault name. The anchored rule on that variable is what makes the fixed indices safe: split() with index access throws from inside a locals block, which is a worse error than a validation failure and arrives from a place the caller was not looking. Anchoring at both ends matters specifically because a Hyper-V site's ID extends a vault's path.
timeouts has four keys and its siblings have three. Declaring a key the resource does not have is a validate error; omitting one it does have means a caller's value is discarded silently. The shape has to match the resource rather than be copied across the family.
There is no secure-defaults table on this module, and the reason is worth stating. All five arguments are required, none is a boolean, none is an enum with a permissive member, and none gates network exposure. There is no empty call to make safe and nothing to close. What this resource governs is retention and replication frequency — availability and cost, which this suite's secure-by-default rule deliberately does not cover.
The choices that are made, and why:
| Decision | Value | Reasoning |
|---|---|---|
| Cross-field rule between the two recovery point settings | Not enforced | The provider documents none and checks none; Microsoft's guidance says should, not must. Reported through two flags instead. |
| Range rules on the two numeric arguments | None | IntBetween(0, 24) and IntBetween(0, 12) are exact and state both bounds in their own messages. |
Rule on replication_interval_in_seconds |
One, for wording | The provider's value set is correct; its message does not say the numbers are seconds, and the probable mistake is writing 5 for five minutes. |
Rules on recovery_vault_id |
Two | The anchored shape makes the module's own derivation total; the second names the likeliest transposition in this family. |
Rules on name |
Two | StringIsNotEmpty accepts a whitespace-only string, so both are genuine coverage. |
| Emitting the subscription in path outputs | Omitted | Matching the weaker form used by the modules that take a vault by name is what keeps every path output in this family comparable. |
terraform init -backend=false
terraform fmt -check
terraform validatePin the module with ?ref=v1.0.0 and never a branch. This library is plan-only: a human applies from CI.
terraform validate proves the configuration parses, the resource's arguments exist and the types line up. It does not fire a variable validation when this module is called from another configuration — root-module validations do fire, and terraform console with a -var-file is the offline harness for them.
What was proven offline for this module:
- All 9 validations fired, each by a fixture built to fail it, with zero condition-evaluation errors. A rule whose condition throws looks exactly like a rule that never fired.
- The fabric-in-the-vault-argument fixture fires both rules on
recovery_vault_id, which is the case the anchoring exists for. - All 15 locals were driven to more than one value across four good fixtures, chosen to reach each posture flag: the ordinary case, zero retention with zero frequency, frequency above retention, and frequency equal to retention.
policy_path_within_the_subscriptionwas evaluated in three modules — this one, the A2A policy module and the association module — from three different argument conventions, and came out byte-identical. That is what makes thecheckblock in example 8 trustworthy.
What only an apply exercises: whether Azure accepts a given combination of the two recovery point settings, and the read path's behaviour against a record written by the other policy resource type.
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-dr-prod/providers/Microsoft.RecoveryServices/vaults/rsv-dr-prod/replicationPolicies/hyperv-policy-24h"
name = "hyperv-policy-24h"
recovery_point_retention_in_hours = 24
recovery_point_retention_in_minutes = 1440
application_consistent_snapshot_frequency_in_hours = 1
replication_interval_in_seconds = 300
vault_name = "rsv-dr-prod"
vault_resource_group_name = "rg-dr-prod"
policy_path_within_the_subscription = "/resourceGroups/rg-dr-prod/providers/Microsoft.RecoveryServices/vaults/rsv-dr-prod/replicationPolicies/hyperv-policy-24h"
application_consistent_points_within_the_retention_window = 24
retains_no_recovery_point_history = false
only_crash_consistent_recovery_points_are_taken = false
application_consistent_frequency_exceeds_the_retention_window = false
replicates_at_the_thirty_second_floor = false
two_terraform_resource_types_write_this_one_azure_record_type = true
the_provider_enforces_no_relationship_between_the_two_recovery_point_settings = true
| Symptom | Cause | Fix |
|---|---|---|
expected replication_interval_in_seconds to be one of [30 300] |
A value other than 30 or 300 — most often 5, meaning "five minutes" | Pass 300 for five minutes. No other interval is expressible through this resource. |
expected recovery_point_retention_in_hours to be in the range (0 - 24) |
A value copied from an A2A policy, where the argument is in minutes | Convert: 1440 minutes is 24 hours, which is this resource's ceiling. |
recovery_vault_id must be a Recovery Services vault Resource ID and nothing deeper |
The Hyper-V site's id was wired instead of the vault's |
Pass the vault module's id; the site's id belongs to the association's hyperv_site_id. |
| A policy created here appears with different settings than configured | Another configuration wrote the same record through azurerm_site_recovery_replication_policy |
Compare policy_path_within_the_subscription across every policy module; see example 8. |
| Every plan proposes changing all three numeric values, with no explanation | The record in Azure is of one of the other two replication types, so the read's type assertion sets nothing | Import or recreate the record through the resource type that matches it. |
A resource with the ID ... already exists on a first apply |
A policy of either resource type already exists at this name in this vault | Import it, rename, or set the provider's overwrite feature knowingly. |
A timeouts key was ignored |
Only on the three-key siblings; not on this resource | Confirm which module the block was written on — object conversion discards an undeclared key silently. |
| Recovery points vanished after a routine change | recovery_point_retention_in_hours was narrowed, which is an in-place update |
Nothing recovers them. Review numeric diffs on this resource as data changes, not tuning. |
azurerm_site_recovery_hyperv_replication_policyazurerm_site_recovery_hyperv_replication_policy_associationazurerm_site_recovery_replication_policy— the Azure-to-Azure policy that shares this record type- Set up disaster recovery of on-premises Hyper-V VMs to Azure
- About Site Recovery — the published 30-second Hyper-V replication floor
- Sibling modules:
terraform-azurerm-site-recovery-hyperv-replication-policy-association,terraform-azurerm-site-recovery-services-vault-hyperv-site,terraform-azurerm-site-recovery-replication-policy,terraform-azurerm-recovery-services-vault - This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."

