diff --git a/examples/source/aws-backups.tf b/examples/source/aws-backups.tf index b57747b..5fe9318 100644 --- a/examples/source/aws-backups.tf +++ b/examples/source/aws-backups.tf @@ -151,7 +151,7 @@ module "source" { } ] } - # Note here that we need to explicitly disable DynamoDB and Aurora backups in the source account. + # Note here that we need to explicitly disable DynamoDB, Aurora and RDS backups in the source account. # The default config in the module enables backups for all resource types. backup_plan_config_dynamodb = { "compliance_resource_types": [ @@ -183,4 +183,14 @@ module "source" { "selection_tag": "NHSE-Enable-Backup" } + backup_plan_config_rds = { + "compliance_resource_types": [ + "RDS" + ], + "rules": [ + ], + "enable": false, + "selection_tag": "NHSE-Enable-Backup" + } + } diff --git a/modules/aws-backup-source/README.md b/modules/aws-backup-source/README.md index e529c0a..bad3558 100644 --- a/modules/aws-backup-source/README.md +++ b/modules/aws-backup-source/README.md @@ -43,10 +43,13 @@ No modules. |------|------| | [aws_backup_framework.dynamodb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_framework) | resource | | [aws_backup_framework.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_framework) | resource | +| [aws_backup_framework.rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_framework) | resource | | [aws_backup_plan.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | | [aws_backup_plan.dynamodb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | +| [aws_backup_plan.rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_plan) | resource | | [aws_backup_selection.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_selection) | resource | | [aws_backup_selection.dynamodb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_selection) | resource | +| [aws_backup_selection.rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_selection) | resource | | [aws_backup_vault.main](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault) | resource | | [aws_backup_vault_notifications.backup_notification](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault_notifications) | resource | | [aws_backup_vault_policy.vault_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/backup_vault_policy) | resource | @@ -61,6 +64,7 @@ No modules. | [aws_sns_topic_subscription.aws_backup_notifications_email_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/sns_topic_subscription) | resource | | [awscc_backup_restore_testing_plan.backup_restore_testing_plan](https://registry.terraform.io/providers/hashicorp/awscc/latest/docs/resources/backup_restore_testing_plan) | resource | | [awscc_backup_restore_testing_selection.backup_restore_testing_selection_dynamodb](https://registry.terraform.io/providers/hashicorp/awscc/latest/docs/resources/backup_restore_testing_selection) | resource | +| [awscc_backup_restore_testing_selection.backup_restore_testing_selection_rds](https://registry.terraform.io/providers/hashicorp/awscc/latest/docs/resources/backup_restore_testing_selection) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy_document.allow_backup_to_sns](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | @@ -77,6 +81,8 @@ No modules. | [backup\_copy\_vault\_arn](#input\_backup\_copy\_vault\_arn) | The ARN of the destination backup vault for cross-account backup copies. | `string` | `""` | no | | [backup\_plan\_config](#input\_backup\_plan\_config) | Configuration for backup plans |
object({
selection_tag = string
selection_tag_value = optional(string)
selection_tags = optional(list(object({
key = optional(string)
value = optional(string)
})))
compliance_resource_types = list(string)
rules = list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = optional(number)
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
}))
}) | {
"compliance_resource_types": [
"S3"
],
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"enable_continuous_backup": true,
"lifecycle": {
"delete_after": 35
},
"name": "point_in_time_recovery",
"schedule": "cron(0 5 * * ? *)"
}
],
"selection_tag": "BackupLocal",
"selection_tag_value": "True",
"selection_tags": []
} | no |
| [backup\_plan\_config\_dynamodb](#input\_backup\_plan\_config\_dynamodb) | Configuration for backup plans with dynamodb | object({
enable = bool
selection_tag = string
selection_tag_value = optional(string)
selection_tags = optional(list(object({
key = optional(string)
value = optional(string)
})))
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
}) | {
"compliance_resource_types": [
"DynamoDB"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "dynamodb_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "dynamodb_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "dynamodb_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupDynamoDB",
"selection_tag_value": "True",
"selection_tags": []
} | no |
+
+| [backup\_plan\_config\_rds](#input\_backup\_plan\_config\_rds) | Configuration for backup plans with RDS | object({
enable = bool
selection_tag = string
selection_tag_value = optional(string)
selection_tags = optional(list(object({
key = optional(string)
value = optional(string)
})))
compliance_resource_types = list(string)
rules = optional(list(object({
name = string
schedule = string
completion_window = optional(number)
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
}) | {
"compliance_resource_types": [
"RDS"
],
"enable": true,
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"completion_window": 24,
"name": "rds_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"completion_window": 48,
"name": "rds_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"completion_window": 72,
"name": "rds_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupRDS",
"selection_tag_value": "True",
"selection_tags": []
} | no |
| [backup_plan_config_aurora](#input_backup_plan_config_aurora) | Configuration for backup plans with aurora | object({
enable = bool
selection_tag = string
compliance_resource_types = list(string)
restore_testing_overrides = optional(string)
rules = optional(list(object({
name = string
schedule = string
enable_continuous_backup = optional(bool)
lifecycle = object({
delete_after = number
cold_storage_after = optional(number)
})
copy_action = optional(object({
delete_after = optional(number)
}))
})))
}) | {
"compliance_resource_types": [
"Aurora"
],
"enable": true,
"restore_testing_overrides" : "{\"dbsubnetgroupname\": \"test-subnet\"}",
"rules": [
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 35
},
"name": "aurora_daily_kept_5_weeks",
"schedule": "cron(0 0 * * ? *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"delete_after": 90
},
"name": "aurora_weekly_kept_3_months",
"schedule": "cron(0 1 ? * SUN *)"
},
{
"copy_action": {
"delete_after": 365
},
"lifecycle": {
"cold_storage_after": 30,
"delete_after": 2555
},
"name": "aurora_monthly_kept_7_years",
"schedule": "cron(0 2 1 * ? *)"
}
],
"selection_tag": "BackupAurora"
} | no |
| [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 |
| [environment\_name](#input\_environment\_name) | The name of the environment where AWS Backup is configured. | `string` | n/a | yes |
diff --git a/modules/aws-backup-source/backup_framework.tf b/modules/aws-backup-source/backup_framework.tf
index b2d3043..86fcb61 100644
--- a/modules/aws-backup-source/backup_framework.tf
+++ b/modules/aws-backup-source/backup_framework.tf
@@ -252,3 +252,44 @@ resource "aws_backup_framework" "aurora" {
}
}
}
+
+resource "aws_backup_framework" "rds" {
+ count = var.backup_plan_config_rds.enable ? 1 : 0
+ # must be underscores instead of dashes
+ name = replace("${var.name_prefix}-rds-framework", "-", "_")
+ description = "${var.project_name} RDS Backup Framework"
+
+ # Evaluates if resources are protected by a backup plan.
+ control {
+ name = "BACKUP_RESOURCES_PROTECTED_BY_BACKUP_PLAN"
+
+ scope {
+ compliance_resource_types = var.backup_plan_config_rds.compliance_resource_types
+ tags = {
+ (var.backup_plan_config_rds.selection_tag) = (var.backup_plan_config_rds.selection_tag_value)
+ }
+ }
+ }
+
+ # Evaluates if resources have at least one recovery point created within the past 1 day.
+ control {
+ name = "BACKUP_LAST_RECOVERY_POINT_CREATED"
+
+ input_parameter {
+ name = "recoveryPointAgeUnit"
+ value = "days"
+ }
+
+ input_parameter {
+ name = "recoveryPointAgeValue"
+ value = "1"
+ }
+
+ scope {
+ compliance_resource_types = var.backup_plan_config_rds.compliance_resource_types
+ tags = {
+ (var.backup_plan_config_rds.selection_tag) = (var.backup_plan_config_rds.selection_tag_value)
+ }
+ }
+ }
+}
diff --git a/modules/aws-backup-source/backup_plan.tf b/modules/aws-backup-source/backup_plan.tf
index e187fe8..1fd28c0 100644
--- a/modules/aws-backup-source/backup_plan.tf
+++ b/modules/aws-backup-source/backup_plan.tf
@@ -29,7 +29,7 @@ resource "aws_backup_plan" "default" {
}
}
-# this backup plan shouldn't include a continous backup rule as it isn't supported for DynamoDB
+# this backup plan shouldn't include a continuous backup rule as it isn't supported for DynamoDB
resource "aws_backup_plan" "dynamodb" {
count = var.backup_plan_config_dynamodb.enable ? 1 : 0
name = "${var.name_prefix}-dynamodb-plan"
@@ -91,7 +91,7 @@ resource "aws_backup_plan" "ebsvol" {
}
}
-# this backup plan shouldn't include a continous backup rule as it isn't supported for Aurora
+# this backup plan shouldn't include a continuous backup rule as it isn't supported for Aurora
resource "aws_backup_plan" "aurora" {
count = var.backup_plan_config_aurora.enable ? 1 : 0
name = "${local.resource_name_prefix}-aurora-plan"
@@ -164,6 +164,58 @@ resource "aws_backup_selection" "dynamodb" {
}
}
}
+resource "aws_backup_plan" "rds" {
+ count = var.backup_plan_config_rds.enable ? 1 : 0
+ name = "${var.name_prefix}-rds-plan"
+
+ dynamic "rule" {
+ for_each = var.backup_plan_config_rds.rules
+ content {
+ recovery_point_tags = {
+ backup_rule_name = rule.value.name
+ }
+ rule_name = rule.value.name
+ target_vault_name = aws_backup_vault.main.name
+ schedule = rule.value.schedule
+ completion_window = rule.value.completion_window
+ lifecycle {
+ delete_after = rule.value.lifecycle.delete_after != null ? rule.value.lifecycle.delete_after : null
+ cold_storage_after = rule.value.lifecycle.cold_storage_after != null ? rule.value.lifecycle.cold_storage_after : null
+ }
+ dynamic "copy_action" {
+ for_each = var.backup_copy_vault_arn != "" && var.backup_copy_vault_account_id != "" && rule.value.copy_action != null ? rule.value.copy_action : {}
+ content {
+ lifecycle {
+ delete_after = copy_action.value
+ }
+ destination_vault_arn = var.backup_copy_vault_arn
+ }
+ }
+ }
+ }
+}
+
+resource "aws_backup_selection" "rds" {
+ count = var.backup_plan_config_rds.enable ? 1 : 0
+ iam_role_arn = aws_iam_role.backup.arn
+ name = "${var.name_prefix}-rds-selection"
+ plan_id = aws_backup_plan.rds[0].id
+
+ selection_tag {
+ key = var.backup_plan_config_rds.selection_tag
+ type = "STRINGEQUALS"
+ value = (var.backup_plan_config_rds.selection_tag_value == null) ? "True" : var.backup_plan_config_rds.selection_tag_value
+ }
+ condition {
+ dynamic "string_equals" {
+ for_each = local.selection_tags_rds_null_checked
+ content {
+ key = (try(string_equals.value.key, null) == null) ? null : "aws:ResourceTag/${string_equals.value.key}"
+ value = try(string_equals.value.value, null)
+ }
+ }
+ }
+}
resource "aws_backup_selection" "ebsvol" {
count = var.backup_plan_config_ebsvol.enable ? 1 : 0
diff --git a/modules/aws-backup-source/backup_restore_testing.tf b/modules/aws-backup-source/backup_restore_testing.tf
index 73a23d6..bd7b663 100644
--- a/modules/aws-backup-source/backup_restore_testing.tf
+++ b/modules/aws-backup-source/backup_restore_testing.tf
@@ -56,3 +56,18 @@ resource "awscc_backup_restore_testing_selection" "backup_restore_testing_select
}
restore_metadata_overrides = local.aurora_overrides
}
+
+resource "awscc_backup_restore_testing_selection" "backup_restore_testing_selection_rds" {
+ count = var.backup_plan_config_rds.enable ? 1 : 0
+ iam_role_arn = aws_iam_role.backup.arn
+ protected_resource_type = "RDS"
+ restore_testing_plan_name = awscc_backup_restore_testing_plan.backup_restore_testing_plan.restore_testing_plan_name
+ restore_testing_selection_name = "backup_restore_testing_selection_rds"
+ protected_resource_arns = ["*"]
+ protected_resource_conditions = {
+ string_equals = [{
+ key = "aws:ResourceTag/${var.backup_plan_config_rds.selection_tag}"
+ value = "True"
+ }]
+ }
+}
diff --git a/modules/aws-backup-source/locals.tf b/modules/aws-backup-source/locals.tf
index 965d62c..ddf3d53 100644
--- a/modules/aws-backup-source/locals.tf
+++ b/modules/aws-backup-source/locals.tf
@@ -4,13 +4,19 @@ locals {
selection_tag_value_dynamodb_null_checked = (var.backup_plan_config_dynamodb.selection_tag_value == null) ? "True" : var.backup_plan_config_dynamodb.selection_tag_value
selection_tags_null_checked = (var.backup_plan_config.selection_tags == null) ? [{ "key" : var.backup_plan_config.selection_tag, "value" : local.selection_tag_value_null_checked }] : var.backup_plan_config.selection_tags
selection_tags_dynamodb_null_checked = (var.backup_plan_config_dynamodb.selection_tags == null) ? [{ "key" : var.backup_plan_config_dynamodb.selection_tag, "value" : local.selection_tag_value_dynamodb_null_checked }] : var.backup_plan_config_dynamodb.selection_tags
+
selection_tag_value_ebsvol_null_checked = (var.backup_plan_config_ebsvol.selection_tag_value == null) ? "True" : var.backup_plan_config_ebsvol.selection_tag_value
selection_tags_ebsvol_null_checked = (var.backup_plan_config_ebsvol.selection_tags == null) ? [{ "key" : var.backup_plan_config_ebsvol.selection_tag, "value" : local.selection_tag_value_ebsvol_null_checked }] : var.backup_plan_config_ebsvol.selection_tags
+
+ selection_tag_value_rds_null_checked = (var.backup_plan_config_rds.selection_tag_value == null) ? "True" : var.backup_plan_config_rds.selection_tag_value
+ selection_tags_rds_null_checked = (var.backup_plan_config_rds.selection_tags == null) ? [{ "key" : var.backup_plan_config_rds.selection_tag, "value" : local.selection_tag_value_rds_null_checked }] : var.backup_plan_config_rds.selection_tags
+
framework_arn_list = flatten(concat(
[aws_backup_framework.main.arn],
var.backup_plan_config_ebsvol.enable ? [aws_backup_framework.ebsvol[0].arn] : [],
var.backup_plan_config_dynamodb.enable ? [aws_backup_framework.dynamodb[0].arn] : [],
- var.backup_plan_config_aurora.enable ? [aws_backup_framework.aurora[0].arn] : []
+ var.backup_plan_config_aurora.enable ? [aws_backup_framework.aurora[0].arn] : [],
+ var.backup_plan_config_rds.enable ? [aws_backup_framework.rds[0].arn] : []
))
aurora_overrides = jsondecode(var.backup_plan_config_aurora.restore_testing_overrides)
terraform_role_arns = var.terraform_role_arns != null ? var.terraform_role_arns : [var.terraform_role_arn]
diff --git a/modules/aws-backup-source/variables.tf b/modules/aws-backup-source/variables.tf
index ee73af0..9bf06fa 100644
--- a/modules/aws-backup-source/variables.tf
+++ b/modules/aws-backup-source/variables.tf
@@ -352,6 +352,76 @@ variable "backup_plan_config_aurora" {
}
}
+variable "backup_plan_config_rds" {
+ description = "Configuration for backup plans with RDS"
+ type = object({
+ enable = bool
+ selection_tag = string
+ selection_tag_value = optional(string)
+ selection_tags = optional(list(object({
+ key = optional(string)
+ value = optional(string)
+ })))
+ compliance_resource_types = list(string)
+ rules = optional(list(object({
+ name = string
+ schedule = string
+ completion_window = optional(number)
+ enable_continuous_backup = optional(bool)
+ lifecycle = object({
+ delete_after = number
+ cold_storage_after = optional(number)
+ })
+ copy_action = optional(object({
+ delete_after = optional(number)
+ }))
+ })))
+ })
+ default = {
+ enable = true
+ selection_tag = "BackupRDS"
+ selection_tag_value = "True"
+ selection_tags = []
+ compliance_resource_types = ["RDS"]
+ rules = [
+ {
+ name = "rds_daily_kept_5_weeks"
+ schedule = "cron(0 0 * * ? *)"
+ completion_window = 24
+ lifecycle = {
+ delete_after = 35
+ }
+ copy_action = {
+ delete_after = 365
+ }
+ },
+ {
+ name = "rds_weekly_kept_3_months"
+ schedule = "cron(0 1 ? * SUN *)"
+ completion_window = 48
+ lifecycle = {
+ delete_after = 90
+ }
+ copy_action = {
+ delete_after = 365
+ }
+ },
+ {
+ name = "rds_monthly_kept_7_years"
+ schedule = "cron(0 2 1 * ? *)"
+ completion_window = 72
+ lifecycle = {
+ cold_storage_after = 30
+ delete_after = 2555
+ }
+ copy_action = {
+ delete_after = 365
+ }
+ }
+ ]
+ }
+}
+
variable "name_prefix" {
description = "Name prefix for vault resources, can't contain numbers"
type = string