Manages one
azurerm_data_protection_backup_policy_postgresql_flexible_serverβ the schedule and retention contract a PostgreSQL backup instance points at. Targetshashicorp/azurerm ~> 4.0.
- π― Creates one PostgreSQL Flexible Server backup policy inside a Backup Vault.
- π§ Every argument is force-new and there is no update function β nothing here can be edited in place.
- π But the repoint path exists: the instance's
backup_policy_idupdates in place, so a retention change costs no data. - π‘οΈ The schedule and time zone are checked for non-emptiness only β this module's shape checks are the real protection.
- π« Carries no
tagsβ the resource exposes none.
π‘ Why it matters: on this service the provider checks that the schedule strings and the time zone are non-empty and nothing more. There is no interval parser and no time-zone validator file at all, where the MySQL Flexible Server policy parses its schedule and checks a closed set of 141 identifiers, and Data Lake Storage parses, caps at five and checks 142. So the shape checks in this module are its own work, not a mirror of the provider's β and a habit carried from either sibling will be wrong about what is caught. The criteria fields, by contrast, are properly validated.
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"]
SRV["azurerm_postgresql_flexible_server"]
MI["the vault managed identity"]
ROLE["role assignment for the vault identity"]
PRE["further prerequisites the provider documents and does not check"]
POL["backup_policy_postgresql_flexible_server"]
INS["backup_instance_postgresql_flexible_server"]
RG -->|"resource group"| VAULT
VAULT -->|"vault_id, FORCE-NEW"| POL
VAULT -->|"vault_id, FORCE-NEW"| INS
POL -->|"backup_policy_id, updates IN PLACE"| INS
SRV -->|"server_id, FORCE-NEW"| INS
SRV -->|"location, the source database region"| INS
VAULT -->|"managed identity"| MI
MI -->|"principal_id"| ROLE
ROLE -.->|"on the server, BEFORE the instance"| INS
PRE -.->|"unmet means the poll times out"| INS
INS -->|"data copied INTO the vault"| VAULT
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,SRV,MI,ROLE,PRE ext
The edge from the policy to the instance is the one that updates in place β which is what makes the create-new-then-repoint sequence work on this service, and what distinguishes it from the Kubernetes cluster service, where the same edge is force-new. The dashed edges are requirements Terraform cannot see.
flowchart TB
VIN["name, vault_id"]
VSCH["backup_repeating_time_intervals, REQUIRED -- provider checks NON-EMPTY ONLY"]
VDEF["default_retention_rule, a REQUIRED BLOCK"]
VRULE["retention_rule map -- criteria ARE properly validated"]
VTZ["time_zone -- NO closed set on this service"]
STORE["data_store_type is VaultStore, NEVER OperationalStore"]
ALL["EVERY argument is FORCE-NEW, and there is NO update function"]
R["azurerm_data_protection_backup_policy_postgresql_flexible_server.this"]
OID["id -- the value a backup instance references"]
OREPOINT["the_repoint_path_exists_on_this_service"]
OSCHED["the_provider_only_checks_the_schedule_is_non_empty"]
OTZ["the_time_zone_is_not_validated_against_a_closed_set_here"]
VIN --> ALL
VSCH --> ALL
VDEF --> STORE
VRULE --> STORE
VTZ --> ALL
STORE --> ALL
ALL --> R
R --> OID
R --> OREPOINT
R --> OSCHED
R --> OTZ
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,STORE,OSCHED,OTZ me
class VIN,VSCH,VDEF,VRULE,VTZ,OID,OREPOINT ext
| Resource | Count | Role |
|---|---|---|
azurerm_data_protection_backup_policy_postgresql_flexible_server |
1 (this) |
The keystone, and the only resource here. |
| 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:
- π΄ The schedule is validated for NON-EMPTINESS ONLY. No interval parser on this service, so a malformed repeating interval is accepted by Terraform and rejected by Azure. The MySQL Flexible Server and Data Lake Storage policies do parse theirs.
- π΄
time_zoneis validated for NON-EMPTINESS ONLY, and no time-zone validator exists for this service at all. MySQL checks a closed set of 141; Data Lake Storage 142. - β But the criteria fields ARE properly validated β closed case-sensitive sets for the absolute criteria, weekdays, months and week numbers, plus a real RFC 3339 parser. The weakness is confined to the schedule and the time zone.
- π΄ There is no update function and every argument is force-new, hence the three-key
timeouts. The backup instance for this service does have an update function and takes four. - β
The repoint path exists. The instance's
backup_policy_idupdates in place β unlike on the Kubernetes cluster service. - π΄
data_store_typeisVaultStorehere andOperationalStoreon the Kubernetes cluster backup policy. Both are closed, case-sensitive, one-member checks carrying a source comment about a future value, so the blocks look identical and accept opposite values. - π΄
default_retention_ruleis a required BLOCK, as on the MySQL and Kubernetes policies. The blob storage and Data Lake Storage policies use plain duration strings. life_cyclehas no maximum, so several entries are permitted; the cap of one sits on the nestedcriteriablock.- The schedule has a minimum of one entry and no maximum, where Data Lake Storage caps at five. Neither bound appears in the exported schema.
priorityis required on every rule and its documented uniqueness rule is enforced by nothing.- There is no
days_of_monthβ month-end goes throughweeks_of_month = ["Last"]with a day selector. - Four selector lists carry a minimum of one item, invisible in the exported schema.
- The requires-import guard can be disabled by a provider feature.
- 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 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 server and no identity. The permissions that decide whether backup actually works belong to the instance β and PostgreSQL Flexible Server backup has further prerequisites the provider documents and does not check.
- The
Microsoft.DataProtectionresource 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 β though the instances can be repointed rather than replaced.
- The caller's
provider "azurerm" { features {} }block. The module declares none.
terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server/
βββ providers.tf # required_version + the azurerm ~> 4.0 pin. No provider block.
βββ variables.tf # 7 variables, 23 validations, the shape checks the provider omits
βββ main.tf # one keystone `this`, a required default_retention_rule + keyed retention rules
βββ outputs.tf # 45 outputs; id first, then name, then the facts
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
module "postgresql_backup_policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-daily-30d"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{
# VaultStore, NOT OperationalStore.
data_store_type = "VaultStore"
duration = "P30D"
}]
}
}π The caller configures the provider, its authentication and its
features {}block. This module declares none of them.
Consumes
| Input | Type | Source |
|---|---|---|
vault_id |
Resource ID | terraform-azurerm-data-protection-backup-vault β id |
name |
string, required |
caller |
backup_repeating_time_intervals |
list(string), required |
caller |
default_retention_rule |
object, required |
caller |
retention_rule, time_zone, timeouts |
see Inputs | caller |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
The policy's Resource ID | β¦-backup-instance-postgresql-flexible-server β backup_policy_id |
the_provider_only_checks_the_schedule_is_non_empty |
What the provider does not do | review |
the_repoint_path_exists_on_this_service |
Retention changes cost no data here | change review |
force_new_fields |
All six arguments | change review |
1 Β· The smallest real policy
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-daily-30d"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{
data_store_type = "VaultStore"
duration = "P30D"
}]
}
}βΉοΈ There is no shorter call. The schedule and the default retention rule are both Required, and the retention rule is a nested block rather than a duration string.
2 Β· The schedule the provider does not parse
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-schedule"
vault_id = module.backup_vault.id
# The provider checks only that each entry is a NON-EMPTY STRING on this
# service. "not-an-interval" would pass its check and be rejected by Azure.
# The checks that catch it are THIS MODULE'S.
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
}π΄ Do not carry a habit from the MySQL module here. That resource parses each entry as a repeating time interval, so a malformed schedule is refused by the provider itself. This one does not. See
the_provider_only_checks_the_schedule_is_non_empty.
3 Β· Five services, five answers on the same argument
# backup_repeating_time_intervals, across this family:
#
# blob storage no validator at all
# Data Lake Storage parsed as an interval, AND capped at 5 entries
# Kubernetes cluster no validator at all
# MySQL Flexible parsed as an interval, uncapped
# PostgreSQL Flexible non-empty check only, uncapped <-- THIS RESOURCE
#
# The protection you get is per-service, not per-family.
output "taxonomy" {
value = module.policy.schedule_validation_differs_across_this_family
}
β οΈ This is the kind of difference that survives review, because the argument has the same name and the same meaning on all five. Only the enforcement differs.
4 Β· time_zone β no closed set on this service
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-eastern"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00-05:00/P1D"]
# A WINDOWS identifier. The provider checks ONLY that this is non-empty on
# this service -- there is no time-zone validator for it at all -- so a
# misspelling reaches Azure and means nothing there.
time_zone = "Eastern Standard Time"
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
}π΄ The MySQL policy validates the same field against 141 identifiers and Data Lake Storage against 142. Neither set applies here. This module refuses only the detectable mistake β a value containing
/, the shape of an IANA name β because the legal set cannot be enumerated from this resource without risking a legal value being refused. Seethe_time_zone_is_not_validated_against_a_closed_set_here.
βΉοΈ Note the interaction: the start instants in
backup_repeating_time_intervalsalready carry their own UTC offset.
5 Β· The criteria ARE validated β the weakness is not uniform
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-criteria"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"weekly" = {
priority = 20
life_cycle = [{ data_store_type = "VaultStore", duration = "P12W" }]
criteria = {
# These ARE checked by the provider: closed case-sensitive sets, and a
# real RFC 3339 parser for scheduled_backup_times. "firstofweek" or
# "Sundays" is refused by the provider itself.
absolute_criteria = "FirstOfWeek"
}
}
}
}
output "not_uniformly_weak" {
value = module.policy.the_criteria_are_validated_even_though_the_schedule_is_not
}π‘ Worth knowing so the caveat is not over-generalised. "This resource validates weakly" would be too broad: only the schedule and the time zone are left to a bare non-empty check.
6 Β· VaultStore, never OperationalStore
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-store-type"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{
# The ONLY legal value on this resource.
data_store_type = "VaultStore"
# REFUSED here -- and it is the only legal value on the KUBERNETES CLUSTER
# backup policy:
# data_store_type = "OperationalStore"
duration = "P30D"
}]
}
}π΄ Both providers apply a closed, case-sensitive check containing exactly one member, and both carry a source comment saying another value may be supported in future β so the blocks look identical and accept opposite values.
βΉοΈ PostgreSQL Flexible Server backup is vaulted: the data is copied into the Backup Vault and survives loss of the server. That is why the value is what it is, and why nothing on this service names a snapshot resource group.
7 Β· The default retention rule is a block, not a string
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-block-shape"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
# A BLOCK, as on the MySQL and Kubernetes cluster policies. The blob storage
# and Data Lake Storage policies take plain duration STRINGS instead.
default_retention_rule = {
life_cycle = [{
data_store_type = "VaultStore"
duration = "P30D"
}]
}
}βΉοΈ
life_cycleis a list with no maximum on this resource, so several entries are permitted. The cap of one sits on the nestedcriteriablock instead.
8 Β· A retention rule, with its required priority
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-weekly"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"keep-weekly" = {
priority = 20
life_cycle = [{ data_store_type = "VaultStore", duration = "P12W" }]
criteria = { absolute_criteria = "FirstOfWeek" }
}
}
}π‘ Lower priority wins, and the numbers must be unique. The provider documents that and checks nothing, so this module enforces it β two rules sharing a priority have no defined order. Note the Data Lake Storage policy has no
priorityfield at all.
9 Β· Several rules at different retentions
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-tiered"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P7D" }]
}
retention_rule = {
"keep-weekly" = {
priority = 20
life_cycle = [{ data_store_type = "VaultStore", duration = "P12W" }]
criteria = { absolute_criteria = "FirstOfWeek" }
}
"keep-monthly" = {
priority = 15
life_cycle = [{ data_store_type = "VaultStore", duration = "P12M" }]
criteria = { absolute_criteria = "FirstOfMonth" }
}
"keep-yearly" = {
priority = 10
life_cycle = [{ data_store_type = "VaultStore", duration = "P7Y" }]
criteria = { absolute_criteria = "FirstOfYear" }
}
}
}
output "rule_names" {
value = module.policy.retention_rule_names
}βΉοΈ Keying by rule name keeps the map stable. Adding a fourth rule never re-indexes the other three, which matters here more than usual: every argument is force-new.
10 Β· Selecting by weekday
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-sundays"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"sundays" = {
priority = 25
life_cycle = [{ data_store_type = "VaultStore", duration = "P8W" }]
criteria = { days_of_week = ["Sunday"] }
}
}
}βΉοΈ All seven weekdays are legal, Wednesday included. The provider's documentation for the blob storage and Kubernetes cluster policies omits Wednesday from its equivalent list, so a reader carrying that list across would refuse a legal value.
all_seven_weekdays_are_legalemits the real set, taken from the provider's SDK constants.
11 Β· Month-end, without a days_of_month field
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-month-end"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"month-end" = {
priority = 18
life_cycle = [{ data_store_type = "VaultStore", duration = "P36M" }]
criteria = {
# No days_of_month on this resource -- the LAST week plus a day selector.
days_of_week = ["Sunday"]
weeks_of_month = ["Last"]
}
}
}
}βΉοΈ The blob storage backup policy encodes month-end as
days_of_month = [0]. That idiom does not transfer. Seethere_is_no_days_of_month_on_this_resource.
12 Β· Narrowing, and the empty-list trap
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-quarterly"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"quarterly-first-sunday" = {
priority = 12
life_cycle = [{ data_store_type = "VaultStore", duration = "P5Y" }]
criteria = {
days_of_week = ["Sunday"]
weeks_of_month = ["First"]
months_of_year = ["January", "April", "July", "October"]
# Do NOT write `scheduled_backup_times = []`. Omit the field instead --
# all four selector lists carry an invisible minimum of one item.
}
}
}
}
β οΈ weeks_of_monthandmonths_of_yearNARROW a selection; they do not make one.rules_narrowing_an_absolute_criterianames rules that set either while selecting only byabsolute_criteria;rules_with_no_selectornames rules that select nothing at all. Both reported, never refused β nothing in the provider requires a selector.
13 Β· Three timeout keys, not four
module "policy" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-timeouts"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
timeouts = {
create = "30m"
read = "5m"
delete = "30m"
}
}π There is no
updatekey, and that is correct. The resource has no update function, so Terraform rejects the key outright rather than accepting and ignoring it β worth stating because object-type conversion normally silently discards an undeclared key. The backup instance for this service does have an update function and takes four keys.
14 Β· Changing retention β repoint, do not rebuild
# The policy cannot be edited: every argument is force-new and there is no update
# function. But the INSTANCE's backup_policy_id updates IN PLACE, so:
#
# 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
#
# That route does NOT exist on the Kubernetes cluster service.
module "policy_v2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-policy-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-365d"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P365D" }]
}
}
module "instance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-instance-postgresql-flexible-server.git?ref=v1.0.0"
name = "bi-pgsql-prod"
location = module.postgresql.location
vault_id = module.backup_vault.id
server_id = module.postgresql.id
# The in-place update.
backup_policy_id = module.policy_v2.id
}
output "repoint_available" {
value = module.policy_v2.the_repoint_path_exists_on_this_service
}π‘ Sequence it. Create the new policy, repoint the instances, then remove the old one β nothing in Terraform records which instances reference a policy, because the reference is held on the instance.
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 "postgresql" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-postgresql-flexible-server.git?ref=v1.0.0"
name = "pgsql-prod-eastus"
resource_group_name = module.rg.name
location = module.rg.location
}
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-postgresql-flexible-server.git?ref=v1.0.0"
name = "bp-pgsql-daily"
vault_id = module.backup_vault.id
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
time_zone = "UTC"
default_retention_rule = {
life_cycle = [{ data_store_type = "VaultStore", duration = "P30D" }]
}
retention_rule = {
"keep-monthly" = {
priority = 15
life_cycle = [{ data_store_type = "VaultStore", duration = "P12M" }]
criteria = { absolute_criteria = "FirstOfMonth" }
}
}
}
# The grant the INSTANCE needs, on the SERVER, held by the VAULT's identity.
# Sequence it ahead of the instance: the create blocks on a protection poll.
module "vault_grant" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.postgresql.id
role_assignments = {
"vault-backup" = {
role_definition_name = "Reader"
principal_id = module.backup_vault.identity_principal_id
}
}
}
module "instance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-data-protection-backup-instance-postgresql-flexible-server.git?ref=v1.0.0"
name = "bi-pgsql-prod"
vault_id = module.backup_vault.id
# The SERVER's own region and ID.
location = module.postgresql.location
server_id = module.postgresql.id
backup_policy_id = module.policy.id
timeouts = {
create = "90m"
}
depends_on = [module.vault_grant]
}
output "policy_id" {
value = module.policy.id
}
output "protection_state" {
value = module.instance.protection_state
}
output "prerequisites_nothing_checks" {
value = module.instance.this_service_has_prerequisites_the_provider_documents_and_does_not_check
}π΄
depends_onis load-bearing. Nothing in the instance's arguments references the role assignment, so without it Terraform may create the instance first β and because that resource polls, the apply then blocks for the full budget waiting for access it does not have.
β οΈ The service has further prerequisites the provider documents and Terraform cannot verify. Checkprotection_stateafter the first apply; that is where an unmet one surfaces.
Required: name, vault_id, backup_repeating_time_intervals, default_retention_rule.
Optional: retention_rule, time_zone, timeouts.
Full schemas
variable "default_retention_rule" {
type = object({
life_cycle = list(object({
data_store_type = string # "VaultStore" -- NOT "OperationalStore"
duration = string
}))
})
# REQUIRED -- no default. `life_cycle` has no maximum on this resource.
}
variable "retention_rule" {
type = map(object({
priority = number
life_cycle = list(object({
data_store_type = string
duration = string
}))
criteria = object({
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 = {}
# No days_of_month on this resource. The criteria fields ARE provider-validated.
}
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.
}| Output | Description | Notes |
|---|---|---|
id |
Policy Resource ID | Emitted first. The value an instance references. |
name, vault_id, vault_name, resource_group_name, subscription_id |
Identity and owning vault | Parsed from the vault ID. |
default_retention_durations, default_life_cycle_count |
The default retention | Known at plan. |
backup_repeating_time_intervals, schedule_count |
The schedule | Known at plan. |
retention_rule_names, retention_rule_count |
The rules | Known at plan. |
rules_with_no_selector, rules_narrowing_an_absolute_criteria |
Reported, not refused | Known at plan. |
the_provider_only_checks_the_schedule_is_non_empty, the_time_zone_is_not_validated_against_a_closed_set_here, schedule_validation_differs_across_this_family |
What the provider does NOT check here | Constant. |
the_criteria_are_validated_even_though_the_schedule_is_not |
The asymmetry inside this resource | Constant. |
the_data_store_type_accepts_exactly_one_value, the_absolute_criteria_set_is_closed_and_case_sensitive, all_seven_weekdays_are_legal, the_week_number_set |
The real enum sets, as values | 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, the_repoint_path_exists_on_this_service, destroying_this_policy_breaks_every_instance_using_it |
The lifecycle facts | Change review. |
the_data_store_type_is_the_opposite_of_the_kubernetes_policy, the_default_retention_rule_is_a_block_not_a_duration_string, there_is_no_days_of_month_on_this_resource |
Differences from the sibling policies | Constant. |
this_service_backs_up_to_the_vault |
Where the data goes | Constant. |
the_schedule_has_a_minimum_but_no_maximum, a_repeating_interval_is_not_a_duration, the_selector_lists_reject_an_empty_list, the_priority_uniqueness_rule_is_documented_but_unenforced |
Validation traps | Constant. |
time_zone, time_zone_was_left_to_the_service |
Time zone | β |
the_provider_declares_no_update_timeout_and_that_is_correct, each_timeout_bounds_only_its_own_operation, 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.
The interesting property of this resource is what its provider does not check, and the documentation is written to say that plainly rather than to imply otherwise. The schedule strings and the time zone are validated for non-emptiness and nothing more: there is no interval parser, and no time-zone validator file exists for this service at all. Both are validated on siblings β the MySQL Flexible Server policy parses its schedule and checks a closed set of 141 identifiers; Data Lake Storage parses, caps at five and checks 142. Reusing that phrasing here would be a protective-claim defect, which this suite treats as the most dangerous documentation error there is, because nobody tests a safeguard they believe exists. So the shape checks in this module are described as the module's own work, which is what they are.
The weakness is not uniform, and over-generalising it would be its own error. The criteria fields are properly validated β closed, case-sensitive sets for the absolute criteria, weekdays, months and week numbers, plus a real RFC 3339 parser for the scheduled times. That asymmetry is emitted as the_criteria_are_validated_even_though_the_schedule_is_not so a reader does not conclude the whole resource is unchecked.
The policy is entirely immutable, and on this service that is survivable. All six arguments are force-new and there is no update function, so any change replaces the policy. But the backup instance's backup_policy_id updates in place, so the supported route β create a new policy, repoint the instances, remove the old one β costs no data. That is worth stating explicitly because it is not true across this family: on the Kubernetes cluster service the instance's policy reference is force-new too, and replacing a policy there replaces every instance using it.
data_store_type is the trap shared with the rest of the family. This resource accepts exactly VaultStore; the Kubernetes cluster policy accepts exactly OperationalStore. Both checks are closed, case-sensitive and one member long, and both carry a provider source comment promising another value later β so the two blocks are textually near-identical and accept opposite values. It follows from the storage model: PostgreSQL backup is vaulted, with the data copied into the vault rather than snapshotted into a resource group you nominate.
What the provider does not enforce, this module reports rather than refuses. Nothing requires a retention rule to select anything, so rules_with_no_selector names the rules that select nothing instead of rejecting them. The one place it crosses into enforcement is priority uniqueness: a duplicate leaves two rules with no defined order, which is unambiguously wrong rather than merely unusual.
lifecycle is not valid inside a module block, so a caller cannot add prevent_destroy. Where deletion protection matters, put a CanNotDelete management lock on the vault β noting that a lock prevents deletion, not replacement.
| Concern | This module's default | Opt-out |
|---|---|---|
data_store_type |
VaultStore enforced β the provider's only legal value |
none exists |
| Schedule shape | Checked by this module, because the provider checks only non-emptiness | n/a |
time_zone |
Only the detectable mistake refused (an IANA-shaped value) | n/a β the legal set cannot be enumerated here |
| Retention and schedule | Required β the provider gives no choice | n/a |
| Unenforced selector rule | Reported through outputs | n/a β never refused |
| Priority uniqueness | Enforced, because a duplicate is unambiguously wrong | none |
| Criteria enum sets | From the provider's SDK constants, resolved not transcribed | 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. The risk here is a malformed value the provider will accept and Azure will reject, so the defaults are chosen to catch what the provider does not.
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module at a tag β ?ref=v1.0.0 β never a branch. This library is plan-only: a human applies from CI.
| Check | Covered by | Needs credentials? |
|---|---|---|
| HCL parses; types are consistent | terraform validate |
No |
| Formatting | terraform fmt -check |
No |
| π΄ The schedule's shape and the IANA-name time-zone check β the provider does neither | terraform plan on a configuration that calls the module |
No |
This module's other validation {} blocks β the name pattern, the anchored vault_id, the VaultStore check, the enum sets, priority uniqueness, the selector minimums |
terraform plan |
No |
| The provider's own schema checks β the criteria enum sets, the RFC 3339 times, the block cardinalities | terraform plan |
No |
| Whether Azure accepts the schedule strings and the time zone identifier | terraform apply |
Yes |
β οΈ terraform validaterun against a configuration that calls this module evaluates none of the module's variable values, so neither this module'svalidation {}blocks nor the provider's schema checks are reached β validate reports success. The refusal lands atterraform plan, which needs no credentials for these checks.
π΄ Note which row matters most here. On this service the schedule and time zone are checked by nothing in the provider, so the first row is the only offline protection there is.
$ terraform output
id = "/subscriptions/.../resourceGroups/rg-backup-eastus/providers/Microsoft.DataProtection/backupVaults/bv-corp/backupPolicies/bp-pgsql-daily"
name = "bp-pgsql-daily"
vault_name = "bv-corp"
backup_repeating_time_intervals = ["R/2026-09-01T23:00:00+00:00/P1D"]
schedule_count = 1
default_retention_durations = ["P30D"]
default_life_cycle_count = 1
retention_rule_names = ["keep-monthly"]
retention_rule_count = 1
rules_with_no_selector = []
rules_narrowing_an_absolute_criteria = []
time_zone = "UTC"
this_policy_can_never_be_updated = true
the_repoint_path_exists_on_this_service = true
the_provider_only_checks_the_schedule_is_non_empty = true
the_time_zone_is_not_validated_against_a_closed_set_here = true
the_data_store_type_accepts_exactly_one_value = ["VaultStore"]
force_new_fields = [
"name",
"vault_id",
"backup_repeating_time_intervals",
"default_retention_rule",
"retention_rule",
"time_zone",
]| Symptom | Cause | Fix |
|---|---|---|
| Apply fails on a schedule string that passed every offline check | The provider checks only non-emptiness here; Azure parses it | Use R/<start instant>/P<period>. This module's shape checks catch the common forms; Azure is the final word. |
| A schedule that works on the MySQL policy is rejected by Azure here | Nothing changed about the string β that resource parses it and this one does not, so the same mistake surfaces later | Read the_provider_only_checks_the_schedule_is_non_empty. |
| A schedule runs at an unexpected hour | time_zone takes a Windows identifier and the provider checks only non-emptiness on this service |
Use e.g. UTC or Eastern Standard Time. Note the start instants carry their own UTC offset. |
| Plan shows the policy being replaced after a small change | Every argument is force-new and there is no update function | Expected. Create a new policy and repoint the instances β the instance's reference updates in place. See example 14. |
data_store_type must be "VaultStore" |
A life_cycle block copied from the Kubernetes cluster backup policy, where the only legal value is OperationalStore |
Change it. The two services accept exactly opposite values. |
A default_retention_rule copied from the blob or Data Lake policy will not parse |
Those take a plain duration string; this one requires a block | Wrap it: default_retention_rule = { life_cycle = [{ ... }] }. |
retention_rule priorities must be distinct |
Two rules share a priority | The provider documents the rule and checks nothing; this module enforces it. |
| 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 []. |
| A rule appears to select nothing | Nothing in the provider requires a selector | Read rules_with_no_selector in the plan output. |
| A month-end rule cannot be expressed | There is no days_of_month on this resource |
Use weeks_of_month = ["Last"] with a day selector. |
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. |
azurerm_data_protection_backup_policy_postgresql_flexible_serverazurerm_data_protection_backup_instance_postgresql_flexible_serverβ the consumer of this module'sid- Azure Backup for PostgreSQL Flexible Server
- Sibling modules:
terraform-azurerm-data-protection-backup-instance-postgresql-flexible-server,terraform-azurerm-data-protection-backup-vault,terraform-azurerm-postgresql-flexible-server,terraform-azurerm-role-assignments - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."