Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 54 additions & 1 deletion modules/aws-backup-source/backup_plan.tf
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,44 @@ resource "aws_backup_plan" "ebsvol" {
}
}

# this backup plan shouldn't include a continous backup rule as it isn't supported for Aurora
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
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_plan" "aurora" {
count = var.backup_plan_config_aurora.enable ? 1 : 0
name = "${local.resource_name_prefix}-aurora-plan"

dynamic "rule" {
for_each = var.backup_plan_config_aurora.rules

content {
recovery_point_tags = {
backup_rule_name = rule.value.name
Expand Down Expand Up @@ -165,6 +196,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 = "${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
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 = "${var.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 = "${var.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.')
}
75 changes: 74 additions & 1 deletion modules/aws-backup-source/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ variable "terraform_role_arn" {
default = ""

validation {
condition = var.terraform_role_arn == null
condition = var.terraform_role_arn == ""
error_message = "Warning: 'terraform_role_arn' is deprecated and should not be used."
}
}
Expand Down Expand Up @@ -90,6 +90,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 @@ -233,6 +239,72 @@ 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 = "Name prefix for vault resources"
type = string
Expand Down Expand Up @@ -304,6 +376,7 @@ variable "backup_plan_config_ebsvol" {
}
]
}

}

variable "backup_plan_config_aurora" {
Expand Down