Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

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

Repository files navigation

☁️ Azure VM Workload Backup Policy Terraform Module

Defines an Azure Recovery Services VM workload backup policy β€” the schedule and multi-tier retention that a Recovery Services vault applies to SQL-in-a-VM and SAP HANA database workloads. Targets hashicorp/azurerm ~> 4.0.


Terraform azurerm Module Version Type Resources


🧩 Overview

  • πŸ—„οΈ Protects database workloads running inside a VM β€” Azure SQL Server (SQLDataBase) or SAP HANA (SAPHanaDatabase).
  • 🧱 Carries one or more protection sub-policies, one per policy_type (Full, Differential, Incremental, Log).
  • ⏱️ Expresses scheduled backups (Daily / Weekly) and log backups (frequency_in_minutes) in a single policy.
  • πŸ“† Layers multi-tier retention β€” daily, weekly, monthly, and yearly for full backups; simple retention for logs.
  • 🌐 Interprets every schedule time against a required time_zone, with optional backup compression.

πŸ’‘ Why it matters: A workload backup policy is the contract between a Recovery Services vault and the databases it protects. Getting the schedule and retention tiers right at authoring time β€” and validating the enum surface before any API call β€” keeps recovery-point objectives predictable and avoids apply-time rejections against the live vault.


❀️ Support this project

If this module saves you time, please consider supporting it:


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

graph TD
  me["terraform-azurerm-backup-policy-vm-workload"]
  target["azurerm_backup_policy_vm_workload"]
  vault["azurerm_recovery_services_vault"]
  item["protected workload item"]
  me -->|"defines"| target
  vault -->|"holds policy"| target
  target -->|"applied to"| item
  classDef me fill:#0078D4,color:#ffffff,stroke:#004578;
  classDef target fill:#004578,color:#ffffff,stroke:#002a44;
  classDef ext fill:#f2f2f2,color:#000000,stroke:#cccccc;
  class me me;
  class target target;
  class vault,item ext;
Loading

This module owns the policy definition only. The Recovery Services vault that holds the policy is created by a sibling module and referenced by name. The protected workload items (databases enrolled into backup) are wired by sibling backup-protection resources that reference this policy's id.


🧬 What this module builds

graph LR
  in_wl["workload_type"]
  in_set["settings (time_zone, compression)"]
  in_pp["protection_policy[] (backup + retention)"]
  res["azurerm_backup_policy_vm_workload.this"]
  out_id["id"]
  out_rpo["has_log_backups + log_backup_interval_minutes"]
  out_disc["retention and schedule discard reporters"]
  in_wl -->|"input"| res
  in_set -->|"input"| res
  in_pp -->|"input"| res
  res -->|"output"| out_id
  res -->|"output"| out_rpo
  res -->|"output"| out_disc
  classDef me fill:#0078D4,color:#ffffff,stroke:#004578;
  class res me;
Loading

Resource inventory

Resource Role Cardinality
azurerm_backup_policy_vm_workload.this The keystone workload backup policy single

The keystone renders one settings block and a dynamic "protection_policy" block over the protection_policy list; each sub-policy renders its own backup block and only the retention blocks it supplies (retention_daily, retention_weekly, retention_monthly, retention_yearly, simple_retention). An optional timeouts block is rendered when supplied.


βœ… Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module β€” the caller configures provider "azurerm", including the mandatory features {} block, plus auth.

