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