Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure Data Lake Storage Backup Policy Terraform Module

Manages one azurerm_data_protection_backup_policy_data_lake_storage β€” the retention and schedule contract a Data Lake backup instance points at. Targets hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat

🧩 Overview

  • 🎯 Creates one Data Lake Storage backup policy inside a Backup Vault.
  • 🧊 Every argument is force-new and there is no update function β€” nothing about this policy can ever be edited in place.
  • πŸ›οΈ Vaulted backup only β€” a single required retention duration, not the two optional tiers the blob storage policy carries.
  • πŸ• Validates the schedule (1 to 5 intervals) and the time zone (a closed set of 142) β€” both of which its blob storage sibling leaves unchecked.
  • 🚫 Carries no tags β€” the resource exposes none.

πŸ’‘ Why it matters: this looks like a clone of the blob storage backup policy and is not. There is one retention duration and it is required; the schedule is required, validated and capped at five; the retention rule's fields are flat, with no criteria or life_cycle block and no priority or data_store_type at all; there is no days_of_month; and time_zone is checked against a closed set rather than merely being non-empty. A configuration ported across does not fit, and several parts of it would be wrong in ways that only surface at apply.

❀️ Support this project

If this module saves you time:

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

flowchart TB
  RG["azurerm_resource_group"]
  VAULT["azurerm_data_protection_backup_vault"]
  SA["azurerm_storage_account with HIERARCHICAL NAMESPACE"]
  MI["the vault system-assigned identity"]
  ROLE["Storage Account Backup Contributor assignment"]
  POL["backup_policy_data_lake_storage"]
  INS["backup_instance_data_lake_storage"]

  RG -->|"resource group"| VAULT
  VAULT -->|"data_protection_backup_vault_id, FORCE-NEW"| POL
  VAULT -->|"data_protection_backup_vault_id, FORCE-NEW"| INS
  POL -->|"backup_policy_data_lake_storage_id, updates IN PLACE"| INS
  SA -->|"storage_account_id, FORCE-NEW"| INS
  SA -->|"location, taken on trust and never verified"| INS
  VAULT -->|"system-assigned identity"| MI
  MI -->|"principal_id"| ROLE
  ROLE -.->|"on the storage account, BEFORE the instance"| INS
  SA -.->|"is_hns_enabled required, checked by NOTHING"| 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,SA,MI,ROLE ext
Loading

This module is one half of a pair. The solid edge from the policy to the instance is the only link between them, and it is the one edge in the diagram that updates in place β€” which is what makes the create-new-then-repoint sequence possible at all.

🧬 What this module builds

flowchart TB
  VIN["name, data_protection_backup_vault_id"]
  VDUR["default_retention_duration, REQUIRED -- one tier only"]
  VSCH["backup_schedule, REQUIRED, 1 to 5 intervals, VALIDATED"]
  VRULE["retention_rule map -- flat fields, no priority, no data_store_type"]
  VTZ["time_zone, a CLOSED set of 142 identifiers"]
  ALL["EVERY argument is FORCE-NEW, and there is NO update function"]
  R["azurerm_data_protection_backup_policy_data_lake_storage.this"]
  OID["id -- the value a backup instance references"]
  OVAULT["this_resource_backs_up_to_the_vault_only"]
  OREP["rules_narrowing_an_absolute_criteria, REPORTED not refused"]
  ONEW["changing_retention_means_a_new_policy_not_an_edit"]

  VIN --> ALL
  VDUR --> ALL
  VSCH --> ALL
  VRULE --> ALL
  VTZ --> ALL
  ALL --> R
  R --> OID
  R --> OVAULT
  R --> OREP
  R --> ONEW

  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 ALL,ONEW me
  class VIN,VDUR,VSCH,VRULE,VTZ,OID,OVAULT,OREP ext
Loading
Resource Count Role
azurerm_data_protection_backup_policy_data_lake_storage 1 (this) The keystone, and the only resource here.

βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None here. The caller configures the provider, its authentication and its features {} block.