Schema notes that bite

  • πŸ”’ name, resource_group_name, recovery_vault_name, and workload_type are force-new β€” changing any of them replaces the policy.
  • πŸ”΄ Each policy_type reads a specific set of retention blocks, and SILENTLY DISCARDS the rest. The long-term blocks β€” retention_daily, retention_weekly, retention_monthly, retention_yearly β€” are read only when policy_type is Full; simple_retention is read only when it is not. Supplying the wrong one is not rejected: it is accepted, stored in state, and dropped when the request is built, so recovery points expire on a schedule nobody chose. The retention_discarded_for_keys output reports exactly this.
  • πŸ”΄ The same silent discard applies inside backup. frequency_in_minutes is read only for a Log sub-policy; frequency, time and weekdays are read only for the others. A Log sub-policy given frequency = "Weekly" runs on its log interval and ignores the word. Reported by schedule_fields_discarded_for_keys.
  • πŸ”΄ Twelve cross-field rules are enforced by the provider only at apply, inside the function that builds the request β€” including retention_daily must be set when frequency is Daily (which applies to every policy_type, while the block is only used for Full), and Incremental is unsupported when workload_type is SQLDataBase. This module mirrors all twelve as validation {} blocks, so they fail at terraform validate instead: offline, with no credentials and no half-created policy.
  • ⏱️ backup.frequency is Daily or Weekly for scheduled backups; log-style sub-policies instead set backup.frequency_in_minutes, one of 15, 30, 60, 120, 240, 480, 720, 1440.
  • 🌐 settings.time_zone is required and governs how every schedule time is interpreted (e.g. "UTC").
  • πŸ—œοΈ settings.compression_enabled defaults to false when omitted.

πŸ”‘ Required Azure RBAC Roles / Permissions

Least-privilege at the Recovery Services vault scope:

  • Microsoft.RecoveryServices/vaults/backupPolicies/write
  • Microsoft.RecoveryServices/vaults/backupPolicies/read
  • Microsoft.RecoveryServices/vaults/backupPolicies/delete

The built-in Backup Contributor role on the vault covers these.


🧰 Azure Prerequisites

  • An existing Recovery Services vault.
  • The Microsoft.RecoveryServices resource provider registered on the subscription.
  • The caller's provider "azurerm" block configured with features {} and a valid authentication method.

πŸ“ Module Structure

terraform-azurerm-backup-policy-vm-workload/
β”œβ”€β”€ providers.tf     # terraform{} block: required_version + pinned azurerm (no provider block)
β”œβ”€β”€ variables.tf     # Typed inputs: name, vault refs, workload_type, settings, protection_policy, timeouts
β”œβ”€β”€ main.tf          # Keystone azurerm_backup_policy_vm_workload.this + dynamic protection_policy
β”œβ”€β”€ outputs.tf       # id first, then the regime (policy types, RPO, retention), the discard reporters, and the constant-fact outputs
β”œβ”€β”€ README.md        # This document
β”œβ”€β”€ SCOPE.md         # Cross-module contract
β”œβ”€β”€ LICENSE          # MIT
└── .gitignore       # Canonical library ignore set

βš™οΈ Quick Start

ℹ️ The caller configures the provider, authentication, and the mandatory features {} block. This module never declares a provider block.

provider "azurerm" {
  features {}
}

module "sql_vm_backup_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-daily-full"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = {
    time_zone = "UTC"
  }

  protection_policy = [
    {
      policy_type      = "Full"
      backup           = { frequency = "Daily", time = "23:00" }
      retention_daily  = { count = 30 }
    },
  ]
}

πŸ”Œ Cross-Module Contract

Consumes

Input Type Source module
resource_group_name string terraform-azurerm-resource-group
recovery_vault_name string terraform-azurerm-recovery-services-vault

Emits

Output Description Consumed by
id Resource ID of the workload backup policy backup-protection items that reference this policy
name Policy name references
recovery_vault_name / resource_group_name / workload_type Placement and workload prerequisite review
policy_types Which sub-policy types are defined change review
has_log_backups Whether point-in-time recovery exists recovery-capability review
log_backup_interval_minutes The effective RPO in minutes data-loss tolerance review
full_backup_schedule The Full sub-policy's frequency, time and weekdays change windows
longest_retention_days Longest retention, approximate days records-retention review
retention_discarded_for_keys Retention blocks the provider will DISCARD silent-no-op review
schedule_fields_discarded_for_keys Schedule fields the provider will DISCARD silent-no-op review
duplicate_policy_types Reported, not refused authoring review
compression_enabled / time_zone Settings in effect capacity and scheduling review
protects_nothing_until_assigned Constant: inert until a protected item references it composition design
changing_retention_applies_to_existing_recovery_points Constant: shortening retention EXPIRES stored points data-destruction gates
policy_cannot_be_destroyed_while_items_are_assigned Constant: destroy fails rather than unprotecting change planning
backups_survive_policy_deletion Constant: destroying this reclaims no storage cost expectations
this_module_verifies_no_prerequisite Constant: names are not checked against reality prerequisite review

