README.md

storage/backup-hub

Provisions the centralized AWS Backup vault(s) for a hub/spoke backup architecture. Deployed once, in the designated hub/backup account. Spoke accounts never create resources via this module. They only reference the vault ARNs this module outputs, as copy_action destinations in their own local backup plans (see storage/backup-spoke).

Usage

module "backup_hub" {
  source = "hcassc.jfrog.io/iac-tf-modules-virtual__storage/backup-hub/aws"
  version = "0.1.0"

  name_prefix = "hca-prod-shared"

  vaults = {
    standard = {
      name_suffix = "backup-vault"
      trusted_spoke_role_arns = local.vault_trusted_arns["standard"]
    }
  }

  tags = local.tags
}

Prerequisites, read before deploying

This module does not create or verify the IAM role AWS Backup uses in each spoke account. trusted_spoke_role_arns on every vault is a list of ARN strings, and the module trusts them blindly. AWS IAM permits a resource policy to reference a principal ARN for a role that does not exist, so the policy applies successfully either way. The failure only surfaces later, silently, as a failed cross-account copy job in the spoke account, not as a Terraform error here.

Before a vault's trust list means anything, the referenced role (hca-backup-service-role, or whatever your platform standardizes on) must already exist in that spoke account. This is currently a deliberate, manual, per-account bootstrap step, created via IAM console/CLI, not Terraform. It falls in the same category as the Bitbucket runner OIDC bootstrap. See iac-governance for the exact trust policy and managed policy attachments required.

How trust lists should be built

trusted_spoke_role_arns is a flat list(string) per vault, deliberately unopinionated. The module has no idea how you arrived at that list. Do not hand-type ARNs directly in the module call. Build them in the calling stack instead, from a small, reviewable, spoke-centric table, then invert it:

# in the calling stack, not this module

locals {
  backup_spokes = {
    shared_services = {
      account_id     = "635649352288"
      trusted_role   = "hca-backup-service-role"
      trusted_vaults = ["standard"]
    }
  }

  vault_trusted_arns = {
    for vault_key in distinct(flatten([
      for spoke in values(local.backup_spokes) : spoke.trusted_vaults
    ])) :
    vault_key => [
      for spoke_key, spoke in local.backup_spokes :
      "arn:aws:iam::${spoke.account_id}:role/${spoke.trusted_role}"
      if contains(spoke.trusted_vaults, vault_key)
    ]
  }
}

Keeping this in the stack, not the module, means adding a new spoke, or granting an existing spoke access to a new vault, is a one-line edit to backup_spokes. It stays reviewable in one place, with zero module changes required.

Naming

Vault and SNS topic names are fully derived by the module, not caller-supplied as a complete string:

<name_prefix>-<region_abbr>-<name_suffix>

region_abbr is derived internally from the deploying region (for example ap-south-1 becomes aps1) via data.aws_availability_zones. name_prefix and name_suffix are the only naming inputs a caller provides. The Name tag mirrors the resource name exactly. Do not expect them to diverge.

Design notes and known limitations

  • Single-region only. Vaults are created in whichever region the provider passed to this module resolves to. Cross-region DR (a second hub vault in another region, with hub-to-hub replication) is out of scope for this module. This is deliberately deferred pending a confirmed RTO/RPO requirement. If added later, expect either a dual-provider version of this module or a second backup-hub invocation per region, not a toggle on the existing one.
  • prevent_destroy and force_destroy = false are hardcoded and non-configurable on every vault this module creates. There is no variable to disable this. A hub vault holds the org's centralized compliance copies, and there is no legitimate everyday reason it should be destroyable. Decommissioning is a deliberate, reviewed action: temporarily remove the lifecycle block in its own PR.
  • Multiple hub vaults are supported and independently trusted. Each entry in vaults gets its own access policy, built only from that vault's own trusted_spoke_role_arns. This is intentional. A stricter compliance-tier vault can trust a smaller set of spokes than a standard-tier vault, in the same hub account.
  • Vault lock (WORM) is opt-in per vault (lock_enabled), not a module default. AWS enforces min_retention_days >= 1 and changeable_for_days >= 3. This module validates both, plus max_retention_days >= min_retention_days when both are set, at plan time rather than surfacing as an apply-time AWS API error.
  • Notifications are opt-in per vault. Either supply an existing sns_topic_arn, or set create_sns_topic = true to have the module provision one. backup_vault_events is validated at plan time against AWS Backup's actual event enum, so a typo'd event name fails fast rather than at apply.
  • No cross-region validation on a supplied sns_topic_arn. If you reuse an existing topic, it must be in the same region as the vault. The module does not check this; AWS will reject it at apply if mismatched.
  • No format validation on kms_key_arn. Both the vault's and the notification topic's KMS key ARNs are passed through as plain strings. A malformed ARN fails at apply with AWS's own error, not a Terraform validation.
  • storage/backup-spoke: deployed once per spoke account, creates the local vault, plan, and selection for that account's actual resources, with an optional copy_action targeting this module's vault ARN output.

Requirements

Name Version
terraform >= 1.15.0, < 2.0.0
aws >= 6.0, < 7.0

Providers

Name Version
aws >= 6.0, < 7.0

Resources

Name Type
aws_backup_vault.this resource
aws_backup_vault_lock_configuration.this resource
aws_backup_vault_notifications.this resource
aws_backup_vault_policy.this resource
aws_sns_topic.backup_notifications resource

Inputs

Name Description Type Default Required
name_prefix Prefix prepended to resource names created by this module string n/a yes
tags Tags applied to all resources in this module. map(string) {} no
vaults Map of centralized backup vaults to create in the hub account.Each vault carries its own list of trusted spoke role ARNs (principalsallowed to backup:CopyIntoBackupVault) so that different vaults canenforce different trust boundaries — e.g. a compliance-tier vaultrestricted to a subset of spoke accounts. map(object({ name_suffix = string kms_key_arn = optional(string, null) # Principals (spoke account backup service roles) allowed to copy # recovery points into this vault. Required — a vault with an empty # list accepts no cross-account copies. trusted_spoke_role_arns = list(string) # Vault lock (WORM compliance mode) lock_enabled = optional(bool, false) lock_changeable_for_days = optional(number, 3) lock_min_retention_days = optional(number, null) lock_max_retention_days = optional(number, null) # Notifications — fully opt-in, no default events fire unless listed. # Set sns_topic_arn to reuse an existing topic; leave null and set # create_sns_topic = true to have the module provision one. notifications = optional(object({ sns_topic_arn = optional(string, null) create_sns_topic = optional(bool, false) kms_key_arn = optional(string, null) backup_vault_events = list(string) }), null) })) {} no

Outputs

Name Description
notification_topic_arns Map of hub vault logical key to SNS topic ARN, for vaults where the module created the topic (notifications.create_sns_topic = true). Vaults using an existing topic (sns_topic_arn provided) or with no notifications configured are omitted.
vault_arns Map of hub vault logical key to ARN. Feed these into spoke backup-spoke module copy_actions.
vault_ids Map of hub vault logical key to vault name.
vault_recovery_points Map of hub vault logical key to current recovery point count.