Schema notes that bite, each verified against the provider source rather than inferred:

  • πŸ”΄ There is no update function. Create, read and delete only. This is why the timeouts block has three keys β€” a fourth is rejected by Terraform, not silently ignored.
  • πŸ”΄ Every single argument is force-new. force_new_fields lists all six. data_protection_backup_vault_id takes its marker from a shared helper, so a reading of the resource file alone shows one fewer.
  • πŸ”΄ A cross-field rule is enforced inside the CREATE function. Every retention rule needs at least one of absolute_criteria and days_of_week, and the provider's own refusal therefore arrives at apply, after a plan has been approved. This module checks the same rule offline.
  • πŸ”΄ backup_schedule is required, validated, and capped at five intervals. Minimum one, maximum five, each parsed as a repeating interval. Neither bound appears in the exported schema. The blob storage policy's equivalent is optional with no validator at all.
  • πŸ”΄ time_zone is a closed, case-sensitive set of 142 Windows identifiers. The blob storage policy checks only that the string is non-empty, so it takes a wrong identifier silently.
  • πŸ”΄ The retention rule's fields are FLAT β€” no criteria block, no life_cycle block, no priority, no data_store_type. The blob storage policy has all four.
  • There is no days_of_month. Month-end must go through weeks_of_month = ["Last"] with a day selector.
  • Four selector lists carry a minimum of one item, invisible in the exported schema. An empty list is refused with an error naming neither the field nor the reason.
  • The name pattern requires a leading letter: ^[a-zA-Z][-a-zA-Z0-9]{2,149}$. The blob storage policy's does not.
  • The requires-import guard can be disabled by a provider feature, which would let a create overwrite an existing policy.
  • No CustomizeDiff, no version gate, no tags.

πŸ”‘ Required Azure RBAC Roles / Permissions