πŸ“š Example Library

1 Β· Minimal SQL Full policy (daily retention)
module "sql_full_daily" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-full-daily"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "22:00" }
      retention_daily = { count = 30 }
    },
  ]
}

πŸ’‘ The smallest useful policy: a nightly full backup kept for 30 days.

2 Β· SAP HANA Full policy
module "hana_full_daily" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "hana-full-daily"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SAPHanaDatabase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "01:30" }
      retention_daily = { count = 14 }
    },
  ]
}

ℹ️ Only workload_type changes between a SQL and a SAP HANA policy shape; both accept the same protection-policy grammar.

3 Β· Weekly Full with weekly + monthly + yearly retention
module "sql_weekly_tiered" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-weekly-tiered"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type      = "Full"
      backup           = { frequency = "Weekly", time = "23:00", weekdays = ["Sunday"] }
      retention_weekly = { count = 12, weekdays = ["Sunday"] }
      retention_monthly = {
        count       = 12
        format_type = "Weekly"
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
      retention_yearly = {
        count       = 5
        format_type = "Weekly"
        months      = ["January"]
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
    },
  ]
}

πŸ’‘ A weekly full backup with three long-term retention tiers layered on top β€” a common regulated-data retention shape.

4 Β· Full + Log combined in one policy
module "sql_full_plus_log" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-full-log"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
    },
    {
      policy_type      = "Log"
      backup           = { frequency_in_minutes = 120 }
      simple_retention = { count = 15 }
    },
  ]
}

πŸ’‘ Full backups anchor the recovery points; log backups every 120 minutes tighten the recovery-point objective for point-in-time restore.

5 Β· Differential sub-policy
module "sql_differential" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-full-diff"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Weekly", time = "23:00", weekdays = ["Sunday"] }
      retention_weekly = { count = 8, weekdays = ["Sunday"] }
    },
    {
      policy_type      = "Differential"
      backup           = { frequency = "Weekly", time = "22:00", weekdays = ["Wednesday"] }
      simple_retention = { count = 14 }
    },
  ]
}

ℹ️ A differential sub-policy pairs a mid-week backup with a simple_retention window between full backups.

6 Β· Incremental sub-policy (SAP HANA)
module "hana_incremental" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "hana-full-incr"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SAPHanaDatabase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Weekly", time = "01:00", weekdays = ["Saturday"] }
      retention_weekly = { count = 6, weekdays = ["Saturday"] }
    },
    {
      policy_type      = "Incremental"
      backup           = { frequency = "Daily", time = "01:00" }
      retention_daily  = { count = 7 } # required by the Daily rule β€” and then discarded
      simple_retention = { count = 7 } # this is the retention that actually applies
    },
  ]
}

πŸ’‘ Incremental backups reduce data movement between full backups; here they run nightly with a one-week simple retention. Incremental is available for SAPHanaDatabase only β€” the provider rejects it outright when workload_type is SQLDataBase, and this module refuses that combination at terraform validate.

πŸ”΄ Note the retention_daily block that does nothing. The provider requires retention_daily whenever backup.frequency is Daily, for every policy_type β€” but it only reads the long-term retention blocks when policy_type is Full. So on this Incremental sub-policy the block is mandatory and then thrown away, and simple_retention is what actually governs expiry. Omitting it fails at apply with "retention_daily must be set when backup.0.frequency is Daily"; the retention_discarded_for_keys output reports the discard.

7 Β· Log backups every 15 minutes
module "sql_tight_rpo" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-tight-rpo"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:30" }
      retention_daily = { count = 35 }
    },
    {
      policy_type      = "Log"
      backup           = { frequency_in_minutes = 15 }
      simple_retention = { count = 30 }
    },
  ]
}

⚠️ frequency_in_minutes must be one of 15, 30, 60, 120, 240, 480, 720, 1440. Any other value fails validation at parse time.

