Skip to content
52 changes: 52 additions & 0 deletions modules/aws-backup-source/backup_plan.tf
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,36 @@ resource "aws_backup_plan" "ebsvol" {
}
}

resource "aws_backup_plan" "rds" {
count = var.backup_plan_config_rds.enable ? 1 : 0
name = "${local.resource_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
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 = rule.value.copy_action != null ? rule.value.copy_action : {}
content {
lifecycle {
delete_after = copy_action.value
}
destination_vault_arn = aws_backup_vault.intermediary-vault[0].arn
}
}
}
}
}

resource "aws_backup_selection" "default" {
iam_role_arn = aws_iam_role.backup.arn
name = "${local.resource_name_prefix}-selection"
Expand Down Expand Up @@ -134,6 +164,28 @@ resource "aws_backup_selection" "dynamodb" {
}
}

resource "aws_backup_selection" "rds" {
count = var.backup_plan_config_rds.enable ? 1 : 0
iam_role_arn = aws_iam_role.backup.arn
name = "${local.resource_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
iam_role_arn = aws_iam_role.backup.arn
Expand Down
6 changes: 6 additions & 0 deletions modules/aws-backup-source/backup_vault.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@ resource "aws_backup_vault" "main" {
name = "${local.resource_name_prefix}-vault"
kms_key_arn = aws_kms_key.aws_backup_key.arn
}

resource "aws_backup_vault" "intermediary-vault" {
count = var.backup_plan_config_rds.enable ? 1 : 0
name = "${local.resource_name_prefix}-intermediary-vault"
kms_key_arn = aws_kms_key.aws_backup_key.arn
}
36 changes: 36 additions & 0 deletions modules/aws-backup-source/eventbridge.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module "eventbridge" {
source = "terraform-aws-modules/eventbridge/aws"
version = "3.14.3"

create_bus = false
create_role = false

rules = {
"Cross-Account-Copy-Job" = {
description = "Identify when a new recovery point is created in the intermediary vault"
event_pattern = jsonencode(
{
"source" : ["aws.backup"],
"account" : ["${data.aws_caller_identity.current.account_id}"],
"region" : ["eu-west-2"],
"detail" : {
"eventName" : ["RecoveryPointCreated"],
"serviceEventDetails" : {
"backupVaultName" : [{ "wildcard" : "*-intermediary-vault" }]
}
}
}
)
enabled = true
}
}

targets = {
"Cross-Account-Copy-Job" = [
{
name = "start_cross_account_copy_job"
arn = "arn:aws:lambda:eu-west-2:${data.aws_caller_identity.current.account_id}:function:start_cross_account_copy_job"
}
]
}
}
1 change: 0 additions & 1 deletion modules/aws-backup-source/kms.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ resource "aws_kms_key" "aws_backup_key" {
description = "AWS Backup KMS Key"
deletion_window_in_days = 30
enable_key_rotation = true
policy = data.aws_iam_policy_document.backup_key_policy.json
}

resource "aws_kms_alias" "backup_key" {
Expand Down
86 changes: 86 additions & 0 deletions modules/aws-backup-source/lambda_copy_job.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
data "aws_iam_policy_document" "lambda_assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}

resource "aws_iam_role" "iam_for_lambda_copy_job" {
count = var.backup_plan_config_rds.enable ? 1 : 0
name = "iam_for_cross_account_copy_job_lambda"
assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

data "aws_iam_policy_document" "lambda_copy_job_permissions" {
version = "2012-10-17"
statement {
effect = "Allow"
actions = ["kms:Decrypt", "kms:GenerateDataKey"]
resources = [aws_kms_key.aws_backup_key.arn]
}
statement {
effect = "Allow"
actions = [
"backup:StartCopyJob",
"backup:DescribeRecoveryPoint",
"backup:ListRecoveryPointsByBackupVault"
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = [aws_iam_role.backup.arn]
}
}

resource "aws_iam_role_policy_attachment" "lambda_role_policy_attachment" {
count = var.backup_plan_config_rds.enable ? 1 : 0
role = aws_iam_role.iam_for_lambda_copy_job[0].name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "cross_account_iam_permissions" {
count = var.backup_plan_config_rds.enable ? 1 : 0
name = "cross_account_iam_permissions_policy"
role = aws_iam_role.iam_for_lambda_copy_job[0].id
policy = data.aws_iam_policy_document.lambda_copy_job_permissions.json
}

data "archive_file" "start_cross_account_copy_job_lambda_zip" {
type = "zip"
source_dir = "${path.module}/resources"
output_path = "${path.module}/.terraform/archive_files/start_cross_account_copy_job_lambda.zip"
}

resource "aws_lambda_function" "start_cross_account_copy_job_lambda" {
count = var.backup_plan_config_rds.enable ? 1 : 0
filename = data.archive_file.start_cross_account_copy_job_lambda_zip.output_path
source_code_hash = data.archive_file.start_cross_account_copy_job_lambda_zip.output_base64sha256
function_name = "start_cross_account_copy_job"
role = aws_iam_role.iam_for_lambda_copy_job[0].arn
handler = "start_cross_account_copy_job.lambda_handler"
runtime = "python3.12"
environment {
variables = {
aws_account_id = data.aws_caller_identity.current.account_id,
backup_account_id = var.backup_copy_vault_account_id,
backup_copy_vault_arn = var.backup_copy_vault_arn,
backup_role_arn = aws_iam_role.backup.arn,
destination_vault_retention_period = var.destination_vault_retention_period
}
}
}

resource "aws_lambda_permission" "allow_eventbridge" {
count = var.backup_plan_config_rds.enable ? 1 : 0
statement_id = "AllowExecutionFromEventbridge"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.start_cross_account_copy_job_lambda[0].function_name
principal = "events.amazonaws.com"
source_arn = "arn:aws:events:eu-west-2:${data.aws_caller_identity.current.account_id}:rule/Cross-Account-Copy-Job-rule"
}
2 changes: 2 additions & 0 deletions modules/aws-backup-source/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ locals {
resource_name_prefix = var.name_prefix != null ? var.name_prefix : "${data.aws_region.current.name}-${data.aws_caller_identity.current.account_id}-backup"
selection_tag_value_null_checked = (var.backup_plan_config.selection_tag_value == null) ? "True" : var.backup_plan_config.selection_tag_value
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_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_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_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
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
framework_arn_list = flatten(concat(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import json
import boto3
import logging
import os

# Initialize AWS Backup client and logger
backup_client = boto3.client('backup')
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Create a Secrets Manager client
region_name = os.environ.get('AWS_REGION')

aws_account_id = os.environ.get('aws_account_id')
backup_account_id = os.environ.get('backup_account_id')
backup_copy_vault_arn = os.environ.get('backup_copy_vault_arn')
backup_role_arn = os.environ.get('backup_role_arn')
destination_vault_retention_period = int(os.environ.get('destination_vault_retention_period'))

def lambda_handler(event, context):
# Log the incoming event for debugging purposes
logger.info(f"Received Event: {json.dumps(event)}")

# Extract the recovery point ARN from the event
recovery_point_arn = event['detail']['serviceEventDetails']['recoveryPointArn']
source_vault_name = event['detail']['serviceEventDetails']['backupVaultName']
logger.info(f"Detected new recovery point in vault {source_vault_name}: {recovery_point_arn}")

# Start the copy job to the destination vault in another AWS account
backup_client.start_copy_job(
RecoveryPointArn=recovery_point_arn,
SourceBackupVaultName=source_vault_name,
DestinationBackupVaultArn=backup_copy_vault_arn,
IamRoleArn=backup_role_arn,
Lifecycle={
'DeleteAfterDays': destination_vault_retention_period
}
)

return {
'statusCode': 200,
'body': json.dumps('Copy job started successfully.')
}
71 changes: 71 additions & 0 deletions modules/aws-backup-source/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ variable "backup_copy_vault_account_id" {
default = ""
}

variable "destination_vault_retention_period" {
description = "Retention period for recovery points made with the copy job lambda"
type = number
default = 365
}

variable "backup_plan_config" {
description = "Configuration for backup plans"
type = object({
Expand Down Expand Up @@ -214,6 +220,71 @@ variable "backup_plan_config_dynamodb" {
}
}

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
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 * * ? *)"
lifecycle = {
delete_after = 35
}
copy_action = {
delete_after = 365
}
},
{
name = "rds_weekly_kept_3_months"
schedule = "cron(0 1 ? * SUN *)"
lifecycle = {
delete_after = 90
}
copy_action = {
delete_after = 365
}
},
{
name = "rds_monthly_kept_7_years"
schedule = "cron(0 2 1 * ? *)"
lifecycle = {
cold_storage_after = 30
delete_after = 2555
}
copy_action = {
delete_after = 365
}
}
]
}
}

variable "name_prefix" {
description = "Optional name prefix for vault resources"
Expand Down