Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Backup Terraform Module

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.

Terraform aws module type resources


🧩 Overview

  • 🗄️ One vault, fully governed. Creates aws_backup_vault plus 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_lock enforces immutable, write-once-read-many retention (SEC 17a-4) in GOVERNANCE (overridable) or COMPLIANCE (irreversible) mode.
  • 🧱 Safe destroys by construction. force_destroy = false by 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 via for_each — no count, no plan churn on reorder.
  • 🏷️ Tags everywhere. var.tags flows to the vault, plans, and frameworks and merges with provider default_tags; the merged set is surfaced as tags_all.
  • 🌍 Regional service. AWS Backup is regional — no region variable, no us-east-1 constraint; cross-region DR is expressed through copy_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.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

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!


🗺️ Where this fits in the family

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
Loading

🧬 What this module builds

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
Loading
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

✅ Provider / Versions

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).


🔑 Required IAM Permissions

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:PassRole is mandatory. The Terraform identity must be allowed to pass iam_role_arn to AWS Backup, scoped to that role's ARN (ideally with a iam:PassedToService = backup.amazonaws.com condition). Without it, plan/selection creation fails with AccessDenied.

🔒 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.


📋 AWS Prerequisites

  • Backup/restore IAM role. A role assumable by backup.amazonaws.com carrying the AWS-managed AWSBackupServiceRolePolicyForBackup (and ...ForRestores / ...ForS3Backup as the workload requires). Wire iam_role_arn from terraform-aws-iam-role; the Terraform identity needs iam:PassRole on it. A role is required whenever any selections are defined.
  • Service-linked role. AWSServiceRoleForBackup is auto-created the first time AWS Backup runs in the account (needs iam:CreateServiceLinkedRole). Audit frameworks additionally use AWSServiceRoleForBackupReports. See Using service-linked roles for AWS Backup.
  • CMK (recommended). A KMS key for vault encryption — wire kms_key_arn from terraform-aws-kms. The key policy must allow backup.amazonaws.com to use the key (kms:CreateGrant, kms:Decrypt, kms:GenerateDataKey). Omit kms_key_arn to 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). frameworks evaluate Config-backed controls — AWS Config recording must be enabled in the Region (see terraform-aws-config) for a framework to reach a COMPLETED deployment status.
  • SNS topic (notifications only). The topic's resource policy must allow backup.amazonaws.com to SNS: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.

📁 Module Structure

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

⚙️ Quick Start

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"
  }
}

🔌 Cross-Module Contract

Consumes

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)

Emits

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

📚 Example Library

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" }
}

📥 Inputs

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.


🧾 Outputs

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.

🧠 Architecture Notes

  • Vault ARN format: arn:aws:backup:<region>:<account-id>:backup-vault:<name>. This is the cross-resource reference type — it feeds copy_action.destination_vault_arn, vault policies, and KMS grants. ID format: the vault id is the vault name. Plan ARN: arn:aws:backup:<region>:<account-id>:backup-plan:<plan-id>.
  • Force-new fields: name and kms_key_arn on 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). Framework name is also force-new.
  • tagstags_alldefault_tags: var.tags is applied to each taggable resource (vault, plans, frameworks; per-item tags merge over module tags). tags_all is the provider-computed merge of resource tags over provider default_tags, with resource tags winning on key conflict. default_tags is 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/InvalidParameterValue and succeed on retry (IAM propagation latency, not a module defect). Framework deployment_status moves through CREATE_IN_PROGRESSCOMPLETED asynchronously after apply.
  • Lifecycle arithmetic: delete_after must 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 — and force_destroy is 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_days starts 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 (omit changeable_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 region variable. Cross-region DR is expressed through copy_actions to a vault created with a provider alias in the DR region.

🧱 Design Principles

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 (especially force_destroy and 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, never count, for child collections — keyed by stable caller strings so reorders don't churn the plan. Optional singletons (lock/policy/notifications) use a guarded for_each on a { this =... } map.
  • Primary outputs id + arn, plus vault_name, the plan/selection/framework maps, vault_lock_enabled, and tags_all.

🚀 Runbook

# Validate without backend or credentials
terraform init -backend=false
terraform validate
terraform fmt -check

plan / apply require valid AWS credentials (profile / SSO / OIDC) resolved through the standard provider chain, a configured Region, the IAM actions listed above (notably iam:PassRole on iam_role_arn), and — for audit frameworks — AWS Config enabled in the Region.


🧪 Testing

  • terraform init -backend=false && terraform validate — schema + reference integrity.
  • terraform fmt -check — canonical formatting.
  • terraform plan against 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, and tags_all in 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.

💬 Example Output

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" }

🔍 Troubleshooting

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

🔗 Related Docs


🧡 "Infrastructure as Code should be standardized, consistent, and secure."