8 Β· Backup compression enabled
module "sql_compressed" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-compressed"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = {
    time_zone           = "UTC"
    compression_enabled = true
  }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
    },
  ]
}

πŸ”’ compression_enabled defaults to false; set it to true explicitly to opt in to backup compression.

9 Β· Custom time zone
module "sql_pacific" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-pacific"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-westus2"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "Pacific Standard Time" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "02:00" }
      retention_daily = { count = 30 }
    },
  ]
}

ℹ️ Every schedule time is interpreted in settings.time_zone. A 02:00 full backup here runs at 02:00 Pacific, not UTC.

10 Β· Yearly retention, format_type Weekly
module "sql_yearly_weekly" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-yearly-weekly"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type      = "Full"
      backup           = { frequency = "Weekly", time = "23:00", weekdays = ["Sunday"] }
      retention_weekly = { count = 12, weekdays = ["Sunday"] }
      retention_yearly = {
        count       = 7
        format_type = "Weekly"
        months      = ["January"]
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
    },
  ]
}

πŸ’‘ With format_type = "Weekly", yearly retention pins to a weeks + weekdays combination (e.g. the first Sunday of January).

11 Β· Yearly retention, format_type Daily
module "sql_yearly_daily" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-yearly-daily"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
      retention_yearly = {
        count       = 10
        format_type = "Daily"
        months      = ["January"]
        monthdays   = [1]
      }
    },
  ]
}

ℹ️ With format_type = "Daily", yearly retention pins to specific monthdays (e.g. January 1) rather than a weeks/weekdays pattern.

12 Β· Monthly retention, format_type Daily
module "sql_monthly_daily" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-monthly-daily"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
      retention_monthly = {
        count       = 12
        format_type = "Daily"
        monthdays   = [1, 15]
      }
    },
  ]
}

πŸ’‘ A Daily monthly format retains backups from the 1st and 15th of each month for a year.

13 Β· Full four-tier retention (daily + weekly + monthly + yearly)
module "sql_all_tiers" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-all-tiers"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
      retention_weekly = {
        count    = 12
        weekdays = ["Sunday"]
      }
      retention_monthly = {
        count       = 12
        format_type = "Weekly"
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
      retention_yearly = {
        count       = 7
        format_type = "Weekly"
        months      = ["January"]
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
    },
  ]
}

πŸ”’ The full grandfather-father-son shape in one sub-policy: 30 dailies, 12 weeklies, 12 monthlies, and 7 yearlies.

14 Β· Custom timeouts
module "sql_with_timeouts" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-timeouts"
  resource_group_name = "rg-backup"
  recovery_vault_name  = "rsv-prod-eastus"
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type     = "Full"
      backup          = { frequency = "Daily", time = "23:00" }
      retention_daily = { count = 30 }
    },
  ]

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

ℹ️ timeouts accepts Go duration strings. Leave it null (the default) for provider defaults.

15 Β· πŸ—οΈ End-to-end composition (resource group + vault β†’ policy)
provider "azurerm" {
  features {}
}

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

  name     = "rg-backup-prod"
  location = "eastus"
}

module "recovery_vault" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-recovery-services-vault.git?ref=v1.0.0"

  name                = "rsv-prod-eastus"
  resource_group_name = module.backup_rg.name
  location            = "eastus"
}

module "sql_vm_backup_policy" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-backup-policy-vm-workload.git?ref=v1.0.0"

  name                = "sqlvm-prod-policy"
  resource_group_name = module.backup_rg.name
  recovery_vault_name  = module.recovery_vault.name
  workload_type        = "SQLDataBase"

  settings = { time_zone = "UTC" }

  protection_policy = [
    {
      policy_type      = "Full"
      backup           = { frequency = "Weekly", time = "23:00", weekdays = ["Sunday"] }
      retention_weekly = { count = 12, weekdays = ["Sunday"] }
      retention_monthly = {
        count       = 12
        format_type = "Weekly"
        weeks       = ["First"]
        weekdays    = ["Sunday"]
      }
    },
    {
      policy_type      = "Log"
      backup           = { frequency_in_minutes = 60 }
      simple_retention = { count = 15 }
    },
  ]
}

