1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
|
# Hub vaults ===================================================================
# Central destination vaults for cross-account recovery point copies.
# Created once, in the designated hub/backup account. Spoke accounts never
# create resources here directly — they only reference these vault ARNs as
# copy_action destinations in their own local backup plans.
#
# force_destroy and prevent_destroy are both fixed, non-configurable — this
# vault is the org-wide compliance record and should never be destroyable
# as a side effect of routine changes. Decommissioning is a deliberate,
# reviewed action (temporarily remove the lifecycle block in its own PR),
# not something a stale plan or a fat-fingered apply should be able to do.
resource "aws_backup_vault" "this" {
for_each = var.vaults
name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}"
kms_key_arn = each.value.kms_key_arn
force_destroy = false
tags = merge(var.tags, local.module_tags, {
Name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}"
resource-type = "backup-vault"
})
lifecycle {
prevent_destroy = true
}
}
# Vault access policy ---------------------------------------------------------
# Grants CopyIntoBackupVault to each vault's own trusted spoke role list.
# This is the only mechanism by which a cross-account copy_action from a
# spoke succeeds — without this statement, the destination vault refuses
# the incoming copy regardless of what the spoke's own IAM role allows.
resource "aws_backup_vault_policy" "this" {
for_each = var.vaults
backup_vault_name = aws_backup_vault.this[each.key].name
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowTrustedSpokeCopyIntoVault"
Effect = "Allow"
Principal = { AWS = each.value.trusted_spoke_role_arns }
Action = ["backup:CopyIntoBackupVault"]
Resource = aws_backup_vault.this[each.key].arn
}
]
})
}
# Vault lock (WORM compliance mode) --------------------------------------------
# Optional per-vault. Once locked in compliance mode, retention cannot be
# shortened or the vault deleted by anyone, including account root, until
# the lock's changeable_for_days grace period has elapsed.
resource "aws_backup_vault_lock_configuration" "this" {
for_each = { for k, v in var.vaults : k => v if v.lock_enabled }
backup_vault_name = aws_backup_vault.this[each.key].name
changeable_for_days = each.value.lock_changeable_for_days
min_retention_days = each.value.lock_min_retention_days
max_retention_days = each.value.lock_max_retention_days
}
# Notifications -----------------------------------------------------------------
# Opt-in per vault. If notifications.create_sns_topic is true and no
# sns_topic_arn is provided, the module provisions a topic. Otherwise the
# supplied sns_topic_arn is used as-is (e.g. an existing ops/alerting topic).
resource "aws_sns_topic" "backup_notifications" {
for_each = {
for k, v in var.vaults : k => v
if v.notifications != null && v.notifications.create_sns_topic && v.notifications.sns_topic_arn == null
}
name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}-notifications"
kms_master_key_id = each.value.notifications.kms_key_arn
tags = merge(var.tags, local.module_tags, {
Name = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}-notifications"
resource-type = "backup-notifications-topic"
})
}
resource "aws_backup_vault_notifications" "this" {
for_each = { for k, v in var.vaults : k => v if v.notifications != null }
backup_vault_name = aws_backup_vault.this[each.key].name
sns_topic_arn = coalesce(
each.value.notifications.sns_topic_arn,
try(aws_sns_topic.backup_notifications[each.key].arn, null)
)
backup_vault_events = each.value.notifications.backup_vault_events
}
|