main.tf
 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
# Backup Spoke =================================================================

# Local vault(s) ---------------------------------------------------------------
# Created in this spoke account, alongside the resources it protects.
# Most stacks need exactly one vault — see README for the multi-vault case.

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

  tags = merge(var.tags, local.module_tags, {
    Name          = "${var.name_prefix}-${local.region_abbr}-${each.value.name_suffix}"
    resource-type = "backup-vault"
  })
}

# Backup plans -----------------------------------------------------------------
# One plan per job, one rule per plan. This is a deliberate simplification —
# AWS Backup allows multiple rules per plan and multiple selections per plan,
# but that flexibility goes unused here in favor of one clearly-named plan
# per job. Two jobs with identical schedules are two plans, not a shared one.

resource "aws_backup_plan" "this" {
  for_each = var.jobs

  name = "${var.name_prefix}-${local.region_abbr}-${each.key}-plan"

  rule {
    rule_name                = "${var.name_prefix}-${local.region_abbr}-${each.key}-rule"
    target_vault_name        = aws_backup_vault.this[each.value.target_vault_key].name
    schedule                 = each.value.schedule
    start_window             = each.value.start_window_minutes
    completion_window        = each.value.completion_window_minutes
    enable_continuous_backup = each.value.enable_continuous_backup

    lifecycle {
      delete_after       = each.value.delete_after_days
      cold_storage_after = each.value.cold_storage_after_days
    }

    dynamic "copy_action" {
      for_each = each.value.copy_action != null ? [each.value.copy_action] : []
      content {
        destination_vault_arn = copy_action.value.destination_vault_arn

        lifecycle {
          delete_after       = copy_action.value.delete_after_days
          cold_storage_after = copy_action.value.cold_storage_after_days
        }
      }
    }
  }

  tags = merge(var.tags, local.module_tags, {
    Name          = "${var.name_prefix}-${local.region_abbr}-${each.key}-plan"
    resource-type = "backup-plan"
  })
}

# Backup selections ------------------------------------------------------------
# resource_arns takes precedence over selection_tags when both are set —
# the validation on var.jobs only requires at least one to be non-empty,
# it does not forbid setting both.

resource "aws_backup_selection" "this" {
  for_each = var.jobs

  name          = "${var.name_prefix}-${local.region_abbr}-${each.key}-selection"
  plan_id       = aws_backup_plan.this[each.key].id
  iam_role_arn  = each.value.iam_role_arn
  resources     = length(each.value.resource_arns) > 0 ? each.value.resource_arns : null
  not_resources = length(each.value.not_resource_arns) > 0 ? each.value.not_resource_arns : null

  dynamic "condition" {
    for_each = length(each.value.resource_arns) == 0 && length(each.value.selection_tags) > 0 ? [each.value.selection_tags] : []
    content {
      dynamic "string_equals" {
        for_each = condition.value
        content {
          key   = "aws:ResourceTag/${string_equals.key}"
          value = string_equals.value
        }
      }
    }
  }
}