Provisions a secure-by-default AWS Backup posture — a KMS-encrypted backup vault plus its backup plans, resource selections, optional audit frameworks, vault access policy, SNS notifications, and optional WORM Vault Lock — a complete, compliant backup stack from one module call. Built for the AWS provider v6.x.
- 🗄️ One vault, fully governed. Creates
aws_backup_vaultplus everything that gives it meaning: backup plans (schedule + lifecycle + cross-region/account copy), resource selections, optional audit frameworks, a vault access policy, event notifications, and an optional Vault Lock. - 🔐 Encrypted at rest, always. Recovery points are encrypted with the AWS-managed AWS Backup key by default, or a caller-supplied CMK (
kms_key_arn) for auditable, independently revocable key access — the PII baseline. - 🧊 WORM-ready when you need it. Opt-in
vault_lockenforces immutable, write-once-read-many retention (SEC 17a-4) inGOVERNANCE(overridable) orCOMPLIANCE(irreversible) mode. - 🧱 Safe destroys by construction.
force_destroy = falseby default — a destroy fails loudly while recovery points remain rather than silently deleting backups. - 🗺️ Map-driven children. Plans, selections, and frameworks are
map(object(...))keyed by stable caller strings and rendered viafor_each— nocount, no plan churn on reorder. - 🏷️ Tags everywhere.
var.tagsflows to the vault, plans, and frameworks and merges with providerdefault_tags; the merged set is surfaced astags_all. - 🌍 Regional service. AWS Backup is regional — no
regionvariable, no us-east-1 constraint; cross-region DR is expressed throughcopy_actions.
💡 Why it matters: an unrecoverable or mutable backup is an existential risk for a regulated FI. This module makes encrypted, retained, policy-driven, optionally-immutable backups the default — so the safe posture is the path of least resistance.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
terraform-aws-backup is a governance/protection module — it consumes an IAM role and (recommended) a CMK, and it protects resources from other families by ARN or tag.
flowchart LR
iam["terraform-aws-iam-role<br/>backup/restore role"]
kms["terraform-aws-kms<br/>vault CMK"]
sns["SNS topic<br/>(app-integration)"]
backup["terraform-aws-backup"]
efs["terraform-aws-efs"]
rds["terraform-aws-rds / aurora"]
ebs["terraform-aws-ebs-volume"]
ddb["terraform-aws-dynamodb"]
s3["terraform-aws-s3-bucket"]
dr["terraform-aws-backup<br/>(DR region/account vault)"]
iam -->|"iam_role_arn (PassRole)"| backup
kms -->|"kms_key_arn"| backup
sns -->|"sns_topic_arn"| backup
efs -.->|"protected by ARN/tag"| backup
rds -.->|"protected by ARN/tag"| backup
ebs -.->|"protected by ARN/tag"| backup
ddb -.->|"protected by ARN/tag"| backup
s3 -.->|"protected by ARN/tag"| backup
backup -->|"copy_action destination_vault_arn"| dr
style backup fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
flowchart TD
subgraph mod["terraform-aws-backup"]
vault["aws_backup_vault.this<br/>(keystone)<br/>KMS-encrypted"]
plan["aws_backup_plan.this<br/>for_each var.plans<br/>rules + lifecycle + copy_action"]
sel["aws_backup_selection.this<br/>for_each var.selections<br/>by ARN / tag / condition"]
fw["aws_backup_framework.this<br/>for_each var.frameworks<br/>(optional, Config-backed)"]
policy["aws_backup_vault_policy.this<br/>(optional, guarded for_each)"]
lock["aws_backup_vault_lock_configuration.this<br/>(optional WORM, guarded for_each)"]
notif["aws_backup_vault_notifications.this<br/>(optional SNS, guarded for_each)"]
end
vault --> plan
plan --> sel
vault --> fw
vault --> policy
vault --> lock
vault --> notif
style vault fill:#FF9900,color:#fff,stroke:#cc7a00,stroke-width:2px
style fw stroke-dasharray: 5 5
style policy stroke-dasharray: 5 5
style lock stroke-dasharray: 5 5
style notif stroke-dasharray: 5 5
| Resource | Count | Created when |
|---|---|---|
aws_backup_vault.this |
1 | always (keystone) |
aws_backup_plan.this |
0..N | one per plans entry |
aws_backup_selection.this |
0..N | one per selections entry |
aws_backup_framework.this |
0..N | one per frameworks entry |
aws_backup_vault_policy.this |
0 or 1 | vault_policy != null |
aws_backup_vault_lock_configuration.this |
0 or 1 | vault_lock != null |
aws_backup_vault_notifications.this |
0 or 1 | notifications != null |
| Requirement | Version |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/aws |
>= 6.0, < 7.0 |
The module declares only a required_providers block (providers.tf) and inherits the configured provider. There is no provider {} block and no credential variable — credentials resolve through the standard AWS chain at the root/pipeline level (env vars → SSO/shared credentials → assume_role → instance profile / IRSA → OIDC web identity).
Least-privilege actions the Terraform execution identity needs to manage this module.
| Action | Required for | Notes |
|---|---|---|
backup:CreateBackupVault, backup:DeleteBackupVault, backup:DescribeBackupVault |
Vault lifecycle | Delete requires the vault to be empty of recovery points |
backup:CreateBackupPlan, backup:UpdateBackupPlan, backup:DeleteBackupPlan, backup:GetBackupPlan |
Plan lifecycle | One per plans entry |
backup:CreateBackupSelection, backup:DeleteBackupSelection, backup:GetBackupSelection |
Resource selection | One per selections entry |
backup:CreateFramework, backup:UpdateFramework, backup:DeleteFramework, backup:DescribeFramework |
Audit framework | Only when frameworks is set |
backup:PutBackupVaultAccessPolicy, backup:DeleteBackupVaultAccessPolicy, backup:GetBackupVaultAccessPolicy |
Vault access policy | Only when vault_policy is set |
backup:PutBackupVaultLockConfiguration, backup:DeleteBackupVaultLockConfiguration |
Vault Lock | COMPLIANCE mode cannot be deleted after the cooling-off window |
backup:PutBackupVaultNotifications, backup:DeleteBackupVaultNotifications |
SNS notifications | Only when notifications is set |
backup:TagResource, backup:UntagResource, backup:ListTags |
Tagging | Vault, plans, frameworks |
iam:PassRole (on iam_role_arn) |
Required — passes the backup/restore role to AWS Backup | Scope to the exact role ARN |
kms:DescribeKey, kms:CreateGrant, kms:GenerateDataKey (on kms_key_arn) |
Vault CMK use | Only when a customer-managed kms_key_arn is supplied |
iam:CreateServiceLinkedRole (for backup.amazonaws.com) |
First-time AWS Backup use in the account | Creates AWSServiceRoleForBackup; one-time |
⚠️ iam:PassRoleis mandatory. The Terraform identity must be allowed to passiam_role_arnto AWS Backup, scoped to that role's ARN (ideally with aiam:PassedToService = backup.amazonaws.comcondition). Without it, plan/selection creation fails withAccessDenied.
🔒 Scope
backup:*actions to the vault ARN pattern (arn:aws:backup:<region>:<account>:backup-vault:casey-*) and the CMK grant actions to the specific key ARN.
- Backup/restore IAM role. A role assumable by
backup.amazonaws.comcarrying the AWS-managedAWSBackupServiceRolePolicyForBackup(and...ForRestores/...ForS3Backupas the workload requires). Wireiam_role_arnfromterraform-aws-iam-role; the Terraform identity needsiam:PassRoleon it. A role is required whenever anyselectionsare defined. - Service-linked role.
AWSServiceRoleForBackupis auto-created the first time AWS Backup runs in the account (needsiam:CreateServiceLinkedRole). Audit frameworks additionally useAWSServiceRoleForBackupReports. See Using service-linked roles for AWS Backup. - CMK (recommended). A KMS key for vault encryption — wire
kms_key_arnfromterraform-aws-kms. The key policy must allowbackup.amazonaws.comto use the key (kms:CreateGrant,kms:Decrypt,kms:GenerateDataKey). Omitkms_key_arnto use the AWS-managed AWS Backup key. - Service opt-in settings. Each protected resource type (EBS, EFS, RDS, Aurora, DynamoDB, S3, FSx, etc.) must be enabled in AWS Backup → Settings → Service opt-in for the Region before backups run. This is an account/Region toggle, not a Terraform-managed resource.
- AWS Config (audit frameworks only).
frameworksevaluate Config-backed controls — AWS Config recording must be enabled in the Region (seeterraform-aws-config) for a framework to reach aCOMPLETEDdeployment status. - SNS topic (notifications only). The topic's resource policy must allow
backup.amazonaws.comtoSNS:Publish. - Cross-account / cross-region copy. The destination vault must already exist and its access policy must permit copy from the source account; cross-account copy additionally requires AWS Organizations with the Backup features enabled.
- Quotas (per AWS Backup quotas): soft limits on backup plans (100), backup vaults (100), and concurrent backup/copy/restore jobs per account/Region — raisable via Service Quotas. Vault names ≤ 50 chars; this module validates 2–50.
terraform-aws-backup/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # name → vault config → plans → selections → frameworks → lock/policy/notifications → tags
├── main.tf # aws_backup_vault.this + plans / selections / frameworks / policy / lock / notifications
├── outputs.tf # id + arn + vault_name + plan/selection/framework maps + vault_lock_enabled + tags_all
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM permissions, prerequisites, gotchas
Smallest production-shaped call — an encrypted vault with a daily plan and a tag-based selection:
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-prod"
kms_key_arn = module.backup_kms.arn # from terraform-aws-kms (recommended)
iam_role_arn = module.backup_role.arn # from terraform-aws-iam-role
plans = {
daily = {
rules = [{
rule_name = "daily-35d"
schedule = "cron(0 5 ? * * *)" # 05:00 UTC daily
start_window = 60
completion_window = 180
lifecycle = {
cold_storage_after = 30
delete_after = 120 # must be >= cold_storage_after + 90
}
}]
}
}
selections = {
tagged = {
plan_key = "daily"
resources = ["*"]
conditions = {
string_equals = [{ key = "aws:ResourceTag/Backup", value = "true" }]
}
}
}
tags = {
Environment = "prod"
DataClass = "confidential"
}
}| Input | Type | Source module |
|---|---|---|
iam_role_arn |
string (IAM role ARN) |
terraform-aws-iam-role |
kms_key_arn |
string (KMS key ARN) |
terraform-aws-kms |
notifications.sns_topic_arn |
string (SNS topic ARN) |
app-integration module |
selections[*].resources |
list(string) (resource ARNs) |
terraform-aws-efs, terraform-aws-rds, terraform-aws-dynamodb, terraform-aws-ebs-volume, terraform-aws-s3-bucket |
plans[*].rules[*].copy_actions[*].destination_vault_arn |
string (vault ARN) |
terraform-aws-backup (DR region/account) |
| Output | Description | Consumed by |
|---|---|---|
id |
Backup vault id (the vault name) | references / CLI |
arn |
Vault ARN arn:aws:backup:<region>:<account>:backup-vault:<name> — the cross-resource reference type |
copy actions (destination_vault_arn), vault policies, KMS grants |
vault_name |
Vault name | cross-account references |
recovery_points |
Number of recovery points currently stored (computed) | reporting |
plan_ids |
Map of plan ids keyed by plans key |
selections / governance |
plan_arns |
Map of plan ARNs keyed by plans key |
governance / audit |
plan_versions |
Map of plan version ids keyed by plans key |
drift detection |
selection_ids |
Map of selection ids keyed by selections key |
inspection |
framework_arns |
Map of framework ARNs keyed by frameworks key |
audit / Audit Manager |
framework_deployment_statuses |
Map of framework deployment statuses (COMPLETED, CREATE_IN_PROGRESS, …) |
audit |
vault_lock_enabled |
Whether a Vault Lock is applied | compliance reporting |
tags_all |
All tags incl. provider default_tags (resource tags win) |
governance / audit |
1 · Minimal vault (AWS-managed key, no plan yet)
module "vault" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-landing"
# kms_key_arn omitted → AWS-managed AWS Backup key
# no plans/selections → just the encrypted vault (e.g. a copy destination)
}2 · Daily plan with cold-storage lifecycle
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-daily"
iam_role_arn = module.backup_role.arn
plans = {
daily = {
rules = [{
rule_name = "daily-1y"
schedule = "cron(0 5 ? * * *)"
lifecycle = {
cold_storage_after = 90
delete_after = 365 # >= cold_storage_after + 90
}
}]
}
}
}3 · Tags (merge with provider default_tags)
# Caller's provider block owns default_tags; the module never sets it.
provider "aws" {
default_tags { tags = { Owner = "platform", ManagedBy = "terraform" } }
}
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-tagged"
tags = {
Environment = "prod" # resource tag — wins over default_tags on key conflict
DataClass = "confidential"
}
}
# module.backup.tags_all == { Owner, ManagedBy, Environment, DataClass }4 · Customer-managed CMK wired from terraform-aws-kms
module "backup_kms" {
source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
alias = "casey/backup"
# key policy must allow backup.amazonaws.com: kms:CreateGrant, kms:Decrypt, kms:GenerateDataKey
}
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-cmk"
kms_key_arn = module.backup_kms.arn # auditable, independently revocable key
}5 · Tag-based selection (back up everything tagged Backup=true)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-tag-driven"
iam_role_arn = module.backup_role.arn
plans = { daily = { rules = [{ rule_name = "daily", schedule = "cron(0 5 ? * * *)" }] } }
selections = {
by_tag = {
plan_key = "daily"
resources = ["*"]
conditions = {
string_equals = [{ key = "aws:ResourceTag/Backup", value = "true" }]
}
}
}
}6 · Selection by explicit resource ARNs (RDS + EFS wired upstream)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-stateful"
iam_role_arn = module.backup_role.arn
plans = { daily = { rules = [{ rule_name = "daily", schedule = "cron(0 5 ? * * *)" }] } }
selections = {
databases = {
plan_key = "daily"
resources = [
module.rds.arn, # from terraform-aws-rds
module.efs.arn, # from terraform-aws-efs
]
}
}
}7 · Continuous backup / point-in-time recovery
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-pitr"
iam_role_arn = module.backup_role.arn
plans = {
pitr = {
rules = [{
rule_name = "continuous-35d"
schedule = "cron(0 5 ? * * *)"
enable_continuous_backup = true # PITR for supported resources (RDS, S3, etc.)
lifecycle = { delete_after = 35 } # PITR max retention is 35 days
}]
}
}
}8 · Cross-region DR copy (copy_action → DR vault)
# DR-region vault (provider alias for the DR region)
module "backup_dr" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
providers = { aws = aws.dr }
name = "casey-dr"
}
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-primary"
iam_role_arn = module.backup_role.arn
plans = {
daily = {
rules = [{
rule_name = "daily-with-dr"
schedule = "cron(0 5 ? * * *)"
lifecycle = { delete_after = 35 }
copy_actions = [{
destination_vault_arn = module.backup_dr.arn # cross-region copy
lifecycle = { delete_after = 365 } # longer retention in DR
}]
}]
}
}
}9 · Windows VSS application-consistent EC2 backups
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-windows"
iam_role_arn = module.backup_role.arn
plans = {
windows = {
rules = [{ rule_name = "daily", schedule = "cron(0 5 ? * * *)" }]
advanced_backup_settings = [{
resource_type = "EC2"
backup_options = { WindowsVSS = "enabled" }
}]
}
}
}10 · SNS event notifications (backup-job failures)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-alerted"
notifications = {
sns_topic_arn = aws_sns_topic.backup_alerts.arn # topic policy must allow backup.amazonaws.com SNS:Publish
backup_vault_events = ["BACKUP_JOB_FAILED", "COPY_JOB_FAILED", "RESTORE_JOB_FAILED"]
}
}11 · Vault access policy (deny DeleteRecoveryPoint)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-protected"
vault_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Deny"
Principal = "*"
Action = "backup:DeleteRecoveryPoint"
Resource = "*"
}]
})
}12 · GOVERNANCE Vault Lock (overridable WORM)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-governance"
# Omit changeable_for_days → GOVERNANCE mode: privileged principals can still remove the lock.
vault_lock = {
min_retention_days = 30
max_retention_days = 365
}
}13 · ⚠️ COMPLIANCE Vault Lock (IRREVERSIBLE WORM — SEC 17a-4)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-worm-7yr"
kms_key_arn = module.backup_kms.arn
iam_role_arn = module.backup_role.arn
# Setting changeable_for_days creates a COMPLIANCE lock. After the 3-day cooling-off
# window it CANNOT be removed or weakened by anyone — including the root account.
vault_lock = {
changeable_for_days = 3
min_retention_days = 2555 # 7 years
max_retention_days = 3650
}
}
# One-way door: Terraform cannot destroy this vault until every recovery point ages out.14 · Audit framework (requires AWS Config enabled)
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-audited"
frameworks = {
casey_baseline = {
description = "backup compliance baseline"
controls = [
{
name = "BACKUP_RECOVERY_POINT_MINIMUM_RETENTION_CHECK"
input_parameters = [{ name = "requiredRetentionDays", value = "35" }]
},
{ name = "BACKUP_RECOVERY_POINT_ENCRYPTED" },
{ name = "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN" },
]
}
}
}15 · ⚠️ Secure-default opt-out — force_destroy (documented exception)
# EXCEPTION: force_destroy deletes ALL recovery points so the vault can be torn down.
# Use only for ephemeral/sandbox vaults with a documented approval. It has NO effect
# on a COMPLIANCE-locked vault, whose recovery points cannot be deleted before they age out.
module "scratch_backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-sandbox"
force_destroy = true # opt-out of the safe-destroy default
}16 · End-to-end composition — KMS + role + protected stateful stack
# Customer-managed CMK for the vault
module "backup_kms" {
source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
alias = "casey/backup"
}
# Backup/restore service role
module "backup_role" {
source = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
name = "casey-backup-service"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "backup.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
managed_policy_arns = [
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForBackup",
"arn:aws:iam::aws:policy/service-role/AWSBackupServiceRolePolicyForRestores",
]
}
# Protected resource (RDS) — tagged for selection
module "rds" {
source = "git::https://github.com/microsoftexpert/terraform-aws-rds?ref=v1.0.0"
#... vpc_id / subnet_ids / kms_key_arn wired from terraform-aws-vpc / terraform-aws-kms
tags = { Backup = "true" }
}
# This module — vault + plan + tag-based selection + DR copy + notifications
module "backup" {
source = "git::https://github.com/microsoftexpert/terraform-aws-backup?ref=v1.0.0"
name = "casey-prod"
kms_key_arn = module.backup_kms.arn
iam_role_arn = module.backup_role.arn
plans = {
daily = {
rules = [{
rule_name = "daily-1y"
schedule = "cron(0 5 ? * * *)"
start_window = 60
completion_window = 180
lifecycle = { cold_storage_after = 90, delete_after = 365 }
}]
}
}
selections = {
tagged = {
plan_key = "daily"
resources = ["*"]
conditions = { string_equals = [{ key = "aws:ResourceTag/Backup", value = "true" }] }
}
}
notifications = {
sns_topic_arn = aws_sns_topic.backup_alerts.arn
backup_vault_events = ["BACKUP_JOB_FAILED", "RESTORE_JOB_FAILED"]
}
tags = { Environment = "prod", DataClass = "confidential" }
}| Name | Type | Default | Description |
|---|---|---|---|
name |
string |
— required | Vault name (the keystone). FORCE-NEW. 2–50 chars [A-Za-z0-9_-]. |
kms_key_arn |
string (ARN) |
null |
CMK encrypting recovery points. FORCE-NEW. null → AWS-managed key. |
force_destroy |
bool |
false |
Delete all recovery points so the vault can be destroyed (documented opt-out). |
iam_role_arn |
string (ARN) |
null |
Default backup/restore role AWS Backup assumes. Required when any selection is defined. |
plans |
map(object({...})) |
{} |
Backup plans (rules → schedule, lifecycle, copy_actions; advanced settings). |
selections |
map(object({...})) |
{} |
Resource selections (by ARN, tag, or condition) attached to a plan_key. |
frameworks |
map(object({...})) |
{} |
Optional Config-backed audit frameworks. |
vault_lock |
object({...}) |
null |
Optional Vault Lock (WORM). Omit changeable_for_days → GOVERNANCE; set it → COMPLIANCE (irreversible). |
vault_policy |
string (JSON) |
null |
Optional vault resource-based access policy. |
notifications |
object({...}) |
null |
Optional SNS event notifications. |
tags |
map(string) |
{} |
Tags for the vault, plans, and frameworks (merge with default_tags). |
See variables.tf for full heredoc schemas and validation rules.
| Name | Description |
|---|---|
id |
Backup vault id (the vault name). |
arn |
Vault ARN (cross-resource reference type). |
vault_name |
Vault name. |
recovery_points |
Number of recovery points currently stored (computed). |
plan_ids / plan_arns / plan_versions |
Maps keyed by plans key. |
selection_ids |
Map keyed by selections key. |
framework_arns / framework_deployment_statuses |
Maps keyed by frameworks key. |
vault_lock_enabled |
Whether a Vault Lock is applied. |
tags_all |
All tags incl. provider default_tags. |
- Vault ARN format:
arn:aws:backup:<region>:<account-id>:backup-vault:<name>. This is the cross-resource reference type — it feedscopy_action.destination_vault_arn, vault policies, and KMS grants. ID format: the vaultidis the vault name. Plan ARN:arn:aws:backup:<region>:<account-id>:backup-plan:<plan-id>. - Force-new fields:
nameandkms_key_arnon the vault both force replacement. Re-keying or renaming destroys and recreates the vault — and a vault cannot be destroyed while it holds recovery points (or while compliance-locked). Frameworknameis also force-new. tags↔tags_all↔default_tags:var.tagsis applied to each taggable resource (vault, plans, frameworks; per-itemtagsmerge over module tags).tags_allis the provider-computed merge of resource tags over providerdefault_tags, with resource tags winning on key conflict.default_tagsis the caller's provider-block concern — never set inside this module. Selections, vault lock, vault policy, and notifications are not taggable.- Eventual consistency: a newly created backup role may not be immediately assumable by AWS Backup — selection creation can transiently fail with
AccessDenied/InvalidParameterValueand succeed on retry (IAM propagation latency, not a module defect). Frameworkdeployment_statusmoves throughCREATE_IN_PROGRESS→COMPLETEDasynchronously after apply. - Lifecycle arithmetic:
delete_aftermust be ≥cold_storage_after+ 90 days (AWS enforces this) — the apply fails otherwise. Continuous-backup (PITR) recovery points are capped at 35 days retention. - Destroy ordering: the resource graph tears down selections, frameworks, policy, lock, and notifications before the vault. The vault itself will not delete while recovery points exist (intentional data protection) unless
force_destroy = true— andforce_destroyis powerless against a COMPLIANCE-locked vault. ENI/NAT destroy concerns do not apply here (AWS Backup is not VPC-attached). - Vault Lock is a one-way door in COMPLIANCE mode. Setting
changeable_for_daysstarts a cooling-off window (min 3 days); once it elapses the lock cannot be removed or weakened by anyone, including root (SEC 17a-4 WORM). Terraform can neither shorten retention nor destroy the vault until recovery points age out. Use GOVERNANCE (omitchangeable_for_days) for non-regulatory vaults. - us-east-1 globals: N/A. AWS Backup is a regional service — there is no us-east-1 constraint and no
regionvariable. Cross-region DR is expressed throughcopy_actionsto a vault created with a provider alias in the DR region.
Secure-by-default posture and every opt-out, explicitly:
| Posture | Default | Opt-out |
|---|---|---|
| Vault encryption | Always encrypted — CMK when kms_key_arn set, else AWS-managed key |
n/a (vaults are always encrypted; you choose the key) |
| Safe destroy | force_destroy = false — destroy fails while recovery points remain |
force_destroy = true (documented exception) |
| Vault Lock | null (no lock) — opt-in because COMPLIANCE is irreversible |
set vault_lock; choose GOVERNANCE (overridable) or COMPLIANCE (WORM) |
| Empty-vault deletion guard | enforced by AWS — a non-empty vault won't delete | only force_destroy (no effect under COMPLIANCE lock) |
| Backup schedule/lifecycle | none baked in — caller defines explicit rules | plans (recommended: daily + cold-storage + expiry) |
| Notifications | none | supply notifications (SNS) |
| Audit frameworks | none | supply frameworks (requires AWS Config) |
Recovery points are encrypted at rest by default; the only encryption choice is which key. For PII/privacy-regulation workloads use a CMK (
terraform-aws-kms) so key access is auditable via CloudTrail and revocable independently of the vault. Document each opt-out (especiallyforce_destroyand COMPLIANCE lock) in your root module so reviewers can see what was loosened or made permanent.
Other principles:
- One composite, one keystone. The vault owns only what is meaningless without it (plans, selections, frameworks, policy, lock, notifications). The backup role and CMK are referenced by ARN; protected resources are selected by ARN or tag — keeping blast radius to the vault and its AWS Backup sub-resources.
for_each, nevercount, for child collections — keyed by stable caller strings so reorders don't churn the plan. Optional singletons (lock/policy/notifications) use a guardedfor_eachon a{ this =... }map.- Primary outputs
id+arn, plusvault_name, the plan/selection/framework maps,vault_lock_enabled, andtags_all.
# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check
plan/applyrequire valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, a configured Region, the IAM actions listed above (notablyiam:PassRoleoniam_role_arn), and — for audit frameworks — AWS Config enabled in the Region.
terraform init -backend=false && terraform validate— schema + reference integrity.terraform fmt -check— canonical formatting.terraform planagainst a sandbox account to confirm the vault, plans, selections, and any optional sub-resources materialize as expected.- Assert
module.<name>.arn,plan_arns,selection_ids, andtags_allin your root-module test harness. ⚠️ Do not test COMPLIANCE Vault Lock in a long-lived account — it cannot be undone. Exercise lock behavior only with GOVERNANCE mode or a disposable account.
module.backup.aws_backup_vault.this: Creation complete after 2s [id=casey-prod]
module.backup.aws_backup_plan.this["daily"]: Creation complete after 1s [id=8f3c...]
module.backup.aws_backup_selection.this["tagged"]: Creation complete after 3s
module.backup.aws_backup_vault_notifications.this["this"]: Creation complete
Outputs:
arn = "arn:aws:backup:us-east-1:123456789012:backup-vault:casey-prod"
id = "casey-prod"
plan_arns = { "daily" = "arn:aws:backup:us-east-1:123456789012:backup-plan:8f3c..." }
vault_lock_enabled = false
tags_all = { "DataClass" = "confidential", "Environment" = "prod" }
| Symptom | Likely cause | Fix |
|---|---|---|
AccessDenied … iam:PassRole on selection create |
Terraform identity lacks iam:PassRole on iam_role_arn |
Grant iam:PassRole scoped to that role ARN (condition iam:PassedToService = backup.amazonaws.com) |
Each selection needs an IAM role (validation) |
Neither module-level iam_role_arn nor selection iam_role_arn set |
Set iam_role_arn at module level or per selection |
delete_after must be ≥ cold_storage_after + 90 |
Lifecycle arithmetic violated | Raise delete_after or lower cold_storage_after |
InvalidParameterValueException referencing the role |
Role not yet assumable / missing AWSBackupServiceRolePolicyForBackup |
Wait for IAM propagation (re-apply); attach the managed backup policy to the role |
Vault won't destroy: Backup vault cannot be deleted … recovery points |
Vault still holds recovery points | Let them age out, delete them manually, or set force_destroy = true (sandbox only) |
| Vault Lock won't release / vault won't destroy | COMPLIANCE lock past its cooling-off window | Irreversible — wait for all recovery points to reach min_retention_days and age out |
Framework stuck CREATE_IN_PROGRESS / FAILED |
AWS Config not recording in the Region | Enable AWS Config (terraform-aws-config); re-evaluate |
CMK errors: KMSKeyNotAccessibleException |
Vault CMK policy doesn't allow AWS Backup | Add backup.amazonaws.com grant perms (kms:CreateGrant, Decrypt, GenerateDataKey) to the key policy |
| SNS notifications never arrive | Topic policy denies AWS Backup | Allow backup.amazonaws.com SNS:Publish on the topic |
| Tag drift on every plan | A tag also set by provider default_tags with a different value |
Let resource tags win, or remove the overlap from default_tags |
| Copy job fails to DR vault | Destination vault policy doesn't permit copy / wrong region | Allow copy into the destination vault; verify destination_vault_arn region |
- AWS Backup Developer Guide
- AWS Backup Vault Lock (GOVERNANCE vs COMPLIANCE)
- Using service-linked roles for AWS Backup
- AWS Backup quotas
- AWS Backup Audit Manager frameworks
- Terraform:
aws_backup_vault·aws_backup_plan·aws_backup_selection·aws_backup_vault_lock_configuration - Sibling modules:
terraform-aws-kms,terraform-aws-iam-role,terraform-aws-rds,terraform-aws-efs,terraform-aws-dynamodb,terraform-aws-config - Module internals:
SCOPE.md
🧡 "Infrastructure as Code should be standardized, consistent, and secure."