Skip to content

Commit d1e89cf

Browse files
Merge pull request #37 from NHSDigital/CCM-7890_importingBackupModule
CCM-7890 importing nhs backup module
2 parents 9308f74 + fedf35e commit d1e89cf

17 files changed

Lines changed: 769 additions & 16 deletions

File tree

.tool-versions

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ terraform-docs 0.19.0
2020
# docker/ghcr.io/nhs-england-tools/github-runner-image 20230909-321fd1e-rt@sha256:ce4fd6035dc450a50d3cbafb4986d60e77cb49a71ab60a053bb1b9518139a646 # SEE: https://github.com/nhs-england-tools/github-runner-image/pkgs/container/github-runner-image
2121
# docker/hadolint/hadolint 2.12.0-alpine@sha256:7dba9a9f1a0350f6d021fb2f6f88900998a4fb0aaf8e4330aa8c38544f04db42 # SEE: https://hub.docker.com/r/hadolint/hadolint/tags
2222
# docker/hashicorp/terraform 1.5.6@sha256:180a7efa983386a27b43657ed610e9deed9e6c3848d54f9ea9b6cb8a5c8c25f5 # SEE: https://hub.docker.com/r/hashicorp/terraform/tags
23-
# docker/jdkato/vale v2.29.7@sha256:5ccfac574231b006284513ac3e4e9f38833989d83f2a68db149932c09de85149 # SEE: https://hub.docker.com/r/jdkato/vale/tags
23+
# docker/jdkato/vale v3.6.0@sha256:0ef22c8d537f079633cfff69fc46f69a2196072f69cab1ab232e8a79a388e425 # SEE: https://hub.docker.com/r/jdkato/vale/tags
2424
# docker/koalaman/shellcheck latest@sha256:e40388688bae0fcffdddb7e4dea49b900c18933b452add0930654b2dea3e7d5c # SEE: https://hub.docker.com/r/koalaman/shellcheck/tags
2525
# docker/mstruebing/editorconfig-checker 2.7.1@sha256:dd3ca9ea50ef4518efe9be018d669ef9cf937f6bb5cfe2ef84ff2a620b5ddc24 # SEE: https://hub.docker.com/r/mstruebing/editorconfig-checker/tags
2626
# docker/sonarsource/sonar-scanner-cli 5.0.1@sha256:494ecc3b5b1ee1625bd377b3905c4284e4f0cc155cff397805a244dee1c7d575 # SEE: https://hub.docker.com/r/sonarsource/sonar-scanner-cli/tags
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# AWS Backup Module
2+
3+
The AWS Backup Module helps automates the setup of AWS Backup resources in a source account. It streamlines the process of creating, managing, and standardising backup configurations.
4+
5+
<!-- vale off -->
6+
See [terraform-aws-backup](https://github.com/NHSDigital/terraform-aws-backup.git) for more details.
7+
<!-- vale on -->
8+
9+
## Inputs
10+
<!-- vale off -->
11+
| Name | Description | Type | Default | Required |
12+
|------|-------------|------|---------|:--------:|
13+
| <a name="input_backup_copy_vault_account_id"></a> [backup\_copy\_vault\_account\_id](#input\_backup\_copy\_vault\_account\_id) | The account id of the destination backup vault for allowing restores back into the source account. | `string` | `""` | no |
14+
| <a name="input_backup_copy_vault_arn"></a> [backup\_copy\_vault\_arn](#input\_backup\_copy\_vault\_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no |
15+
| <a name="input_backup_plan_config"></a> [backup\_plan\_config](#input\_backup\_plan\_config) | Configuration for backup plans | <pre>object({<br> selection_tag = string<br> compliance_resource_types = list(string)<br> rules = list(object({<br> name = string<br> schedule = string<br> enable_continuous_backup = optional(bool)<br> lifecycle = object({<br> delete_after = optional(number)<br> cold_storage_after = optional(number)<br> })<br> copy_action = optional(object({<br> delete_after = optional(number)<br> }))<br> }))<br> })</pre> | <pre>{<br> "compliance_resource_types": [<br> "S3"<br> ],<br> "rules": [<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "delete_after": 35<br> },<br> "name": "daily_kept_5_weeks",<br> "schedule": "cron(0 0 * * ? *)"<br> },<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "delete_after": 90<br> },<br> "name": "weekly_kept_3_months",<br> "schedule": "cron(0 1 ? * SUN *)"<br> },<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "cold_storage_after": 30,<br> "delete_after": 2555<br> },<br> "name": "monthly_kept_7_years",<br> "schedule": "cron(0 2 1 * ? *)"<br> },<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "enable_continuous_backup": true,<br> "lifecycle": {<br> "delete_after": 35<br> },<br> "name": "point_in_time_recovery",<br> "schedule": "cron(0 5 * * ? *)"<br> }<br> ],<br> "selection_tag": "BackupLocal"<br>}</pre> | no |
16+
| <a name="input_backup_plan_config_dynamodb"></a> [backup\_plan\_config\_dynamodb](#input\_backup\_plan\_config\_dynamodb) | Configuration for backup plans with dynamodb | <pre>object({<br> enable = bool<br> selection_tag = string<br> compliance_resource_types = list(string)<br> rules = optional(list(object({<br> name = string<br> schedule = string<br> enable_continuous_backup = optional(bool)<br> lifecycle = object({<br> delete_after = number<br> cold_storage_after = optional(number)<br> })<br> copy_action = optional(object({<br> delete_after = optional(number)<br> }))<br> })))<br> })</pre> | <pre>{<br> "compliance_resource_types": [<br> "DynamoDB"<br> ],<br> "enable": true,<br> "rules": [<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "delete_after": 35<br> },<br> "name": "dynamodb_daily_kept_5_weeks",<br> "schedule": "cron(0 0 * * ? *)"<br> },<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "delete_after": 90<br> },<br> "name": "dynamodb_weekly_kept_3_months",<br> "schedule": "cron(0 1 ? * SUN *)"<br> },<br> {<br> "copy_action": {<br> "delete_after": 365<br> },<br> "lifecycle": {<br> "cold_storage_after": 30,<br> "delete_after": 2555<br> },<br> "name": "dynamodb_monthly_kept_7_years",<br> "schedule": "cron(0 2 1 * ? *)"<br> }<br> ],<br> "selection_tag": "BackupDynamoDB"<br>}</pre> | no |
17+
| <a name="input_bootstrap_kms_key_arn"></a> [bootstrap\_kms\_key\_arn](#input\_bootstrap\_kms\_key\_arn) | The ARN of the bootstrap KMS key used for encryption at rest of the SNS topic. | `string` | n/a | yes |
18+
| <a name="input_environment_name"></a> [environment\_name](#input\_environment\_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes |
19+
| <a name="input_notifications_target_email_address"></a> [notifications\_target\_email\_address](#input\_notifications\_target\_email\_address) | The email address to which backup notifications will be sent via SNS. | `string` | `""` | no |
20+
| <a name="input_project_name"></a> [project\_name](#input\_project\_name) | The name of the project this relates to. | `string` | n/a | yes |
21+
| <a name="input_reports_bucket"></a> [reports\_bucket](#input\_reports\_bucket) | Bucket to drop backup reports into | `string` | n/a | yes |
22+
| <a name="input_restore_testing_plan_algorithm"></a> [restore\_testing\_plan\_algorithm](#input\_restore\_testing\_plan\_algorithm) | Algorithm of the Recovery Selection Point | `string` | `"LATEST_WITHIN_WINDOW"` | no |
23+
| <a name="input_restore_testing_plan_recovery_point_types"></a> [restore\_testing\_plan\_recovery\_point\_types](#input\_restore\_testing\_plan\_recovery\_point\_types) | Recovery Point Types | `list(string)` | <pre>[<br> "SNAPSHOT"<br>]</pre> | no |
24+
| <a name="input_restore_testing_plan_scheduled_expression"></a> [restore\_testing\_plan\_scheduled\_expression](#input\_restore\_testing\_plan\_scheduled\_expression) | Scheduled Expression of Recovery Selection Point | `string` | `"cron(0 1 ? * SUN *)"` | no |
25+
| <a name="input_restore_testing_plan_selection_window_days"></a> [restore\_testing\_plan\_selection\_window\_days](#input\_restore\_testing\_plan\_selection\_window\_days) | Selection window days | `number` | `7` | no |
26+
| <a name="input_restore_testing_plan_start_window"></a> [restore\_testing\_plan\_start\_window](#input\_restore\_testing\_plan\_start\_window) | Start window from the scheduled time during which the test should start | `number` | `1` | no |
27+
| <a name="input_terraform_role_arn"></a> [terraform\_role\_arn](#input\_terraform\_role\_arn) | ARN of Terraform role used to deploy to account | `string` | n/a | yes |
28+
<!-- vale on -->
29+
30+
## Example
31+
32+
```terraform
33+
module "test_aws_backup" {
34+
source = "./modules/aws-backup"
35+
36+
environment_name = "environment_name"
37+
bootstrap_kms_key_arn = kms_key[0].arn
38+
project_name = "testproject"
39+
reports_bucket = "compliance-reports"
40+
terraform_role_arn = data.aws_iam_role.terraform_role.arn
41+
}
42+
```
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
resource "aws_backup_framework" "main" {
2+
# must be underscores instead of dashes
3+
name = replace("${local.resource_name_prefix}-framework", "-", "_")
4+
description = "${var.project_name} Backup Framework"
5+
6+
# Evaluates if recovery points are encrypted.
7+
control {
8+
name = "BACKUP_RECOVERY_POINT_ENCRYPTED"
9+
10+
scope {
11+
tags = {
12+
"environment_name" = var.environment_name
13+
}
14+
}
15+
}
16+
17+
# Evaluates if backup vaults do not allow manual deletion of recovery points with the exception of certain IAM roles.
18+
control {
19+
name = "BACKUP_RECOVERY_POINT_MANUAL_DELETION_DISABLED"
20+
21+
scope {
22+
tags = {
23+
"environment_name" = var.environment_name
24+
}
25+
}
26+
27+
input_parameter {
28+
name = "principalArnList"
29+
value = var.terraform_role_arn
30+
}
31+
}
32+
33+
# Evaluates if recovery point retention period is at least 1 month.
34+
control {
35+
name = "BACKUP_RECOVERY_POINT_MINIMUM_RETENTION_CHECK"
36+
37+
scope {
38+
tags = {
39+
"environment_name" = var.environment_name
40+
}
41+
}
42+
43+
input_parameter {
44+
name = "requiredRetentionDays"
45+
value = "35"
46+
}
47+
}
48+
49+
# Evaluates if backup plan creates backups at least every 1 day and retains them for at least 1 month before deleting them.
50+
control {
51+
name = "BACKUP_PLAN_MIN_FREQUENCY_AND_MIN_RETENTION_CHECK"
52+
53+
scope {
54+
tags = {
55+
"environment_name" = var.environment_name
56+
}
57+
}
58+
59+
input_parameter {
60+
name = "requiredFrequencyUnit"
61+
value = "days"
62+
}
63+
64+
input_parameter {
65+
name = "requiredRetentionDays"
66+
value = "35"
67+
}
68+
69+
input_parameter {
70+
name = "requiredFrequencyValue"
71+
value = "1"
72+
}
73+
}
74+
75+
# Evaluates if resources are protected by a backup plan.
76+
control {
77+
name = "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN"
78+
79+
scope {
80+
compliance_resource_types = var.backup_plan_config.compliance_resource_types
81+
tags = {
82+
(var.backup_plan_config.selection_tag) = "True"
83+
}
84+
}
85+
}
86+
87+
# Evaluates if resources have at least one recovery point created within the past 1 day.
88+
control {
89+
name = "BACKUP_LAST_RECOVERY_POINT_CREATED"
90+
91+
input_parameter {
92+
name = "recoveryPointAgeUnit"
93+
value = "days"
94+
}
95+
96+
input_parameter {
97+
name = "recoveryPointAgeValue"
98+
value = "1"
99+
}
100+
101+
scope {
102+
compliance_resource_types = var.backup_plan_config.compliance_resource_types
103+
tags = {
104+
(var.backup_plan_config.selection_tag) = "True"
105+
}
106+
}
107+
}
108+
}
109+
110+
resource "aws_backup_framework" "dynamodb" {
111+
count = var.backup_plan_config_dynamodb.enable ? 1 : 0
112+
# must be underscores instead of dashes
113+
name = replace("${local.resource_name_prefix}-dynamodb-framework", "-", "_")
114+
description = "${var.project_name} DynamoDB Backup Framework"
115+
116+
# Evaluates if resources are protected by a backup plan.
117+
control {
118+
name = "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN"
119+
120+
scope {
121+
compliance_resource_types = var.backup_plan_config_dynamodb.compliance_resource_types
122+
tags = {
123+
(var.backup_plan_config_dynamodb.selection_tag) = "True"
124+
}
125+
}
126+
}
127+
128+
# Evaluates if resources have at least one recovery point created within the past 1 day.
129+
control {
130+
name = "BACKUP_LAST_RECOVERY_POINT_CREATED"
131+
132+
input_parameter {
133+
name = "recoveryPointAgeUnit"
134+
value = "days"
135+
}
136+
137+
input_parameter {
138+
name = "recoveryPointAgeValue"
139+
value = "1"
140+
}
141+
142+
scope {
143+
compliance_resource_types = var.backup_plan_config_dynamodb.compliance_resource_types
144+
tags = {
145+
(var.backup_plan_config_dynamodb.selection_tag) = "True"
146+
}
147+
}
148+
}
149+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
resource "aws_backup_vault_notifications" "backup_notification" {
2+
count = var.notifications_target_email_address != "" ? 1 : 0
3+
backup_vault_name = aws_backup_vault.main.name
4+
sns_topic_arn = aws_sns_topic.backup[0].arn
5+
backup_vault_events = [
6+
"BACKUP_JOB_COMPLETED",
7+
"RESTORE_JOB_COMPLETED",
8+
"S3_BACKUP_OBJECT_FAILED",
9+
"S3_RESTORE_OBJECT_FAILED",
10+
"COPY_JOB_FAILED"
11+
]
12+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
resource "aws_backup_plan" "default" {
2+
name = "${local.resource_name_prefix}-plan"
3+
4+
dynamic "rule" {
5+
for_each = var.backup_plan_config.rules
6+
content {
7+
recovery_point_tags = {
8+
backup_rule_name = rule.value.name
9+
}
10+
rule_name = rule.value.name
11+
target_vault_name = aws_backup_vault.main.name
12+
schedule = rule.value.schedule
13+
enable_continuous_backup = rule.value.enable_continuous_backup != null ? rule.value.enable_continuous_backup : null
14+
lifecycle {
15+
delete_after = rule.value.lifecycle.delete_after != null ? rule.value.lifecycle.delete_after : null
16+
cold_storage_after = rule.value.lifecycle.cold_storage_after != null ? rule.value.lifecycle.cold_storage_after : null
17+
}
18+
dynamic "copy_action" {
19+
for_each = var.backup_copy_vault_arn != "" && var.backup_copy_vault_account_id != "" && rule.value.copy_action != null ? rule.value.copy_action : {}
20+
content {
21+
lifecycle {
22+
delete_after = copy_action.value
23+
}
24+
destination_vault_arn = var.backup_copy_vault_arn
25+
}
26+
}
27+
}
28+
}
29+
}
30+
31+
# this backup plan shouldn't include a continous backup rule as it isn't supported for DynamoDB
32+
resource "aws_backup_plan" "dynamodb" {
33+
count = var.backup_plan_config_dynamodb.enable ? 1 : 0
34+
name = "${local.resource_name_prefix}-dynamodb-plan"
35+
36+
dynamic "rule" {
37+
for_each = var.backup_plan_config_dynamodb.rules
38+
content {
39+
recovery_point_tags = {
40+
backup_rule_name = rule.value.name
41+
}
42+
rule_name = rule.value.name
43+
target_vault_name = aws_backup_vault.main.name
44+
schedule = rule.value.schedule
45+
lifecycle {
46+
delete_after = rule.value.lifecycle.delete_after != null ? rule.value.lifecycle.delete_after : null
47+
cold_storage_after = rule.value.lifecycle.cold_storage_after != null ? rule.value.lifecycle.cold_storage_after : null
48+
}
49+
dynamic "copy_action" {
50+
for_each = var.backup_copy_vault_arn != "" && var.backup_copy_vault_account_id != "" && rule.value.copy_action != null ? rule.value.copy_action : {}
51+
content {
52+
lifecycle {
53+
delete_after = copy_action.value
54+
}
55+
destination_vault_arn = var.backup_copy_vault_arn
56+
}
57+
}
58+
}
59+
}
60+
}
61+
62+
resource "aws_backup_selection" "default" {
63+
iam_role_arn = aws_iam_role.backup.arn
64+
name = "${local.resource_name_prefix}-selection"
65+
plan_id = aws_backup_plan.default.id
66+
67+
selection_tag {
68+
key = var.backup_plan_config.selection_tag
69+
type = "STRINGEQUALS"
70+
value = "True"
71+
}
72+
}
73+
74+
resource "aws_backup_selection" "dynamodb" {
75+
count = var.backup_plan_config_dynamodb.enable ? 1 : 0
76+
iam_role_arn = aws_iam_role.backup.arn
77+
name = "${local.resource_name_prefix}-dynamodb-selection"
78+
plan_id = aws_backup_plan.dynamodb[0].id
79+
80+
selection_tag {
81+
key = var.backup_plan_config_dynamodb.selection_tag
82+
type = "STRINGEQUALS"
83+
value = "True"
84+
}
85+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# Create the reports
2+
resource "aws_backup_report_plan" "backup_jobs" {
3+
name = "backup_jobs"
4+
description = "Report for showing whether backups ran successfully in the last 24 hours"
5+
6+
report_delivery_channel {
7+
formats = [
8+
"JSON"
9+
]
10+
s3_bucket_name = var.reports_bucket
11+
s3_key_prefix = "backup_jobs"
12+
}
13+
14+
report_setting {
15+
report_template = "BACKUP_JOB_REPORT"
16+
}
17+
}
18+
19+
# Create the restore testing completion reports
20+
resource "aws_backup_report_plan" "backup_restore_testing_jobs" {
21+
name = "backup_restore_testing_jobs"
22+
description = "Report for showing whether backup restore test ran successfully in the last 24 hours"
23+
24+
report_delivery_channel {
25+
formats = [
26+
"JSON"
27+
]
28+
s3_bucket_name = var.reports_bucket
29+
s3_key_prefix = "backup_restore_testing_jobs"
30+
}
31+
32+
report_setting {
33+
report_template = "RESTORE_JOB_REPORT"
34+
}
35+
}
36+
37+
resource "aws_backup_report_plan" "resource_compliance" {
38+
name = "resource_compliance"
39+
description = "Report for showing whether resources are compliant with the framework"
40+
41+
report_delivery_channel {
42+
formats = [
43+
"JSON"
44+
]
45+
s3_bucket_name = var.reports_bucket
46+
s3_key_prefix = "resource_compliance"
47+
}
48+
49+
report_setting {
50+
framework_arns = var.backup_plan_config_dynamodb.enable ? [aws_backup_framework.main.arn, aws_backup_framework.dynamodb[0].arn] : [aws_backup_framework.main.arn]
51+
number_of_frameworks = 2
52+
report_template = "RESOURCE_COMPLIANCE_REPORT"
53+
}
54+
}
55+
56+
resource "aws_backup_report_plan" "copy_jobs" {
57+
count = var.backup_copy_vault_arn != "" && var.backup_copy_vault_account_id != "" ? 1 : 0
58+
name = "copy_jobs"
59+
description = "Report for showing whether copies ran successfully in the last 24 hours"
60+
61+
report_delivery_channel {
62+
formats = [
63+
"JSON"
64+
]
65+
s3_bucket_name = var.reports_bucket
66+
s3_key_prefix = "copy_jobs"
67+
}
68+
69+
report_setting {
70+
report_template = "COPY_JOB_REPORT"
71+
}
72+
}

0 commit comments

Comments
 (0)