Skip to content

Commit abd1b9c

Browse files
committed
Add Backup Restore Step Function to copy backups back to member accounts
1 parent e6974e7 commit abd1b9c

File tree

12 files changed

+337
-72
lines changed

12 files changed

+337
-72
lines changed

modules/service-deployment-regional/backup-vaults.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ locals {
1212
Resource : "*",
1313
Condition : {
1414
ArnLike : {
15-
"aws:PrincipalArn" : "arn:${var.current_aws_partition}:iam::*:role/${var.member_account_backup_service_role_name}"
15+
"aws:PrincipalArn" : "arn:${var.current.partition}:iam::*:role/${var.deployment.member_account_backup_service_role_name}"
1616
},
1717
"ForAnyValue:StringLike" : {
1818
"aws:PrincipalOrgPaths" : var.deployment.ou_paths_including_children

modules/service-deployment-regional/eventbridge.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ resource "aws_cloudwatch_event_bus_policy" "event_bus" {
2222
Resource : "*",
2323
Condition : {
2424
ArnLike : {
25-
"aws:PrincipalArn" : "arn:${var.current_aws_partition}:iam::*:role/${var.member_account_eventbridge_rule_name}",
25+
"aws:PrincipalArn" : "arn:${var.current.partition}:iam::*:role/${var.deployment.member_account_eventbridge_rule_name}",
2626
},
2727
"ForAnyValue:StringLike" : {
2828
"aws:PrincipalOrgPaths" : var.deployment.ou_paths_including_children
@@ -93,7 +93,7 @@ resource "aws_cloudwatch_event_rule" "default_to_event_bus" {
9393
"detail-type" : ["Backup Job State Change", "Copy Job State Change"],
9494
"detail" : {
9595
"$or" : [
96-
{ "backupVaultName" : [var.member_account_backup_vault_name] },
96+
{ "backupVaultName" : [var.deployment.member_account_backup_vault_name] },
9797
{ "sourceBackupVaultArn" : local.central_backup_vault_arns },
9898
{ "destinationBackupVaultArn" : local.central_backup_vault_arns }
9999
]

modules/service-deployment-regional/kms.tf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
resource "aws_kms_replica_key" "key" {
2-
count = var.region != var.current_aws_region ? 1 : 0
2+
count = var.region != var.current.region ? 1 : 0
33

44
region = var.region
55
primary_key_arn = var.kms.primary_key_arn
66
policy = var.kms.kms_key_policy
77
}
88

99
resource "aws_kms_alias" "key" {
10-
count = var.region != var.current_aws_region ? 1 : 0
10+
count = var.region != var.current.region ? 1 : 0
1111

1212
region = var.region
1313
name = var.kms.kms_key_alias

modules/service-deployment-regional/sfn_backup_ingest.tf

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ resource "aws_cloudwatch_event_rule" "backup_ingest" {
1515
"$or" : [
1616
{
1717
# Member -> Intermediate
18-
"sourceBackupVaultArn" : [{ "wildcard" : "arn:*:backup:*:*:backup-vault:${var.member_account_backup_vault_name}" }],
18+
"sourceBackupVaultArn" : [{ "wildcard" : "arn:*:backup:*:*:backup-vault:${var.deployment.member_account_backup_vault_name}" }],
1919
"destinationBackupVaultArn" : [aws_backup_vault.intermediate.arn]
2020
},
2121
{
@@ -25,7 +25,7 @@ resource "aws_cloudwatch_event_rule" "backup_ingest" {
2525
},
2626
{
2727
# Member -> LAG
28-
"sourceBackupVaultArn" : [{ "wildcard" : "arn:*:backup:*:*:backup-vault:${var.member_account_backup_vault_name}" }],
28+
"sourceBackupVaultArn" : [{ "wildcard" : "arn:*:backup:*:*:backup-vault:${var.deployment.member_account_backup_vault_name}" }],
2929
"destinationBackupVaultArn" : concat([1], values(aws_backup_logically_air_gapped_vault.lag)[*].arn)
3030
}
3131
]
@@ -70,16 +70,16 @@ resource "aws_sfn_state_machine" "backup_ingest" {
7070
"Type" : "Pass",
7171
"Output" : "", # Don't output anything to reduce CloudWatch Logs ingest
7272
"Assign" : {
73-
"accountId" : var.current_aws_account_id,
73+
"accountId" : var.current.account_id,
7474
"backupIngestSfnStateRoleArn" : var.stepfunctions.ingest_state_role_arn,
7575
"centralBackupServiceRoleArn" : var.deployment.backup_service_role_arn,
7676
"destinationBackupVaultArn" : "{% $states.input.detail.destinationBackupVaultArn %}",
7777
"destinationRecoveryPointArn" : "{% $states.input.detail.destinationRecoveryPointArn %}",
7878
"intermediateBackupVaultArn" : aws_backup_vault.intermediate.arn,
7979
"jobStatus" : "{% $states.input.detail.state %}",
8080
"lagBackupVaultNamePrefix" : var.backup_vaults.lag_vault_prefix,
81-
"memberAccountBackupServiceRoleName" : var.member_account_backup_service_role_name,
82-
"partitionId" : var.current_aws_partition,
81+
"memberAccountBackupServiceRoleName" : var.deployment.member_account_backup_service_role_name,
82+
"partitionId" : var.current.partition,
8383
"retentionTags" : {
8484
"member" : var.backup_policies.local_retention_days_tag,
8585
"intermediate" : var.backup_policies.intermediate_retention_days_tag,
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
#
2+
# Step Function to copy backups to member account restore vaults (...-default)
3+
#
4+
resource "aws_cloudwatch_log_group" "backup_restore" {
5+
region = var.region
6+
name = "/aws/vendedlogs/states/${var.stepfunctions.restore_state_machine_name}"
7+
retention_in_days = 90
8+
}
9+
10+
resource "aws_sfn_state_machine" "backup_restore" {
11+
name = var.stepfunctions.restore_state_machine_name
12+
#TODO: Fix this
13+
role_arn = var.stepfunctions.ingest_state_machine_role_arn
14+
15+
logging_configuration {
16+
level = "ALL"
17+
include_execution_data = true
18+
log_destination = "${aws_cloudwatch_log_group.backup_restore.arn}:*"
19+
}
20+
21+
/*
22+
Step Function input:
23+
{
24+
"destinationAccount": "222222222222",
25+
"recoveryPointArn": "arn:aws:backup:eu-west-2:111111111111:recovery-point:website-logs-20250708044140-61ebc5da",
26+
"sourceBackupVaultName": "central-account-backup-vault"
27+
}
28+
*/
29+
definition = jsonencode({
30+
"QueryLanguage" : "JSONata"
31+
"StartAt" : "SetVars",
32+
"States" : {
33+
"SetVars" : {
34+
"Type" : "Pass",
35+
"Assign" : {
36+
"accountId" : var.current.account_id,
37+
"backupVaultArnPrefix" : "arn:${var.current.partition}:backup:${var.current.region}:<accountId>:backup-vault:",
38+
"centralBackupServiceRoleArn" : var.deployment.backup_service_role_arn,
39+
"iamRoleArnPrefix" : "arn:${var.current.partition}:iam::<accountId>:role/",
40+
"intermediateBackupVaultArn" : aws_backup_vault.intermediate.arn,
41+
"memberAccountBackupServiceRoleName" : var.deployment.member_account_backup_service_role_name,
42+
"memberAccountBackupVaultName" : var.deployment.member_account_backup_vault_name,
43+
"memberAccountRestoreVaultName" : var.deployment.member_account_restore_vault_name,
44+
"standardBackupVaultArns" : values(aws_backup_vault.standard)[*].arn,
45+
"waitSeconds" : 30
46+
},
47+
"Output" : "{% $states.input %}"
48+
"Next" : "SourceVault?"
49+
}
50+
"SourceVault?" : {
51+
"Type" : "Choice",
52+
"Choices" : [
53+
{
54+
"Condition" : "{% ($replace($backupVaultArnPrefix, '<accountId>', $accountId) & $states.input.sourceBackupVaultName) in $standardBackupVaultArns %}",
55+
"Next" : "StartCopyToIntermediateVault"
56+
},
57+
{
58+
"Condition" : "{% ($replace($backupVaultArnPrefix, '<accountId>', $accountId) & $states.input.sourceBackupVaultName) = $intermediateBackupVaultArn %}",
59+
"Next" : "StartCopyToDestinationAccountBackupVault"
60+
}
61+
]
62+
},
63+
# Copy Standard -> Intermediate vault
64+
"StartCopyToIntermediateVault" : {
65+
"Type" : "Task",
66+
"Resource" : "arn:aws:states:::aws-sdk:backup:startCopyJob",
67+
"Arguments" : {
68+
"DestinationBackupVaultArn" : "{% $intermediateBackupVaultArn %}",
69+
"IamRoleArn" : "{% $centralBackupServiceRoleArn %}",
70+
"RecoveryPointArn" : "{% $states.input.recoveryPointArn %}",
71+
"SourceBackupVaultName" : "{% $states.input.sourceBackupVaultName %}",
72+
},
73+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
74+
"Next" : "WaitForCopyToIntermediateVault"
75+
},
76+
"WaitForCopyToIntermediateVault" : {
77+
"Type" : "Wait",
78+
"Seconds" : "{% $waitSeconds %}",
79+
"Next" : "DescribeCopyToIntermediateVault"
80+
}
81+
"DescribeCopyToIntermediateVault" : {
82+
"Type" : "Task",
83+
"Resource" : "arn:aws:states:::aws-sdk:backup:describeCopyJob",
84+
"Arguments" : {
85+
"CopyJobId" : "{% $states.input.CopyJobId %}"
86+
},
87+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
88+
"Next" : "CopiedToIntermediateVault?"
89+
},
90+
"CopiedToIntermediateVault?" : {
91+
"Type" : "Choice",
92+
"Choices" : [
93+
{
94+
"Condition" : "{% $states.input.CopyJob.State = 'COMPLETED' %}",
95+
"Next" : "StartCopyToDestinationAccountBackupVault"
96+
},
97+
{
98+
"Condition" : "{% $states.input.CopyJob.State in ['CREATED', 'RUNNING'] %}",
99+
"Next" : "WaitForCopyToIntermediateVault"
100+
}
101+
],
102+
"Default" : "Fail"
103+
},
104+
# Copy Intermediate -> Destination Account Backup vault
105+
"StartCopyToDestinationAccountBackupVault" : {
106+
"Type" : "Task",
107+
"Resource" : "arn:aws:states:::aws-sdk:backup:startCopyJob",
108+
"Arguments" : {
109+
"DestinationBackupVaultArn" : "{% $replace($backupVaultArnPrefix, '<accountId>', $states.input.destinationAccount) & $memberAccountBackupVaultName %}",
110+
"IamRoleArn" : "{% $centralBackupServiceRoleArn %}",
111+
"RecoveryPointArn" : "{% $states.input.CopyJob ? $states.input.CopyJob.DestinationRecoveryPointArn : $states.input.recoveryPointArn %}",
112+
"SourceBackupVaultName" : "{% $match($intermediateBackupVaultArn, /backup-vault:([^:]*)/).groups[0] %}",
113+
},
114+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
115+
"Next" : "WaitForCopyToDestinationAccountBackupVault"
116+
},
117+
"WaitForCopyToDestinationAccountBackupVault" : {
118+
"Type" : "Wait",
119+
"Seconds" : "{% $waitSeconds %}",
120+
"Next" : "DescribeCopyToDestinationAccountBackupVault"
121+
}
122+
"DescribeCopyToDestinationAccountBackupVault" : {
123+
"Type" : "Task",
124+
"Resource" : "arn:aws:states:::aws-sdk:backup:describeCopyJob",
125+
"Arguments" : {
126+
"CopyJobId" : "{% $states.input.CopyJobId %}"
127+
},
128+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
129+
"Next" : "CopiedToDestinationAccountBackupVault?"
130+
},
131+
"CopiedToDestinationAccountBackupVault?" : {
132+
"Type" : "Choice",
133+
"Choices" : [
134+
{
135+
"Condition" : "{% $states.input.CopyJob.State = 'COMPLETED' %}",
136+
"Next" : "StartCopyToDestinationAccountRestoreVault"
137+
},
138+
{
139+
"Condition" : "{% $states.input.CopyJob.State in ['CREATED', 'RUNNING'] %}",
140+
"Next" : "WaitForCopyToDestinationAccountBackupVault"
141+
}
142+
],
143+
"Default" : "Fail"
144+
},
145+
# Copy Destination Account Backup vault -> Destination Account Restore vault
146+
"StartCopyToDestinationAccountRestoreVault" : {
147+
"Type" : "Task",
148+
"Resource" : "arn:aws:states:::aws-sdk:backup:startCopyJob",
149+
"Credentials" : { "RoleArn" : "{% $replace($iamRoleArnPrefix, '<accountId>', $states.input.destinationAccount) & $memberAccountBackupServiceRoleName %}" },
150+
"Arguments" : {
151+
"DestinationBackupVaultArn" : "{% $replace($backupVaultArnPrefix, '<accountId>', $states.input.destinationAccount) & $memberAccountRestoreVaultName %}",
152+
"IamRoleArn" : "{% $replace($iamRoleArnPrefix, '<accountId>', $states.input.destinationAccount) & $memberAccountBackupServiceRoleName %}",
153+
"RecoveryPointArn" : "{% $states.input.CopyJob ? $states.input.CopyJob.DestinationRecoveryPointArn : $states.input.recoveryPointArn %}",
154+
"SourceBackupVaultName" : "{% $memberAccountBackupVaultName %}",
155+
},
156+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
157+
"Next" : "WaitForCopyToDestinationAccountRestoreVault"
158+
},
159+
"WaitForCopyToDestinationAccountRestoreVault" : {
160+
"Type" : "Wait",
161+
"Seconds" : "{% $waitSeconds %}",
162+
"Next" : "DescribeCopyToDestinationAccountRestoreVault"
163+
}
164+
"DescribeCopyToDestinationAccountRestoreVault" : {
165+
"Type" : "Task",
166+
"Resource" : "arn:aws:states:::aws-sdk:backup:describeCopyJob",
167+
"Arguments" : {
168+
"CopyJobId" : "{% $states.input.CopyJobId %}"
169+
},
170+
"Output" : "{% $merge([$states.input, $states.result ]) %}",
171+
"Next" : "CopiedToDestinationAccountRestoreVault?"
172+
},
173+
"CopiedToDestinationAccountRestoreVault?" : {
174+
"Type" : "Choice",
175+
"Choices" : [
176+
{
177+
"Condition" : "{% $states.input.CopyJob.State = 'COMPLETED' %}",
178+
"Next" : "Succeed"
179+
},
180+
{
181+
"Condition" : "{% $states.input.CopyJob.State in ['CREATED', 'RUNNING'] %}",
182+
"Next" : "WaitForCopyToDestinationAccountRestoreVault"
183+
}
184+
],
185+
"Default" : "Fail"
186+
},
187+
"Succeed" : {
188+
"Type" : "Succeed"
189+
},
190+
"Fail" : {
191+
"Type" : "Fail",
192+
},
193+
}
194+
})
195+
}

modules/service-deployment-regional/variables.tf

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,23 @@ variable "backup_vaults" {
1616
})
1717
}
1818

19-
variable "current_aws_account_id" {
20-
description = "The AWS account ID where Terraform is being executed."
21-
type = string
22-
}
23-
24-
variable "current_aws_partition" {
25-
description = "The current AWS partition (e.g., aws, aws-cn, aws-us-gov) where Terraform is being executed."
26-
type = string
27-
}
28-
29-
variable "current_aws_region" {
30-
description = "The current AWS region where Terraform is being executed."
31-
type = string
19+
variable "current" {
20+
description = "The current AWS account ID, organization, partition, and region."
21+
type = object({
22+
account_id : string
23+
partition : string
24+
region : string
25+
})
3226
}
3327

3428
variable "deployment" {
3529
type = object({
36-
backup_service_role_arn = string
37-
ou_paths_including_children = list(string),
30+
backup_service_role_arn = string
31+
member_account_backup_service_role_name = string
32+
member_account_backup_vault_name = string
33+
member_account_eventbridge_rule_name = string
34+
member_account_restore_vault_name = string
35+
ou_paths_including_children = list(string)
3836
})
3937
}
4038

@@ -54,21 +52,6 @@ variable "kms" {
5452
})
5553
}
5654

57-
variable "member_account_backup_service_role_name" {
58-
description = "The name of the backup service role in member accounts."
59-
type = string
60-
}
61-
62-
variable "member_account_eventbridge_rule_name" {
63-
description = "The name of the EventBridge rule in member accounts."
64-
type = string
65-
}
66-
67-
variable "member_account_backup_vault_name" {
68-
description = "The name of the backup vault in member accounts."
69-
type = string
70-
}
71-
7255
variable "ram" {
7356
type = object({
7457
create_lag_shares = bool
@@ -88,6 +71,8 @@ variable "stepfunctions" {
8871
ingest_state_machine_name = string
8972
ingest_state_machine_role_arn = string
9073
ingest_state_role_arn = string
74+
restore_state_machine_name = string
75+
restore_state_machine_role_arn = string
9176
})
9277
}
9378

modules/service-deployment/cloudformation.tf

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ locals {
33
{ "Fn::GetAtt" : ["DeploymentHelperRole", "Arn"] },
44
{ "Fn::Sub" : "arn:$${AWS::Partition}:iam::$${AWS::AccountId}:role/$${BackupServiceRoleName}" },
55
[for i in var.admin_role_names : { "Fn::Sub" : "arn:$${AWS::Partition}:iam::$${AWS::AccountId}:role/${i}" }],
6+
{ "Ref" : "CentralBackupServiceRoleArn" }
67
])
78
}
89

@@ -15,19 +16,20 @@ resource "aws_cloudformation_stack_set" "member_account_deployments" {
1516

1617
# Try to do as much as possible in native CloudFormation, but some things, like dynamic lists, are only possible in Terraform.
1718
# jsonencode(jsondecode(...)) used to minify the file.
18-
template_body = jsonencode(jsondecode(templatefile("${path.module}/templates/stackset.json.tftpl", {
19+
template_body = templatefile("${path.module}/templates/stackset.json.tftpl", {
1920
central_backup_vault_arn_templates = [for i in local.central_backup_vault_arns_template : { "Fn::Sub" : replace(replace(i, "<REGION>", "$${AWS::Region}"), var.current.account_id, "$${CentralAccountId}") }],
2021
member_eventbridge_rule_arn_templates = [for i in var.deployment_regions : { "Fn::Sub" : "arn:${var.current.partition}:events:${i}:$${AWS::AccountId}:rule/${local.member_account_eventbridge_rule_name}" }],
2122
backup_vault_admin_arn_templates = local.cfn_backup_vault_admin_arn_templates
22-
})))
23+
})
2324

2425
parameters = {
2526
BackupServiceLinkedRoleArn = var.central_backup_service_linked_role_arn
2627
BackupServiceRoleName = local.member_account_backup_service_role_name
2728
BackupServiceRestoreRoleName = local.member_account_backup_service_restore_role_name
28-
BackupServiceRolePrincipals = join(", ", [module.backup_ingest_sfn_role.role.arn])
29+
BackupServiceRolePrincipals = join(", ", [module.backup_ingest_sfn_role.role.arn, module.backup_restore_sfn_role.role.arn])
2930
BackupVaultName = local.member_account_backup_vault_name
3031
CentralAccountId = var.current.account_id
32+
CentralBackupServiceRoleArn = module.backup_service_role.role.arn
3133
DeploymentHelperRoleArn = var.central_deployment_helper_role_arn
3234
DeploymentHelperRoleNamePrefix = replace(var.member_account_deployment_helper_role_name_template, "<REGION>", "")
3335
DeploymentHelperTopicName = var.central_deployment_helper_topic_name

modules/service-deployment/iam-service-role.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
module "backup_service_role" {
77
source = "../iam-role"
88

9-
name = join("", [local.central_account_resource_name_prefix, "backup-service-role"])
9+
name = join("", [local.central_account_resource_name_prefix, "-backup-service-role"])
1010
assume_role_policy = jsonencode({
1111
Version : "2012-10-17"
1212
Statement : [

0 commit comments

Comments
 (0)