output "backup_policy_id" {
  value = module.sql_vm_backup_policy.id
}

πŸ’‘ The resource-group and recovery-services-vault modules feed their name outputs straight into this policy's resource_group_name and recovery_vault_name inputs, keeping ownership boundaries clean.


πŸ“₯ Inputs

Required

Name Type Description
name string Policy name. Force-new.
resource_group_name string Resource group holding the vault. Force-new.
recovery_vault_name string Recovery Services vault name. Force-new.
workload_type string SQLDataBase or SAPHanaDatabase. Force-new.
settings object Policy-wide settings (time_zone required).
protection_policy list(object) One or more protection sub-policies.

Optional

Name Type Default Description
timeouts object null Per-operation timeouts (Go duration strings).
Full object() schemas
variable "settings" {
  type = object({
    time_zone           = string
    compression_enabled = optional(bool) # defaults to false in main.tf
  })
}

variable "protection_policy" {
  type = list(object({
    policy_type = string # Full | Differential | Incremental | Log
    backup = object({
      frequency            = optional(string) # Daily | Weekly
      frequency_in_minutes = optional(number) # 15|30|60|120|240|480|720|1440
      time                 = optional(string)
      weekdays             = optional(list(string))
    })
    retention_daily = optional(object({
      count = number
    }))
    retention_weekly = optional(object({
      count    = number
      weekdays = list(string)
    }))
    retention_monthly = optional(object({
      count       = number
      format_type = string
      monthdays   = optional(list(number))
      weekdays    = optional(list(string))
      weeks       = optional(list(string))
    }))
    retention_yearly = optional(object({
      count       = number
      format_type = string
      months      = list(string)
      monthdays   = optional(list(number))
      weekdays    = optional(list(string))
      weeks       = optional(list(string))
    }))
    simple_retention = optional(object({
      count = number
    }))
  }))
  # Validations: >= 1 sub-policy; policy_type in {Full,Differential,Incremental,Log};
  # backup.frequency in {Daily,Weekly}; frequency_in_minutes in {15,30,60,120,240,480,720,1440}.
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default = null
}

🧾 Outputs

Output Description Notes
id Resource ID of the VM workload backup policy Emitted first
name Policy name β€”
recovery_vault_name The vault holding the policy Echoed - the provider exposes no vault attribute
resource_group_name The vault's group Not the group of what is backed up
workload_type SQLDataBase or SAPHanaDatabase Constrains which sub-policy types are legal
policy_types Sorted sub-policy types Sorted so reordering shows no change
has_log_backups A Log sub-policy exists πŸ”΄ False means no point-in-time recovery between full backups
log_backup_interval_minutes The Log interval, or null πŸ”΄ This is the effective RPO. A closed set, not a range
full_backup_schedule Frequency, time, weekdays of the Full sub-policy null if no Full sub-policy β€” worth a second look
longest_retention_days Longest retention, approximate days ⚠️ Approximate on purpose; not a compliance figure
retention_discarded_for_keys Retention that will be discarded πŸ”΄ Long-term retention is read only for Full; simple_retention only for the rest
schedule_fields_discarded_for_keys Schedule fields that will be discarded πŸ”΄ frequency_in_minutes only for Log; frequency/time/weekdays only for the rest
duplicate_policy_types A policy_type declared twice ⚠️ Reported, not refused β€” identical entries collapse, differing ones both apply
compression_enabled SQL backup compression Cost lands on the protected server, not the vault
time_zone The schedule's time zone Windows name (Pacific Standard Time), not IANA
protects_nothing_until_assigned Always true Inert until a protected item references id
changing_retention_applies_to_existing_recovery_points Always true πŸ”΄ Shortening retention expires recovery points already stored
policy_cannot_be_destroyed_while_items_are_assigned Always true Destroy fails rather than silently unprotecting β€” including on a rename
backups_survive_policy_deletion Always true Destroying this reclaims no storage and is not a cost control
this_module_verifies_no_prerequisite Always true Vault name, resource group and time zone are unchecked strings