Role Scope Why
Backup Contributor (or a custom role with Microsoft.DataProtection/backupVaults/backupPolicies/*) the Backup Vault Create, read and delete backup policies. There is no update permission to need, because there is no update.
Reader the Backup Vault Sufficient for terraform plan to refresh an existing policy.

ℹ️ This policy is not a grant. It names no storage account and no identity. The permission that decides whether backup actually works β€” Storage Account Backup Contributor, held by the vault's system-assigned identity on the protected account β€” belongs to the instance, not here.

Azure Prerequisites

  • The Microsoft.DataProtection resource provider registered in the subscription.
  • An existing Backup Vault. The policy is created inside it and cannot be moved between vaults.
  • A retention decision taken before the first apply β€” changing it later replaces the policy.
  • The caller's provider "azurerm" { features {} } block. The module declares none.

πŸ“ Module Structure

terraform-azurerm-data-protection-backup-policy-data-lake-storage/
β”œβ”€β”€ providers.tf     # required_version + the azurerm ~> 4.0 pin. No provider block.
β”œβ”€β”€ variables.tf     # 7 variables, 18 validations, every enum mirrored from the provider's own source
β”œβ”€β”€ main.tf          # one keystone `this`, a dynamic retention_rule over a keyed map
β”œβ”€β”€ outputs.tf       # 40 outputs; id first, then name, then the facts
β”œβ”€β”€ README.md        # this file
β”œβ”€β”€ SCOPE.md         # the cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore

βš™οΈ Quick Start

provider "azurerm" {
  features {}
}

module "data_lake_backup_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-daily-30d"
  data_protection_backup_vault_id = module.backup_vault.id

  # Both are REQUIRED on this resource. Data Lake backup is vaulted only, so there
  # is one retention duration and a schedule is always needed.
  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]
}

πŸ”’ The caller configures the provider, its authentication and its features {} block. This module declares none of them.

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source
data_protection_backup_vault_id Resource ID terraform-azurerm-data-protection-backup-vault β†’ id
name string, required caller
default_retention_duration ISO 8601 duration, required caller
backup_schedule list(string), required, 1–5 caller
retention_rule map(object) keyed by rule name caller
time_zone, timeouts see Inputs caller

Emits

Output Description Consumed by
id The policy's Resource ID …-backup-instance-data-lake-storage β†’ backup_policy_data_lake_storage_id
this_resource_backs_up_to_the_vault_only The tier model review
rules_narrowing_an_absolute_criteria Reported, not refused review
force_new_fields All six arguments change review

πŸ“š Example Library

1 Β· The smallest real policy
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-daily-30d"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]
}

ℹ️ There is no smaller call. Both the duration and the schedule are Required β€” unlike the blob storage backup policy, where the schedule is optional because its operational tier is continuous. Data Lake backup is vaulted only, so a schedule is always needed.

2 Β· Vaulted only β€” there is no operational tier here
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-vaulted"
  data_protection_backup_vault_id = module.backup_vault.id

  # ONE duration. There is no `operational_default_retention_duration` on this
  # resource and no `vault_default_retention_duration` either -- just this.
  default_retention_duration = "P365D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]
}

output "vault_only" {
  value = module.policy.this_resource_backs_up_to_the_vault_only
}

πŸ’‘ This is the good case, not a limitation. Vaulted backup copies data into the Backup Vault, so it survives loss of the storage account. The blob storage policy's operational tier keeps data on the account and does not.

3 Β· Several schedule intervals β€” and the cap at five
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-four-times-daily"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"

  # Four start instants, each repeating daily. A FIFTH is legal; a SIXTH is not.
  backup_schedule = [
    "R/2026-09-01T00:00:00+00:00/P1D",
    "R/2026-09-01T06:00:00+00:00/P1D",
    "R/2026-09-01T12:00:00+00:00/P1D",
    "R/2026-09-01T18:00:00+00:00/P1D",
  ]
}

⚠️ Minimum one, maximum five, and neither bound appears in the exported schema. The provider's own refusal for a sixth is a bare "too many list items" that names no reason; this module reports the limit before the provider does.

4 Β· The schedule is validated here β€” unlike the blob policy
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-shapes"
  data_protection_backup_vault_id = module.backup_vault.id

  # A BARE duration.
  default_retention_duration = "P30D"

  # A REPEATING INTERVAL: "R/", a start instant, then the period. On THIS resource
  # the provider parses it. On the blob storage policy it applies no validator at
  # all, so the same mistake there reaches Azure.
  backup_schedule = ["R/2026-09-01T23:00:00+00:00/P1D"]
}

πŸ”’ Swapping the two is the likeliest misconfiguration, because both are called "ISO 8601" and both are strings. Writing "P1D" into backup_schedule is refused here by the provider and by this module. See a_repeating_interval_is_not_a_duration and the_schedule_is_validated_here_unlike_the_blob_policy.

5 Β· A retention rule β€” note the FLAT fields
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-weekly"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    "keep-weekly" = {
      # `duration` sits DIRECTLY on the rule. There is no `life_cycle` block here,
      # and no `criteria` block either -- the selectors are flat too.
      duration          = "P12W"
      absolute_criteria = "FirstOfWeek"
    }
  }
}

πŸ”΄ A rule written for the blob storage backup policy will not parse here. That resource nests duration inside life_cycle and the selectors inside criteria, and additionally requires a priority. This one has none of those. See the_criteria_fields_are_flat_on_this_resource.

6 Β· Several rules, and no priority to order them
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-tiered"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    "keep-weekly" = {
      duration          = "P12W"
      absolute_criteria = "FirstOfWeek"
    }
    "keep-monthly" = {
      duration          = "P24M"
      absolute_criteria = "FirstOfMonth"
    }
    "keep-yearly" = {
      duration          = "P7Y"
      absolute_criteria = "FirstOfYear"
    }
  }
}

ℹ️ There is no priority field on this resource, so there is nothing to order and nothing to keep unique β€” the blob storage backup policy requires one on every rule, and this module's counterpart there enforces uniqueness. Neither applies here.

7 Β· Every rule needs absolute_criteria or days_of_week
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-selectors"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    # Selects by absolute_criteria.
    "weekly" = {
      duration          = "P12W"
      absolute_criteria = "FirstOfWeek"
    }
    # Selects by days_of_week instead. Either satisfies the rule; both together
    # are legal too.
    "sundays" = {
      duration     = "P8W"
      days_of_week = ["Sunday"]
    }
  }
}

πŸ”΄ This check exists in the provider β€” but inside its CREATE function. Its refusal therefore arrives at apply, after a plan has been reviewed and approved. Both fields sit in one object here, so this module runs the same check offline. It is an earlier failure, not a stricter rule. See every_rule_needs_absolute_criteria_or_days_of_week.

8 Β· All seven weekdays, Wednesday included
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-midweek"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    "midweek" = {
      duration     = "P8W"
      days_of_week = ["Wednesday"]
    }
  }
}

⚠️ Worth stating because of a defect in the neighbouring documentation. The provider's registry page for the blob storage backup policy lists the legal days as "Monday, Tuesday, Thursday, Friday, Saturday and Sunday" β€” omitting Wednesday β€” while every validator in this family accepts all seven. A reader carrying that list across would refuse a legal value. all_seven_weekdays_are_legal emits the real set.

9 Β· Month-end, without a days_of_month field
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-month-end"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    "month-end" = {
      duration = "P36M"
      # There is NO days_of_month on this resource. Month-end is expressed as the
      # LAST week plus a day selector.
      days_of_week   = ["Sunday"]
      weeks_of_month = ["Last"]
    }
  }
}

πŸ”΄ The blob storage backup policy has a days_of_month field that encodes month-end as 0. This resource has no such field at all, so that idiom does not transfer. See there_is_no_days_of_month_on_this_resource.

10 Β· Narrowing with weeks_of_month and months_of_year
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-quarterly"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  retention_rule = {
    "quarterly-first-sunday" = {
      duration       = "P5Y"
      days_of_week   = ["Sunday"]
      weeks_of_month = ["First"]
      months_of_year = ["January", "April", "July", "October"]
    }
  }
}

⚠️ weeks_of_month and months_of_year NARROW a selection; they do not make one. A rule that sets either while selecting only by absolute_criteria is legal and does something, but reads as though it selected the backups by itself. rules_narrowing_an_absolute_criteria names those rules so the intent can be confirmed rather than assumed β€” reported, never refused.

11 Β· scheduled_backup_times, and the empty-list trap
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-pinned"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P60D"
  backup_schedule            = ["R/2026-09-01T03:00:00+00:00/P1D"]

  retention_rule = {
    "pinned-times" = {
      duration               = "P90D"
      days_of_week           = ["Sunday"]
      scheduled_backup_times = ["2026-09-06T03:00:00Z"]

      # Do NOT write `months_of_year = []`. Omit the field instead -- the provider
      # puts a minimum of one item on all four selector lists, and its error names
      # neither the field nor the reason. This module sends an empty list as null,
      # so omitting and clearing produce the same plan either way.
    }
  }
}

⚠️ Four selector lists carry an invisible minimum of one item β€” days_of_week, months_of_year, weeks_of_month and scheduled_backup_times. None of those minimums appears in the schema Terraform exports. See the_selector_lists_reject_an_empty_list.

12 Β· time_zone β€” a closed set of 142, checked
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-eastern"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00-05:00/P1D"]

  # A WINDOWS identifier from the provider's own closed set. Not "America/New_York",
  # and not a near-miss like "Eastern Time" -- both are refused.
  time_zone = "Eastern Standard Time"
}

πŸ”’ This is a real check here and not on the sibling. The provider validates time_zone against 142 identifiers, case-sensitively; the blob storage backup policy accepts any non-empty string and therefore takes a wrong identifier silently. Both UTC and Coordinated Universal Time are members, as are offset forms such as UTC-02 and UTC+13.

ℹ️ Note the interaction: the start instants in backup_schedule already carry their own UTC offset.

13 Β· Three timeout keys, not four
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-timeouts"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P30D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]

  timeouts = {
    create = "30m"
    read   = "5m"
    delete = "30m"
  }
}

πŸ”’ There is no update key, and that is correct. The resource has no update function, so no update timeout is declared and Terraform rejects the key outright β€” worth stating because object-type conversion normally silently discards an undeclared key.

ℹ️ On this resource each operation declares its own timeout beside its own function, so none can bound an operation other than the one it names. That is a property of the provider's typed framework rather than luck; the defect where a read silently runs on the create timeout is possible in the older framework and impossible here.

14 Β· Changing retention β€” create the new policy, then repoint
# THE POLICY CANNOT BE EDITED. Every argument is force-new and there is no update
# function, so raising retention from 30 to 365 days is a migration, not an edit.
#
#   1. create a NEW policy alongside the old one
#   2. point the instance at it -- an IN-PLACE update, no data disturbed
#   3. remove the old policy, once nothing references it

module "policy_v2" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-365d"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P365D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]
}

module "instance" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-instance-data-lake-storage.git?ref=v1.0.0"

  name                            = "bi-adls-prod"
  location                        = module.storage.location
  data_protection_backup_vault_id = module.backup_vault.id
  storage_account_id              = module.storage.id
  storage_container_names         = ["data"]

  # The in-place update.
  backup_policy_data_lake_storage_id = module.policy_v2.id
}

πŸ”΄ Do not reverse the order. Editing the old policy in place destroys it, and any instance still referencing the old ID goes with it β€” nothing in Terraform records which instances use a policy, because the reference is held on the instance. See destroying_this_policy_breaks_every_instance_using_it.

15 Β· πŸ—οΈ 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-backup-eastus"
  location = "eastus"
}

module "storage" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"

  name                = "stadlsprod01"
  resource_group_name = module.rg.name
  location            = module.rg.location

  # πŸ”΄ THIS IS WHAT MAKES IT DATA LAKE STORAGE GEN2, and backup here requires it.
  # Nothing in the backup resources checks it -- an account without it fails when
  # protection is configured.
  is_hns_enabled = true

  containers = {
    "data" = {}
  }
}

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.rg.name
  location            = module.rg.location
  datastore_type      = "VaultStore"

  identity = {
    type = "SystemAssigned"
  }
}

# THIS MODULE.
module "policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-data-lake-storage.git?ref=v1.0.0"

  name                            = "bp-adls-hybrid"
  data_protection_backup_vault_id = module.backup_vault.id

  default_retention_duration = "P365D"
  backup_schedule            = ["R/2026-09-01T23:00:00+00:00/P1D"]
  time_zone                  = "UTC"

  retention_rule = {
    "keep-monthly" = {
      duration          = "P24M"
      absolute_criteria = "FirstOfMonth"
    }
  }
}

# The grant the INSTANCE needs, on the STORAGE ACCOUNT, held by the VAULT's
# identity. Sequence it ahead of the instance.
module "vault_grant" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"

  scope = module.storage.id

  role_assignments = {
    "vault-backup" = {
      role_definition_name = "Storage Account Backup Contributor"
      principal_id         = module.backup_vault.identity_principal_id
    }
  }
}

module "instance" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-instance-data-lake-storage.git?ref=v1.0.0"

  name                            = "bi-adls-prod"
  data_protection_backup_vault_id = module.backup_vault.id

  # The STORAGE ACCOUNT's region, from the account itself -- nothing verifies it.
  location                           = module.storage.location
  storage_account_id                 = module.storage.id
  backup_policy_data_lake_storage_id = module.policy.id

  storage_container_names = ["data"]

  depends_on = [module.vault_grant]
}

output "policy_id" {
  value = module.policy.id
}

output "vault_only" {
  value = module.policy.this_resource_backs_up_to_the_vault_only
}

output "protection_state" {
  value = module.instance.protection_state
}

πŸ”΄ Two things here are doing quiet work. is_hns_enabled = true is the Data Lake Gen2 requirement that nothing in the backup resources checks, and depends_on on the grant orders a permission that nothing in the instance's arguments references β€” without it Terraform may create the instance first and block on a protection poll waiting for access it does not have.

πŸ“₯ Inputs

Required: name, data_protection_backup_vault_id, default_retention_duration, backup_schedule. Optional: retention_rule, time_zone, timeouts.

Full schemas
variable "retention_rule" {
  type = map(object({
    duration               = string
    absolute_criteria      = optional(string)
    days_of_week           = optional(list(string))
    months_of_year         = optional(list(string))
    weeks_of_month         = optional(list(string))
    scheduled_backup_times = optional(list(string))
  }))
  default = {}
  # Flat -- no `criteria` block, no `life_cycle` block, no `priority`,
  # no `data_store_type`, no `days_of_month`.
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    delete = optional(string)
  })
  default = null
  # Provider defaults: create 30m, read 5m, delete 30m. There is no update.
}

🧾 Outputs

Output Description Notes
id Policy Resource ID Emitted first. The value an instance references.
name, data_protection_backup_vault_id, vault_name, resource_group_name, subscription_id Identity and owning vault Parsed from the vault ID.
default_retention_duration, backup_schedule, schedule_count Retention and schedule Known at plan.
retention_rule_names, retention_rule_count, rules_using_absolute_criteria, rules_using_days_of_week The rules and how each selects Known at plan.
rules_narrowing_an_absolute_criteria Reported, not refused Known at plan.
the_absolute_criteria_set_is_closed_and_case_sensitive, all_seven_weekdays_are_legal, the_week_number_set The real enum sets, as values Taken from the provider's SDK constants.
force_new_fields All six arguments β€”
this_policy_can_never_be_updated, changing_retention_means_a_new_policy_not_an_edit, destroying_this_policy_breaks_every_instance_using_it The lifecycle facts Constant.
this_resource_backs_up_to_the_vault_only, the_criteria_fields_are_flat_on_this_resource, there_is_no_days_of_month_on_this_resource Differences from the blob storage policy Constant.
the_schedule_is_capped_at_five_intervals, the_schedule_is_validated_here_unlike_the_blob_policy, a_repeating_interval_is_not_a_duration, the_selector_lists_reject_an_empty_list Schedule and selector traps Constant.
every_rule_needs_absolute_criteria_or_days_of_week A provider check moved earlier Constant.
time_zone, time_zone_was_left_to_the_service, the_time_zone_set_is_closed_and_case_sensitive Time zone β€”
the_provider_declares_no_update_timeout_and_that_is_correct, the_timeout_wiring_defect_class_cannot_occur_here, no_customize_diff_guards_this_resource Where checks do and do not fire Constant.
this_resource_supports_no_azure_resource_tags Why there is no tags tail Constant.
import_address, the_import_guard_names_this_resource_correctly, the_import_check_can_be_disabled_by_a_provider_feature Import The guard is correct by construction.
lifecycle_prevent_destroy_is_not_available_to_a_module_caller A caller cannot add prevent_destroy Constant.

No output is a secret, and nothing here is marked sensitive β€” there is nothing sensitive to mark.

🧠 Architecture Notes

The absence of an update function is the whole story, exactly as on the blob storage backup policy. All six arguments are force-new, and even if one were not, there is no update function to call. Treat this module's inputs as a version, not as settings: change any of them and you are provisioning a new policy. The safe sequence β€” create, repoint, remove β€” depends on a fact that lives in a different resource, so both modules state it.

Everything else about this resource differs from that sibling, and that is the point of most of the documentation here. Data Lake Storage backup is vaulted only, so there is one required retention duration instead of two optional tiers under an at-least-one rule; the schedule is required rather than optional, and validated rather than unchecked, and capped at five intervals; the retention rule's fields are flat, with no criteria block, no life_cycle block, no priority and no data_store_type; there is no days_of_month; the name pattern demands a leading letter; and even the vault argument is spelled differently. The two resources look like clones and behave like different products, so this module emits the differences as outputs rather than only describing them β€” a reader porting a configuration needs them where they will be seen.

One provider check is enforced in the wrong place, and this module moves it. Every retention rule needs at least one of absolute_criteria and days_of_week. The provider agrees β€” but it applies the rule inside its create function, so its refusal arrives at apply, after a plan has been reviewed and approved. Both fields sit in one object here, so the same check runs offline. It is worth being precise about what that does and does not mean: the module adds no constraint, it only makes an existing one fail earlier.

Two constraints are invisible in the schema Terraform exports and both produce unhelpful errors when hit. backup_schedule has a floor of one interval and a ceiling of five; the four selector lists each have a floor of one item. The provider's messages for these name neither the field nor the reason, so this module checks both and says why. The selector lists are additionally sent as null when empty, so omitting a selector and clearing it produce the same plan.

The 142 time zones and the four enum sets are mirrored from the provider's own source rather than transcribed. That matters more than it sounds: the SDK's enum helpers return constant names rather than string literals, so the values have to be resolved rather than assumed equal β€” and this library has previously found constant names that differ from their values with no rule. The module's lists are asserted equal to that extraction.

lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy here. Where deletion protection matters, put a CanNotDelete management lock on the vault β€” noting that a lock prevents deletion, not replacement, which is the failure mode that actually applies to this resource.

🧱 Design Principles

Concern This module's default Opt-out
Retention Required β€” the provider gives no choice n/a
Schedule Required, 1–5 intervals, validated n/a
The at-least-one-of criteria rule Enforced offline, because the provider enforces it at apply n/a
weeks_of_month / months_of_year narrowing Reported through an output n/a β€” never refused
Enum sets Mirrored exactly from the provider's own source n/a
Empty selector list Sent as null, so omit and clear plan alike n/a
retention_rule keying A map keyed by rule name, so nothing re-indexes n/a
tags Not offered β€” the resource exposes none tag the vault instead
Secrets None accepted, none emitted n/a

ℹ️ Secure-by-default has nothing to act on here, and this module says so rather than implying otherwise. Every security-relevant argument on this resource is Required and none of them is permissive β€” there is no empty call to make safe and no risky value to make the caller type. The risk here is an unnoticed replacement, so the defaults are chosen to make the lifecycle visible.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module at a tag β€” ?ref=v1.0.0 β€” never a branch. This library is plan-only: a human applies from CI.

πŸ§ͺ Testing

Check Covered by Needs credentials?
HCL parses; types are consistent terraform validate No
Formatting terraform fmt -check No
This module's own validation {} blocks β€” the duration and interval shapes, the 1–5 schedule bound, the anchored vault ID, the four enum sets, the 142 time zones, the at-least-one-of criteria rule terraform plan on a configuration that calls the module No
The provider's own schema-level checks β€” the name pattern, the enum sets, the selector minimums terraform plan No
πŸ”΄ The provider's at-least-one-of criteria rule in its own right terraform apply β€” it lives in the create function Yes
Whether Azure accepts the schedule and the retention combination terraform apply Yes

⚠️ terraform validate run against a configuration that calls this module evaluates none of the module's variable values, so neither this module's validation {} blocks nor the provider's schema checks are reached β€” validate reports success. The refusal lands at terraform plan, which needs no credentials for these checks.

πŸ’¬ Example Output

$ terraform output

id                                    = "/subscriptions/.../resourceGroups/rg-backup-eastus/providers/Microsoft.DataProtection/backupVaults/bv-corp/backupPolicies/bp-adls-hybrid"
name                                  = "bp-adls-hybrid"
vault_name                            = "bv-corp"
default_retention_duration            = "P365D"
backup_schedule                       = ["R/2026-09-01T23:00:00+00:00/P1D"]
schedule_count                        = 1
retention_rule_names                  = ["keep-monthly"]
retention_rule_count                  = 1
rules_using_absolute_criteria         = ["keep-monthly"]
rules_using_days_of_week              = []
rules_narrowing_an_absolute_criteria  = []
time_zone                             = "UTC"
time_zone_was_left_to_the_service     = false
this_resource_backs_up_to_the_vault_only = true
this_policy_can_never_be_updated      = true
force_new_fields                      = [
  "name",
  "data_protection_backup_vault_id",
  "backup_schedule",
  "default_retention_duration",
  "retention_rule",
  "time_zone",
]

πŸ” Troubleshooting

Symptom Cause Fix
Plan shows the policy being replaced after a one-character change Every argument is force-new and there is no update function Expected. Create a new policy, repoint the instances, then remove the old one β€” see example 14.
A backup instance fails after a policy change The policy was replaced and the instance still referenced the old ID Sequence it: new policy, then the instance's backup_policy_data_lake_storage_id, then remove the old policy.
A retention rule copied from a blob storage policy will not parse That resource nests duration in life_cycle and the selectors in criteria, and requires priority Flatten it. This resource has none of those blocks or fields.
every retention_rule must set at least one of absolute_criteria and days_of_week A rule with neither selector Add one. The provider enforces the same rule, but only at apply.
backup_schedule must contain between 1 and 5 intervals Six or more intervals, or none Both bounds are the provider's own and invisible in the exported schema.
every backup_schedule entry must be an ISO 8601 REPEATING interval beginning "R/" A bare duration such as P1D put in the schedule Use R/<start instant>/P<period>.
time_zone must be one of the 142 Windows time zone identifiers An IANA name, a near-miss, or wrong casing Use e.g. UTC or Eastern Standard Time. The provider's set is closed and case-sensitive here, unlike on the blob storage policy.
A "not enough list items" error naming no field An empty days_of_week / months_of_year / weeks_of_month / scheduled_backup_times Omit the field rather than passing [].
name must be … and START WITH A LETTER A name beginning with a digit or hyphen Legal on the blob storage policy, refused here.
A rule appears to select nothing weeks_of_month or months_of_year narrows a selection, it does not make one Check rules_narrowing_an_absolute_criteria in the plan output.
Unsupported argument: update on timeouts The resource has no update function Remove the key. Three are supported: create, read, delete.
A create silently overwrote an existing policy The provider's skip-import-check feature is enabled in the caller's provider block That is provider configuration, not module configuration. See the_import_check_can_be_disabled_by_a_provider_feature.

πŸ”— Related Docs

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