Add Recovery Services Vault module with VM backup policies and outputs
This commit is contained in:
115
README.md
115
README.md
@@ -1,3 +1,116 @@
|
||||
# Azure Recovery Services Vault Terraform Module
|
||||
# Azure Recovery Services Vault Module
|
||||
|
||||
Creates a Recovery Services Vault and can optionally configure VM backup policies and VM protection.
|
||||
|
||||
## Usage scenarios
|
||||
|
||||
The recovery services vault may be used to protect the following Azure workloads:
|
||||
|
||||
- **Azure Virtual Machines**: Policy-based backup and restore for IaaS VMs.
|
||||
- **SQL Server in Azure VMs**: Workload-aware database backup for SQL running inside Azure VMs.
|
||||
- **SAP HANA in Azure VMs**: Workload-aware backup for SAP HANA databases running in Azure VMs.
|
||||
- **Azure Files**: Share-level backup and restore for Azure file shares.
|
||||
- **MARS agent workloads**: File/folder and system-state backup from supported Windows servers/clients.
|
||||
- **MABS / DPM-protected workloads**: Backup streams managed through Azure Backup Server or System Center DPM.
|
||||
|
||||
## Storage modes
|
||||
|
||||
`LocallyRedundant` stores backup data redundantly within a single region.
|
||||
|
||||
`ZoneRedundant` stores backup data across availability zones in the same region.
|
||||
|
||||
`GeoRedundant` replicates backup data to a paired region and enables cross-region restore when `cross_region_restore_enabled` is set to `true`.
|
||||
|
||||
## Protecting Resources
|
||||
|
||||
This module can protect Recovery Services Vault workloads. Supported resource types in module status are listed below.
|
||||
|
||||
Implemented:
|
||||
|
||||
- Azure Virtual Machines (`azurerm_backup_policy_vm`, `azurerm_backup_protected_vm`)
|
||||
|
||||
Not implemented yet:
|
||||
|
||||
- SQL Server in Azure VMs (`azurerm_backup_policy_vm_workload` + protected workload resources)
|
||||
- SAP HANA in Azure VMs (`azurerm_backup_policy_vm_workload` + protected workload resources)
|
||||
|
||||
### Azure Virtual Machines
|
||||
|
||||
Use `vm_backup_policies` to define one or more VM backup policy profiles, and `protected_vms` to map each VM to a selected policy via `backup_policy_key`.
|
||||
|
||||
For each protected VM, you can optionally set:
|
||||
|
||||
- `include_disk_luns` to include only selected data disks
|
||||
- `exclude_disk_luns` to exclude selected data disks
|
||||
- `protection_state` to control protection state (`Protected`, `BackupsSuspended`, `ProtectionStopped`)
|
||||
|
||||
## Module Inputs, Outputs, and Examples
|
||||
|
||||
### Variables
|
||||
|
||||
- `rg_name`: The name of the resource group where the Recovery Services Vault will be created.
|
||||
- `location`: The Azure region where the Recovery Services Vault will be created.
|
||||
- `base_name`: Optional base name used to generate a unique vault name when `name` is not set.
|
||||
- `name`: Optional explicit vault name. If omitted, the module generates a deterministic name from `base_name`.
|
||||
- `sku`: Vault SKU. Allowed values: `Standard`, `RS0`.
|
||||
- `storage_mode_type`: Backup storage redundancy type. Allowed values: `GeoRedundant`, `LocallyRedundant`, `ZoneRedundant`.
|
||||
- `cross_region_restore_enabled`: Enables cross-region restore. Can only be set to `true` when `storage_mode_type = "GeoRedundant"`.
|
||||
- `soft_delete_enabled`: Enables soft delete in the Recovery Services Vault.
|
||||
- `public_network_access_enabled`: Enables public network access to the vault.
|
||||
- `immutability`: Immutability state. Allowed values: `Disabled`, `Locked`, `Unlocked`.
|
||||
- `identity`: Optional managed identity configuration object:
|
||||
- `type`: Identity type. Allowed values: `SystemAssigned`, `UserAssigned`, `SystemAssigned, UserAssigned`.
|
||||
- `identity_ids`: Optional list of user-assigned identity IDs (required when `type` includes `UserAssigned`).
|
||||
- `tags`: A map of tags to apply to the vault.
|
||||
- `vm_backup_policies`: Map of VM backup policy definitions.
|
||||
- `protected_vms`: Map of VMs to protect, including policy mapping via `backup_policy_key`.
|
||||
|
||||
### Outputs
|
||||
|
||||
- `recovery_services_vault_id`: The ID of the created Recovery Services Vault.
|
||||
- `recovery_services_vault_name`: The name of the created Recovery Services Vault.
|
||||
- `recovery_services_vault_identity_principal_id`: Principal ID of the assigned managed identity, if configured.
|
||||
- `vm_backup_policy_ids`: Map of VM backup policy IDs keyed by policy key.
|
||||
- `protected_vm_backup_ids`: Map of protected VM backup item IDs keyed by protected VM key.
|
||||
|
||||
### Example Usage
|
||||
|
||||
```hcl
|
||||
module "recovery_services_vault" {
|
||||
source = "./modules/recovery-services-vault"
|
||||
|
||||
rg_name = azurerm_resource_group.rg.name
|
||||
location = azurerm_resource_group.rg.location
|
||||
|
||||
base_name = "rsv"
|
||||
|
||||
storage_mode_type = "LocallyRedundant"
|
||||
|
||||
vm_backup_policies = {
|
||||
default = {
|
||||
backup = {
|
||||
frequency = "Daily"
|
||||
time = "23:00"
|
||||
}
|
||||
retention_daily = {
|
||||
count = 30
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected_vms = {
|
||||
app = {
|
||||
source_vm_id = azurerm_linux_virtual_machine.app.id
|
||||
backup_policy_key = "default"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [Recovery Services vaults overview](https://learn.microsoft.com/azure/backup/backup-azure-recovery-services-vault-overview)
|
||||
- [Back up Azure VMs in a Recovery Services vault](https://learn.microsoft.com/azure/backup/backup-azure-arm-vms-prepare)
|
||||
- [Azure Backup FAQ: vault support matrix](https://learn.microsoft.com/azure/backup/backup-azure-backup-faq#what-are-the-various-vaults-supported-for-backup-and-restore)
|
||||
|
||||
|
||||
|
||||
146
main.tf
Normal file
146
main.tf
Normal file
@@ -0,0 +1,146 @@
|
||||
data "azurerm_client_config" "current" {}
|
||||
|
||||
locals {
|
||||
recovery_services_vault_name = (
|
||||
var.name != null &&
|
||||
trimspace(var.name) != "" ?
|
||||
var.name :
|
||||
"${coalesce(var.base_name, "")}${substr(md5("${data.azurerm_client_config.current.subscription_id}/${var.rg_name}/${coalesce(var.base_name, "")}"), 0, 6)}"
|
||||
)
|
||||
|
||||
default_vm_backup_policies = {
|
||||
default = {
|
||||
name = "${local.recovery_services_vault_name}-vm-policy"
|
||||
policy_type = "V2"
|
||||
timezone = "UTC"
|
||||
instant_restore_retention_days = 5
|
||||
backup = {
|
||||
frequency = "Daily"
|
||||
time = "23:00"
|
||||
}
|
||||
retention_daily = {
|
||||
count = 30
|
||||
}
|
||||
retention_weekly = null
|
||||
retention_monthly = null
|
||||
retention_yearly = null
|
||||
}
|
||||
}
|
||||
|
||||
effective_vm_backup_policies = length(var.vm_backup_policies) > 0 ? {
|
||||
for key, policy in var.vm_backup_policies : key => {
|
||||
name = coalesce(try(policy.name, null), "${local.recovery_services_vault_name}-${key}-vm-policy")
|
||||
policy_type = coalesce(try(policy.policy_type, null), "V2")
|
||||
timezone = coalesce(try(policy.timezone, null), "UTC")
|
||||
instant_restore_retention_days = try(policy.instant_restore_retention_days, null)
|
||||
backup = policy.backup
|
||||
retention_daily = try(policy.retention_daily, null)
|
||||
retention_weekly = try(policy.retention_weekly, null)
|
||||
retention_monthly = try(policy.retention_monthly, null)
|
||||
retention_yearly = try(policy.retention_yearly, null)
|
||||
}
|
||||
} : (length(var.protected_vms) > 0 ? local.default_vm_backup_policies : {})
|
||||
|
||||
default_vm_backup_policy_key = contains(keys(local.effective_vm_backup_policies), "default") ? "default" : (
|
||||
length(keys(local.effective_vm_backup_policies)) > 0 ? sort(keys(local.effective_vm_backup_policies))[0] : null
|
||||
)
|
||||
}
|
||||
|
||||
resource "azurerm_recovery_services_vault" "this" {
|
||||
name = local.recovery_services_vault_name
|
||||
location = var.location
|
||||
resource_group_name = var.rg_name
|
||||
|
||||
sku = var.sku
|
||||
storage_mode_type = var.storage_mode_type
|
||||
cross_region_restore_enabled = var.cross_region_restore_enabled
|
||||
soft_delete_enabled = var.soft_delete_enabled
|
||||
public_network_access_enabled = var.public_network_access_enabled
|
||||
immutability = var.immutability
|
||||
|
||||
dynamic "identity" {
|
||||
for_each = var.identity == null ? [] : [var.identity]
|
||||
|
||||
content {
|
||||
type = identity.value.type
|
||||
identity_ids = try(identity.value.identity_ids, null)
|
||||
}
|
||||
}
|
||||
|
||||
tags = var.tags
|
||||
}
|
||||
|
||||
resource "azurerm_backup_policy_vm" "this" {
|
||||
for_each = local.effective_vm_backup_policies
|
||||
|
||||
name = each.value.name
|
||||
resource_group_name = var.rg_name
|
||||
recovery_vault_name = azurerm_recovery_services_vault.this.name
|
||||
|
||||
policy_type = each.value.policy_type
|
||||
timezone = each.value.timezone
|
||||
instant_restore_retention_days = each.value.instant_restore_retention_days
|
||||
|
||||
backup {
|
||||
frequency = each.value.backup.frequency
|
||||
time = each.value.backup.time
|
||||
hour_interval = try(each.value.backup.hour_interval, null)
|
||||
hour_duration = try(each.value.backup.hour_duration, null)
|
||||
weekdays = try(each.value.backup.weekdays, null)
|
||||
}
|
||||
|
||||
dynamic "retention_daily" {
|
||||
for_each = try(each.value.retention_daily, null) == null ? [] : [each.value.retention_daily]
|
||||
|
||||
content {
|
||||
count = retention_daily.value.count
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "retention_weekly" {
|
||||
for_each = try(each.value.retention_weekly, null) == null ? [] : [each.value.retention_weekly]
|
||||
|
||||
content {
|
||||
count = retention_weekly.value.count
|
||||
weekdays = retention_weekly.value.weekdays
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "retention_monthly" {
|
||||
for_each = try(each.value.retention_monthly, null) == null ? [] : [each.value.retention_monthly]
|
||||
|
||||
content {
|
||||
count = retention_monthly.value.count
|
||||
weekdays = try(retention_monthly.value.weekdays, null)
|
||||
weeks = try(retention_monthly.value.weeks, null)
|
||||
days = try(retention_monthly.value.days, null)
|
||||
include_last_days = try(retention_monthly.value.include_last_days, null)
|
||||
}
|
||||
}
|
||||
|
||||
dynamic "retention_yearly" {
|
||||
for_each = try(each.value.retention_yearly, null) == null ? [] : [each.value.retention_yearly]
|
||||
|
||||
content {
|
||||
count = retention_yearly.value.count
|
||||
months = retention_yearly.value.months
|
||||
weekdays = try(retention_yearly.value.weekdays, null)
|
||||
weeks = try(retention_yearly.value.weeks, null)
|
||||
days = try(retention_yearly.value.days, null)
|
||||
include_last_days = try(retention_yearly.value.include_last_days, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "azurerm_backup_protected_vm" "this" {
|
||||
for_each = var.protected_vms
|
||||
|
||||
resource_group_name = var.rg_name
|
||||
recovery_vault_name = azurerm_recovery_services_vault.this.name
|
||||
source_vm_id = each.value.source_vm_id
|
||||
backup_policy_id = azurerm_backup_policy_vm.this[coalesce(try(each.value.backup_policy_key, null), local.default_vm_backup_policy_key)].id
|
||||
|
||||
include_disk_luns = try(each.value.include_disk_luns, null)
|
||||
exclude_disk_luns = try(each.value.exclude_disk_luns, null)
|
||||
protection_state = try(each.value.protection_state, null)
|
||||
}
|
||||
25
outputs.tf
Normal file
25
outputs.tf
Normal file
@@ -0,0 +1,25 @@
|
||||
output "recovery_services_vault_id" {
|
||||
value = azurerm_recovery_services_vault.this.id
|
||||
}
|
||||
|
||||
output "recovery_services_vault_name" {
|
||||
value = azurerm_recovery_services_vault.this.name
|
||||
}
|
||||
|
||||
output "recovery_services_vault_identity_principal_id" {
|
||||
value = try(azurerm_recovery_services_vault.this.identity[0].principal_id, null)
|
||||
}
|
||||
|
||||
output "vm_backup_policy_ids" {
|
||||
value = {
|
||||
for key, policy in azurerm_backup_policy_vm.this :
|
||||
key => policy.id
|
||||
}
|
||||
}
|
||||
|
||||
output "protected_vm_backup_ids" {
|
||||
value = {
|
||||
for key, item in azurerm_backup_protected_vm.this :
|
||||
key => item.id
|
||||
}
|
||||
}
|
||||
194
variables.tf
Normal file
194
variables.tf
Normal file
@@ -0,0 +1,194 @@
|
||||
variable "rg_name" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "location" {
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "base_name" {
|
||||
type = string
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "name" {
|
||||
type = string
|
||||
default = null
|
||||
|
||||
validation {
|
||||
condition = (
|
||||
(var.name != null && trimspace(var.name) != "") ||
|
||||
(var.base_name != null && trimspace(var.base_name) != "")
|
||||
)
|
||||
error_message = "Provide name or base_name with a non-empty value."
|
||||
}
|
||||
}
|
||||
|
||||
variable "sku" {
|
||||
type = string
|
||||
default = "Standard"
|
||||
|
||||
validation {
|
||||
condition = contains(["Standard", "RS0"], var.sku)
|
||||
error_message = "sku must be one of 'Standard' or 'RS0'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "storage_mode_type" {
|
||||
type = string
|
||||
default = "LocallyRedundant"
|
||||
|
||||
validation {
|
||||
condition = contains(["GeoRedundant", "LocallyRedundant", "ZoneRedundant"], var.storage_mode_type)
|
||||
error_message = "storage_mode_type must be one of 'GeoRedundant', 'LocallyRedundant', or 'ZoneRedundant'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "cross_region_restore_enabled" {
|
||||
type = bool
|
||||
default = false
|
||||
|
||||
validation {
|
||||
condition = var.cross_region_restore_enabled == false || var.storage_mode_type == "GeoRedundant"
|
||||
error_message = "cross_region_restore_enabled can only be true when storage_mode_type is 'GeoRedundant'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "soft_delete_enabled" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "public_network_access_enabled" {
|
||||
type = bool
|
||||
default = true
|
||||
}
|
||||
|
||||
variable "immutability" {
|
||||
type = string
|
||||
default = "Disabled"
|
||||
|
||||
validation {
|
||||
condition = contains(["Disabled", "Locked", "Unlocked"], var.immutability)
|
||||
error_message = "immutability must be one of 'Disabled', 'Locked', or 'Unlocked'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "identity" {
|
||||
type = object({
|
||||
type = string
|
||||
identity_ids = optional(list(string))
|
||||
})
|
||||
default = null
|
||||
|
||||
validation {
|
||||
condition = (
|
||||
var.identity == null ||
|
||||
contains([
|
||||
"SystemAssigned",
|
||||
"UserAssigned",
|
||||
"SystemAssigned, UserAssigned",
|
||||
], var.identity.type)
|
||||
)
|
||||
error_message = "identity.type must be one of 'SystemAssigned', 'UserAssigned', or 'SystemAssigned, UserAssigned'."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = (
|
||||
var.identity == null ||
|
||||
var.identity.type == "SystemAssigned" ||
|
||||
length(try(var.identity.identity_ids, [])) > 0
|
||||
)
|
||||
error_message = "identity.identity_ids must be provided when identity.type includes 'UserAssigned'."
|
||||
}
|
||||
}
|
||||
|
||||
variable "tags" {
|
||||
type = map(string)
|
||||
default = {}
|
||||
}
|
||||
|
||||
variable "vm_backup_policies" {
|
||||
type = map(object({
|
||||
name = optional(string)
|
||||
policy_type = optional(string)
|
||||
timezone = optional(string)
|
||||
instant_restore_retention_days = optional(number)
|
||||
|
||||
backup = object({
|
||||
frequency = string
|
||||
time = string
|
||||
hour_interval = optional(number)
|
||||
hour_duration = optional(number)
|
||||
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
|
||||
weekdays = optional(list(string))
|
||||
weeks = optional(list(string))
|
||||
days = optional(list(number))
|
||||
include_last_days = optional(bool)
|
||||
}))
|
||||
|
||||
retention_yearly = optional(object({
|
||||
count = number
|
||||
months = list(string)
|
||||
weekdays = optional(list(string))
|
||||
weeks = optional(list(string))
|
||||
days = optional(list(number))
|
||||
include_last_days = optional(bool)
|
||||
}))
|
||||
}))
|
||||
default = {}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for policy in values(var.vm_backup_policies) : contains(["V1", "V2"], coalesce(try(policy.policy_type, null), "V2"))
|
||||
])
|
||||
error_message = "Each vm_backup_policies[*].policy_type must be 'V1' or 'V2' when set."
|
||||
}
|
||||
}
|
||||
|
||||
variable "protected_vms" {
|
||||
type = map(object({
|
||||
source_vm_id = string
|
||||
backup_policy_key = optional(string)
|
||||
include_disk_luns = optional(list(number))
|
||||
exclude_disk_luns = optional(list(number))
|
||||
protection_state = optional(string)
|
||||
}))
|
||||
default = {}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for vm in values(var.protected_vms) : (
|
||||
try(vm.backup_policy_key, null) == null ||
|
||||
contains(
|
||||
keys(length(var.vm_backup_policies) > 0 ? var.vm_backup_policies : { default = {} }),
|
||||
vm.backup_policy_key
|
||||
)
|
||||
)
|
||||
])
|
||||
error_message = "Each protected_vms[*].backup_policy_key must exist in vm_backup_policies."
|
||||
}
|
||||
|
||||
validation {
|
||||
condition = alltrue([
|
||||
for vm in values(var.protected_vms) : (
|
||||
try(vm.protection_state, null) == null ||
|
||||
contains(["Protected", "BackupsSuspended", "ProtectionStopped"], vm.protection_state)
|
||||
)
|
||||
])
|
||||
error_message = "Each protected_vms[*].protection_state must be one of 'Protected', 'BackupsSuspended', or 'ProtectionStopped' when set."
|
||||
}
|
||||
}
|
||||
10
versions.tf
Normal file
10
versions.tf
Normal file
@@ -0,0 +1,10 @@
|
||||
terraform {
|
||||
required_providers {
|
||||
azurerm = {
|
||||
source = "hashicorp/azurerm"
|
||||
version = ">= 4.0.0, < 5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
required_version = ">= 1.0.0"
|
||||
}
|
||||
Reference in New Issue
Block a user