🧠 Architecture Notes

  • Force-new identity. name, resource_group_name, recovery_vault_name, and workload_type all force replacement. Rename or re-target a policy only with the understanding that the old policy is destroyed and a new one created β€” detach protected items first.
  • Retention grammar is per policy_type. The keystone renders each retention block only when the sub-policy supplies it (dynamic blocks guarded with try(..., null) != null). A Full sub-policy typically carries retention_daily / retention_weekly / retention_monthly / retention_yearly; a Log sub-policy carries simple_retention. Attaching a retention block a policy_type does not accept passes the type checker but is rejected at apply by the provider.
  • frequency vs frequency_in_minutes. Scheduled full/differential/incremental backups set backup.frequency (Daily / Weekly) with a time and, for weekly, weekdays. Log backups instead set backup.frequency_in_minutes from the fixed enum. Both fields are optional in the type so each sub-policy uses only the one it needs.
  • time_zone semantics. settings.time_zone is required and governs interpretation of every schedule time across all sub-policies. Choosing a local zone (for example Pacific Standard Time) means the printed time is local, not UTC.
  • compression_enabled defaults off. When omitted from settings, main.tf renders compression_enabled = false.
  • features {} dependence. The module declares no provider block. Initialization depends entirely on the caller's provider "azurerm" { features {} }; a missing features {} block in the root module is the usual cause of an init failure in isolation.

🧱 Design Principles

  • Fail at parse time, not apply time. The value sets for workload_type (SQLDataBase / SAPHanaDatabase), policy_type (Full / Differential / Incremental / Log), backup.frequency (Daily / Weekly), and backup.frequency_in_minutes (15 … 1440) are enforced with validation {} blocks, so a malformed policy is rejected before any Azure API call.
  • At least one sub-policy. protection_policy requires a minimum of one entry.
  • Compression is opt-in. compression_enabled defaults to false; the caller types true to enable it.
  • No tags. This resource type does not support tags, so the universal tail is timeouts only.

πŸš€ Runbook

terraform init -backend=false
terraform validate
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 β€” never a branch.
  • This is plan-only in authoring: terraform apply is run by a human from CI, never as part of module development.

πŸ§ͺ Testing

The offline proof gate exercises everything that does not require a live subscription:

Check Command What it proves
Type + schema terraform validate The object() schemas, optional(...) defaults, and every validation {} (enums, minimum sub-policy count) hold.
Formatting terraform fmt -check Canonical HCL formatting.
Init (no backend) terraform init -backend=false Provider constraints resolve against ~> 4.0.

Only terraform plan / apply against a real vault exercises the provider-side rule that a given policy_type accepts only certain retention blocks β€” that surface is validated in CI, not at author time.


πŸ’¬ Example Output

$ terraform output
id   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-backup/providers/Microsoft.RecoveryServices/vaults/rsv-prod-eastus/backupPolicies/sqlvm-prod-policy"
name = "sqlvm-prod-policy"

πŸ” Troubleshooting

Symptom Cause Fix
Apply rejects a retention block A retention block was supplied that the policy_type does not accept (e.g. simple_retention on a Full policy, or retention_monthly on a Log policy). Match retention blocks to the policy_type: full-style retention on Full; simple_retention on Log.
frequency_in_minutes must be one of... at plan backup.frequency_in_minutes set to a value outside the enum. Use one of 15, 30, 60, 120, 240, 480, 720, 1440.
time_zone error / times off by hours settings.time_zone omitted or set to an unexpected zone. Set settings.time_zone explicitly (e.g. "UTC"); confirm each time is meant in that zone.
workload_type must be... at plan workload_type not one of the two legal values. Use SQLDataBase or SAPHanaDatabase.
Plan wants to replace the policy A force-new field (name, resource_group_name, recovery_vault_name, workload_type) changed. Revert the change, or accept replacement and detach protected items first.
Provider fails to initialize in isolation The caller's root module is missing provider "azurerm" { features {} }. Add the features {} block to the root module.
At least one protection_policy is required. protection_policy passed as an empty list. Supply at least one sub-policy.

πŸ”— Related Docs


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