Manages one
azurerm_data_protection_backup_policy_diskβ the schedule and retention rules governing managed-disk backups in a Backup Vault. Targetshashicorp/azurerm ~> 4.0.
- π― Creates one disk backup policy inside an existing Backup Vault.
- π Takes the schedule as ISO 8601 repeating intervals (
R/<start>/P1D) and the fallback retention as a plain ISO 8601 duration (P30D) β two different grammars, easily swapped. - ποΈ Accepts one or more retention rules, keyed by rule name.
- π§ Every argument is force-new and there is no update function β this policy can never be edited.
- π« Carries no
tagsβ the resource exposes none.
π‘ Why it matters: you do not edit a backup policy. All six arguments are force-new and the provider declares no update function, so any change destroys and recreates it β taking with it every backup instance that still references the old ID. The intended way to change retention is to create a new policy and repoint the instances, because
backup_policy_idonazurerm_data_protection_backup_instance_diskis deliberately the one argument in this pair that updates in place.
If this module saves you time:
- β Star the repository
- πΌ Connect on LinkedIn
- β Buy me a coffee
flowchart TB
RG["azurerm_resource_group"]
VAULT["azurerm_data_protection_backup_vault"]
DISK["azurerm_managed_disk"]
SNAPRG["snapshot resource group"]
ROLE["role assignments for the vault identity"]
POL["backup_policy_disk"]
INS["backup_instance_disk"]
RG -->|"resource group"| VAULT
VAULT -->|"vault_id, FORCE-NEW"| POL
VAULT -->|"vault_id, FORCE-NEW"| INS
POL -->|"backup_policy_id, the ONLY updatable argument"| INS
DISK -->|"disk_id, FORCE-NEW"| INS
INS -->|"snapshots written to"| SNAPRG
ROLE -.->|"required BEFORE protection can start, not created here"| INS
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef key fill:#004578,stroke:#002b47,color:#ffffff
classDef ext fill:#F0F3F6,stroke:#9AA5B1,color:#1F2933
class POL,INS me
class VAULT key
class RG,DISK,SNAPRG,ROLE ext
The policy is referenced by the instance, never the other way round β and that single edge is the one thing in this pair that can change without replacing anything.
flowchart TB
VN["name, vault_id, intervals, retention_duration, retention_rule, time_zone"]
ALLFN["ALL SIX are FORCE-NEW, and there is NO update function"]
R["azurerm_data_protection_backup_policy_disk.this"]
NEWPOL["To change retention: create a NEW policy, repoint the instance, drop the old one"]
OID["id, name, retention_rule_names"]
OFLAG["retention_rule_count, rules_without_criteria"]
OWARN["a_repeating_interval_is_not_a_duration"]
VN --> ALLFN
ALLFN --> R
R -->|"never edited, only replaced"| NEWPOL
R --> OID
R --> OFLAG
R --> OWARN
classDef me fill:#0078D4,stroke:#004578,color:#ffffff
classDef key fill:#004578,stroke:#002b47,color:#ffffff
classDef ext fill:#F0F3F6,stroke:#9AA5B1,color:#1F2933
class R key
class ALLFN,NEWPOL me
class VN,OID,OFLAG,OWARN ext
| Resource | Count | Role |
|---|---|---|
azurerm_data_protection_backup_policy_disk.this |
1 | The keystone. Retention rules are rendered inline. |
| Item | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures the provider, its authentication, and the mandatory features {} block |
| Module type | standalone |
Schema notes that bite β each verified against the provider source and cross-checked against the provider's own documentation:
- π΄ No update function, and all six arguments force-new. The docs agree on every one: "Changing this forces a new Backup Policy Disk to be created."
- π΄ The nested retention fields are individually force-new too β eleven
ForceNewmarkers for six arguments. - π΄
retention_ruleis NOT capped at one. It is a list with noMaxItems; theMaxItems: 1belongs to the nestedcriteriablock. The docs say "One or moreretention_ruleblocks". - β
The
Timeoutsblock correctly declares noupdate, so Terraform rejects the key outright. Not universal in this service βazurerm_data_protection_resource_guarddeclares one its code never reads. - The two duration fields are genuinely parsed;
backup_repeating_time_intervalshas no validator at all. absolute_criteriais a closed, case-sensitive set from the SDK's ownPossibleValuesForAbsoluteMarker().time_zoneis checked only for non-emptiness and takes a Windows identifier, not IANA.- The name pattern
^[-a-zA-Z0-9]{3,150}$permits a name that is entirely hyphens. - No
CustomizeDiff, no version gate, notags.
| Role | Scope | Why |
|---|---|---|
Backup Contributor (or a custom role with Microsoft.DataProtection/backupVaults/backupPolicies/*) |
the Backup Vault | Create, read and delete policies. There is no update permission to need β the resource has no update path. |
β οΈ Creating a policy grants nothing and protects nothing. The permissions that matter at backup time belong to the vault's managed identity, on the disk and the snapshot resource group, and are granted alongside the backup instance β not here.
Microsoft.DataProtectionregistered in the subscription.- An existing Backup Vault.
- Nothing else. A policy is inert until a backup instance references it.
terraform-azurerm-data-protection-backup-policy-disk/
βββ providers.tf # required_version + the pinned azurerm; no provider block
βββ variables.tf # 7 typed inputs, 13 validations, a THREE-key timeouts object
βββ main.tf # locals + the single keystone resource
βββ outputs.tf # 31 outputs; id first, then name, then the derived facts
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
module "disk_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-daily"
vault_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-backup-eastus/providers/Microsoft.DataProtection/backupVaults/bv-corp"
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
}
β οΈ Note the two grammars. The schedule is a repeating interval beginningR/; the retention is a bare duration. Swapping them puts an unvalidated value in the field the provider does not check.
Consumes
| Input | Type | Source |
|---|---|---|
vault_id |
Resource ID | terraform-azurerm-data-protection-backup-vault β id |
Emits
| Output | Note |
|---|---|
id |
What a backup instance references |
retention_rule_names, retention_rule_count |
Several rules are legal |
rules_without_criteria |
Rules that select nothing |
the_absolute_criteria_set_is_closed_and_case_sensitive |
The five legal values |
force_new_fields |
All six |
1 Β· Minimal β daily backups kept 30 days
module "disk_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-daily"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
}π‘ Without a retention rule, every backup is kept for
default_retention_durationand no longer.
2 Β· Weekly backups
module "weekly" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-weekly"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-06T02:00:00+00:00/P1W"]
default_retention_duration = "P90D"
}βΉοΈ The start instant sets both the first run and the day of week.
P1Wfrom a Sunday means Sundays.
3 Β· Twice a day
module "twice_daily" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-twice-daily"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = [
"R/2026-09-01T11:00:00+00:00/P1D",
"R/2026-09-01T23:00:00+00:00/P1D",
]
default_retention_duration = "P7D"
}π‘ Several intervals are legal and are how sub-daily schedules are expressed.
schedule_countreports how many.
4 Β· A long-term retention rule
module "with_yearly" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-daily-plus-yearly"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
retention_rule = {
"yearly" = {
duration = "P7Y"
priority = 20
absolute_criteria = "FirstOfYear"
}
}
}π‘ The map key becomes the rule's
name. Everything else is kept for the 30-day default; the first backup of each year is kept for seven years.
5 Β· Several rules β the shape the provider actually allows
module "tiered" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-tiered"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
retention_rule = {
"weekly" = { duration = "P1Y", priority = 10, absolute_criteria = "FirstOfWeek" }
"yearly" = { duration = "P7Y", priority = 20, absolute_criteria = "FirstOfYear" }
}
}
β οΈ retention_ruleis a list with noMaxItemsβ several rules are legal. TheMaxItems: 1in the provider belongs to the nestedcriteriablock, which is why each rule takes a singleabsolute_criteria. Reading that cap as belonging to the outer block would silently limit you to one rule.π‘
priorityorders the rules, and this module refuses duplicate priorities β two rules sharing one have no defined order and the provider does not check it.
6 Β· A rule with no criteria β legal and rarely intended
module "no_criteria" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-loose"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
retention_rule = {
"long" = { duration = "P7Y", priority = 10 }
}
}
β οΈ Omittingabsolute_criteriameans nothing selects which backups the rule applies to. The provider permits it; this module reports it throughrules_without_criteriarather than refusing it, because refusing would reject a configuration the provider accepts β and a failed validation blocksterraform destroyas well as apply.
7 Β· The five criteria values
# The complete, CASE-SENSITIVE set, read from the SDK's own PossibleValuesForAbsoluteMarker():
#
# AllBackup FirstOfDay FirstOfWeek FirstOfMonth FirstOfYear
#
# "firstofday" is refused. The module emits the list so a composition can assert on it:
output "legal_criteria" {
value = module.disk_policy.the_absolute_criteria_set_is_closed_and_case_sensitive
}βΉοΈ Each value means the first successful backup of that period β not the first attempt.
8 Β· Time zone, and why it interacts with the schedule
module "with_tz" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-local"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
time_zone = "Eastern Standard Time"
}
β οΈ A Windows identifier, not IANA βAmerica/New_Yorkis refused by this module because the provider checks only that the string is non-empty, and would otherwise accept it.
β οΈ The interval already carries its own UTC offset in the start instant. Setting a time zone as well is how a schedule ends up firing at an hour nobody intended. PreferUTC, or omit the time zone.
9 Β· Why there is no "edit the policy" example
# There is none, because it is not possible. All six arguments are force-new and
# the provider has no update function -- changing a retention duration, a
# schedule or a time zone destroys and recreates the policy.
#
# The intended sequence for changing retention is:
#
# 1. create a NEW policy alongside the old one
# 2. update backup_policy_id on the instances -- an IN-PLACE change
# 3. remove the old policy
#
# Doing it by editing this module's inputs instead destroys the policy and
# leaves every instance still referencing the old ID pointing at nothing.
module "disk_policy_v2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-daily-v2" # a NEW name, alongside the old policy
vault_id = var.backup_vault_id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P90D" # the change
}π΄
this_policy_can_never_be_updatedandchanging_retention_means_a_new_policy_not_an_editstate this as constants, because it is the single most consequential fact about the resource and nothing in a plan explains it.
10 Β· Many policies from one map
locals {
tiers = {
"gold" = { interval = "R/2026-09-01T23:00:00+00:00/P1D", retain = "P365D" }
"silver" = { interval = "R/2026-09-06T02:00:00+00:00/P1W", retain = "P90D" }
"bronze" = { interval = "R/2026-09-06T02:00:00+00:00/P1W", retain = "P30D" }
}
}
module "tiers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
for_each = local.tiers
name = "bp-disk-${each.key}"
vault_id = var.backup_vault_id
backup_repeating_time_intervals = [each.value.interval]
default_retention_duration = each.value.retain
}π‘ Because policies cannot be edited, having several named tiers up front β and moving instances between them β is a more workable pattern than tuning one policy.
11 Β· Reviewing the policy before an apply
output "policy_review" {
value = {
schedules = module.disk_policy.backup_repeating_time_intervals
schedule_count = module.disk_policy.schedule_count
default_retain = module.disk_policy.default_retention_duration
rules = module.disk_policy.retention_rule_names
rule_count = module.disk_policy.retention_rule_count
loose_rules = module.disk_policy.rules_without_criteria
replace_on = module.disk_policy.force_new_fields
}
}
check "every_rule_selects_something" {
assert {
condition = length(module.disk_policy.rules_without_criteria) == 0
error_message = "A retention rule has no absolute_criteria and selects nothing in particular."
}
}π‘ Every value is known at plan.
force_new_fieldslists all six arguments, which on this resource is the same as saying "everything".
12 Β· ποΈ End-to-end composition
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-backup-eastus"
location = "eastus"
}
module "backup_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-vault.git?ref=v1.0.0"
name = "bv-corp"
resource_group_name = module.resource_group.name
location = module.resource_group.location
datastore_type = "VaultStore"
redundancy = "LocallyRedundant"
identity = {
type = "SystemAssigned"
}
}
module "disk_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-disk.git?ref=v1.0.0"
name = "bp-disk-daily"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_duration = "P30D"
retention_rule = {
"yearly" = { duration = "P7Y", priority = 20, absolute_criteria = "FirstOfYear" }
}
}
module "disk_backup" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-instance-disk.git?ref=v1.0.0"
name = "bi-disk-data-01"
location = module.resource_group.location
vault_id = module.backup_vault.id
disk_id = var.managed_disk_id
snapshot_resource_group_name = "rg-snapshots-eastus"
backup_policy_id = module.disk_policy.id
}
output "policy_id" {
value = module.disk_policy.id
}π΄ This composition is not finished when the apply is. The backup vault's managed identity needs Disk Backup Reader on the disk and Disk Snapshot Contributor on
rg-snapshots-eastus. Neither is created here, and without them the apply is green while protection silently fails β see the instance module'sprotection_state.
β οΈ The policy and the instance must live in the same vault. Both takevault_idand the policy's ID embeds its own vault; the provider compares them nowhere, so the instance module reports the comparison instead.
Required: name, vault_id, backup_repeating_time_intervals, default_retention_duration.
Optional: retention_rule, time_zone, timeouts.
Full input schemas
| Name | Type | Default | Notes |
|---|---|---|---|
name |
string |
β | Force-new. ^[-a-zA-Z0-9]{3,150}$, reproduced exactly including its looseness. |
vault_id |
string |
β | Force-new. Anchored to backupVaults/<vault> with $. |
backup_repeating_time_intervals |
list(string) |
β | Force-new. MinItems: 1. ISO 8601 repeating intervals. No provider validator. |
default_retention_duration |
string |
β | Force-new. ISO 8601 duration, genuinely parsed by the provider. |
retention_rule |
map(object({ duration, priority, absolute_criteria })) |
{} |
Force-new. Keyed by rule name. Several are legal. |
time_zone |
string |
null |
Force-new. A Windows identifier. Provider checks non-emptiness only. |
timeouts |
object({create, read, delete}) |
null |
THREE keys β there is no update. |
| Output | Type | Notes |
|---|---|---|
id |
string |
What a backup instance references |
name / vault_id / vault_name / resource_group_name / subscription_id |
string |
|
backup_repeating_time_intervals / schedule_count |
||
default_retention_duration |
string |
|
has_retention_rule / retention_rule_names / retention_rule_count |
||
rules_without_criteria |
list(string) |
Rules that select nothing |
the_absolute_criteria_set_is_closed_and_case_sensitive |
list(string) |
The five legal values |
force_new_fields |
list(string) |
All six arguments |
import_address |
string |
For terraform import |
the CONSTANT outputs |
bool |
Facts that are consequential, invisible in state, and inferable from nothing else |
Nothing is sensitive: this resource carries no credential.
This policy can never be updated. All six arguments are force-new and the provider declares no update function at all β the Resource struct has Create, Read and Delete and nothing else. The nested retention fields are marked force-new individually as well, which is why the file carries eleven ForceNew markers for six arguments. Consequently there is no counterpart list of updatable fields in the outputs: there are none.
So retention changes are a create-and-repoint, never an edit. Create the new policy alongside the old, update backup_policy_id on each instance β an in-place change that disturbs no data β then remove the old policy. Editing this module's inputs instead destroys the policy, and any instance still referencing the old ID is left pointing at nothing.
retention_rule is not capped, and that is easy to get backwards. The provider models it as a list with no MaxItems; the MaxItems: 1 belongs to the nested criteria block. So several rules are legal β a common shape is one keeping the first backup of each week for a year and another keeping the first of each year for seven β while each individual rule carries exactly one absolute_criteria. This module takes a map keyed by rule name so the names are unique by construction, and refuses duplicate priority values, which the provider does not check and which leave two rules with no defined order.
Two grammars, both called ISO 8601. backup_repeating_time_intervals takes R/<start instant>/<period>; default_retention_duration and each rule's duration take a bare Pβ¦ duration. The durations are genuinely parsed by the provider and a malformed one is refused. The repeating intervals carry no validator at all, so a malformed schedule is accepted by Terraform and rejected by Azure β which is why this module adds shape checks of its own for exactly that field.
time_zone is barely validated and interacts with the schedule. It takes a Windows identifier, the provider checks only non-emptiness, and the repeating interval already carries its own UTC offset. Setting both is how a schedule fires at an unintended hour.
features {} dependence. As with every module in this suite, the caller supplies provider "azurerm" { features {} }.
| Concern | Default in the empty call | Opt-out |
|---|---|---|
| Retention | No rule β every backup kept for default_retention_duration |
add retention_rule entries |
| Number of rules | Unbounded by the provider; the module reflects that | none |
| Duplicate rule priorities | Refused β no defined order, and the provider does not check | none |
| Schedule validity | Shape-checked here because the provider checks nothing | none |
| Name rules | The provider's exact pattern, including permitting --- |
none |
| Time zone | Left to the service; only the IANA-style mistake is refused | set a Windows identifier |
| A rule with no criteria | Reported, not refused | none needed |
Two rules govern the validations. Never invent a constraint that could reject legal input β a validation {} failure blocks terraform destroy as well as apply, which is why the loose name pattern is reproduced rather than tightened. And enforce a configuration rule, report a service one β the interval's shape and the criteria set are enforced; a criteria-less rule is reported.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This module is plan-only; a human applies from CI.
Know which command reaches which check. terraform validate on a configuration that CALLS this module checks types and syntax only: it evaluates none of the module's variable values. The 13 validation {} blocks are reached at terraform plan, and none of them needs credentials, because variable validation runs before the provider is configured. To exercise them without a plan, run the module as the root module and drive it through terraform console -var-file=....
Because this resource has no CustomizeDiff and no version gate, nothing is deferred to plan by the provider.
What no Terraform stage exercises: whether a repeating interval is one Azure will accept, whether the time zone identifier is real, and whether any backup instance uses this policy. The first two fail at apply; the third never fails at all β an unused policy is simply inert.
$ terraform output
id = "/subscriptions/.../backupVaults/bv-corp/backupPolicies/bp-disk-daily"
name = "bp-disk-daily"
vault_name = "bv-corp"
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
schedule_count = 1
default_retention_duration = "P30D"
retention_rule_names = ["yearly"]
retention_rule_count = 1
rules_without_criteria = []
force_new_fields = ["name", "vault_id", "backup_repeating_time_intervals", "default_retention_duration", "retention_rule", "time_zone"]
this_policy_can_never_be_updated = true| Symptom | Cause | Fix |
|---|---|---|
| A plan shows the policy being destroyed and recreated after a small edit | Every argument is force-new and there is no update function. | Expected. Create a new policy and repoint the instances instead β see Example 9. |
| Recreating the policy broke the backup instances | The instances referenced the old policy ID, which no longer exists. | Sequence it: new policy β update backup_policy_id β remove the old policy. |
every backup_repeating_time_intervals entry must be an ISO 8601 REPEATING interval beginning "R/" |
A bare duration such as P1D was put in the schedule field. |
Use R/<start>/P1D. A bare duration belongs in default_retention_duration. |
| The schedule was accepted and Azure rejected it at apply | The provider applies no validator to the intervals. This module checks the shape only. | Check the interval against Azure's expected format. |
every retention_rule absolute_criteria must be one of "AllBackup", "FirstOfDay", "FirstOfWeek", "FirstOfMonth" or "FirstOfYear", case-sensitively. |
A misspelled or lowercased criteria value. | Match the casing exactly. |
retention_rule priorities must be distinct |
Two rules share a priority. |
Give them different priorities; the provider does not check this and the order would be undefined. |
| I can only configure one retention rule | A misreading of the provider's MaxItems: 1, which belongs to the nested criteria block. |
Several rules are legal β see Example 5. |
time_zone looks like an IANA name such as "America/New_York". |
An IANA zone was supplied. | Use a Windows identifier such as Eastern Standard Time, or UTC. |
| Backups run at an unexpected hour | The interval's start instant carries a UTC offset and a time zone was set. | Prefer UTC, or omit time_zone. |
azurerm_data_protection_backup_policy_diskazurerm_data_protection_backup_instance_disk- Microsoft Learn β Azure Disk Backup
- Sibling modules:
terraform-azurerm-data-protection-backup-instance-disk,terraform-azurerm-data-protection-backup-vault - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."