# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. # SPDX-License-Identifier: MIT-0 # # This sets up the AWS infrastructure to copy RDS DB Snapshots off to a separate DR account # This template is for the consumer of the snapshots # Remember that to give the producer account publish access to the DR topic you need both an identity policy on the source side and a resource policy on this side! # Show ABAC for the DR account to delete the snapshot copies in the Source account. # AWSTemplateFormatVersion: "2010-09-09" Description: "A CloudFormation Template for Copying DB Snapshots to another (DR) account" Parameters: DeploymentTimestamp: Description: The time of deploy used to trigger the fetch of the latest code Default: 2000-01-01T00:00:00 Type: String TargetAcct: Default: 222222222222 Description: The account that saves the DR Snapshots Type: String DrTagKey: Description: The tag key that identifies snapshot transit copies made for DR Default: DRACO Type: String DrTagValue: Description: The tag value that identifies snapshot transit copies made for DR Default: TransitCopy Type: String Resources: GetObjectVersions: Type: Custom::GetObjectVersions Properties: DeploymentTimestamp: !Ref DeploymentTimestamp ServiceToken: !GetAtt GetObjectVersionsFunction.Arn SourceBucket: !Sub "draco-${TargetAcct}-${AWS::Region}" PhysicalResourceId: DracoFiles Prefix: draco/ Objects: - producer.zip - producer.yaml - wait4copy.zip - wait4copy.yaml GetObjectVersionsRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: sts:AssumeRole ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Path: / Policies: - PolicyName: lambda-getobjectversions PolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: - s3:GetObject Resource: - !Sub 'arn:aws:s3:::draco-${TargetAcct}-${AWS::Region}/draco/*' - Effect: Allow Action: - s3:ListBucket Resource: - !Sub 'arn:aws:s3:::draco-${TargetAcct}-${AWS::Region}' GetObjectVersionsFunction: Type: AWS::Lambda::Function Properties: Description: Retrieves latest object versions from an S3 bucket Handler: index.handler Runtime: python3.7 Role: !GetAtt GetObjectVersionsRole.Arn Timeout: 240 Code: ZipFile: | import boto3 import botocore import cfnresponse import logging import threading from datetime import datetime logging.getLogger().setLevel(logging.INFO) def get_object_versions(src_bucket, prefix, objects): s3 = boto3.client('s3') versions = {} for o in objects: try: rsp = s3.head_object(Bucket=src_bucket, Key=prefix+o) versions[o] = rsp['VersionId'] except botocore.exceptions.ClientError as error: versions[o] = None return versions def timeout(event, context): logging.error('Execution is about to time out, sending failure response to CloudFormation') cfnresponse.send(event, context, cfnresponse.FAILED, {}, None) def handler(event, context): # make sure we send a failure to CloudFormation if the function is going to timeout timer = threading.Timer((context.get_remaining_time_in_millis() / 1000.00) - 0.5, timeout, args=[event, context]) timer.start() status = cfnresponse.SUCCESS responseData = {} try: src_bucket = event['ResourceProperties']['SourceBucket'] physical_resource_id = event['ResourceProperties']['PhysicalResourceId'] prefix = event['ResourceProperties']['Prefix'] objects = event['ResourceProperties']['Objects'] if event['RequestType'] != 'Delete': versions = get_object_versions(src_bucket, prefix, objects) responseData = versions logging.info(f'Bucket: {src_bucket} has versions: {versions}') except Exception as e: logging.error(f'Exception: {e}', exc_info=True) status = cfnresponse.FAILED finally: timer.cancel() cfnresponse.send(event, context, status, responseData, physical_resource_id) SnapshotEncryptionKey: Type: AWS::KMS::Key Properties: Description: Key used for encrypting transit copies Enabled: true EnableKeyRotation: false KeyPolicy: Version: '2012-10-17' Id: transit_key_policy Statement: - Sid: Allow Source Account Full Access Effect: Allow Principal: AWS: - !Sub ${AWS::AccountId} Action: kms:* Resource: '*' - Sid: Lambda function to copy encrypted RDS Snapshots Effect: Allow Principal: AWS: - !GetAtt LambdaExecutionRole.Arn Action: - kms:Encrypt - kms:Decrypt - kms:ReEncrypt* - kms:GenerateDataKey* - kms:ListGrants - kms:RevokeGrant - kms:CreateGrant - kms:DescribeKey Resource: '*' - Sid: DR account copy encrypted RDS Snapshots Effect: Allow Principal: "*" Action: - kms:Encrypt - kms:Decrypt - kms:ReEncrypt* - kms:GenerateDataKey* - kms:ListGrants - kms:RevokeGrant - kms:CreateGrant - kms:DescribeKey Resource: '*' Condition: StringEquals: aws:PrincipalAccount: !Ref TargetAcct LambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Principal: Service: lambda.amazonaws.com Action: - sts:AssumeRole Policies: - PolicyName: CloudwatchLogs PolicyDocument: Statement: - Effect: Allow Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents - logs:GetLogEvents Resource: - !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:* - PolicyName: ManipulateSnapshots PolicyDocument: Statement: - Effect: Allow Action: - rds:CopyDBSnapshot - rds:CopyDBClusterSnapshot - rds:DeleteDBSnapshot - rds:DeleteDBClusterSnapshot - rds:ModifyDBSnapshotAttribute - rds:ModifyDBClusterSnapshotAttribute - rds:DescribeDBSnapshots - rds:DescribeDBClusterSnapshots - rds:ListTagsForResource - ec2:CopySnapshot - ec2:CreateTags - ec2:DescribeSnapshots - ec2:DescribeTags - ec2:ModifySnapshotAttribute - ec2:DeleteSnapshot Resource: '*' - PolicyName: AllowPublish2DrTopic PolicyDocument: Statement: - Effect: Allow Action: - sns:Publish Resource: !Sub "arn:aws:sns:${AWS::Region}:${TargetAcct}:DracoConsumer" - PolicyName: AllowStepfunctions PolicyDocument: Statement: - Effect: Allow Action: - states:StartExecution Resource: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:wait4copy-${AWS::AccountId}" SnapshotTopicPolicy: Type: AWS::SNS::TopicPolicy Properties: Topics: - !Ref SnapshotTopic PolicyDocument: Version: '2012-10-17' Id: SetupSnapshotTopic Statement: - Sid: Defaults Effect: Allow Principal: AWS: "*" Action: - sns:GetTopicAttributes - sns:SetTopicAttributes - sns:AddPermission - sns:RemovePermission - sns:DeleteTopic - sns:Subscribe - sns:ListSubscriptionsByTopic - sns:Publish - sns:Receive Resource: !Ref SnapshotTopic Condition: StringEquals: AWS:SourceOwner: !Sub ${AWS::AccountId} - Sid: AllowTargetPublish Effect: Allow Principal: AWS: !Ref TargetAcct Action: - sns:Publish Resource: !Ref SnapshotTopic SnapshotTopic: Type: AWS::SNS::Topic Properties: DisplayName: Topic for Snapshot Processing TopicName: DracoProducer Subscription: - Endpoint: !GetAtt SnapshotHandlerLambda.Arn Protocol: lambda SnapshotEventSubscription: Type: AWS::RDS::EventSubscription Properties: Enabled: true SourceType: db-snapshot SnsTopicArn: !Ref SnapshotTopic EventCategories: - creation ClusterSnapshotEventSubscription: Type: AWS::RDS::EventSubscription Properties: Enabled: true SourceType: db-cluster-snapshot SnsTopicArn: !Ref SnapshotTopic EventCategories: - backup EventBridgeSubscriptions: Type: AWS::Events::Rule Properties: EventPattern: source: - aws.ec2 detail-type: - EBS Snapshot Notification detail: event: - createSnapshot State: ENABLED Targets: - Arn: !GetAtt SnapshotHandlerLambda.Arn Id: DracoHandler EventInvokePermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction Principal: events.amazonaws.com SourceArn: !GetAtt EventBridgeSubscriptions.Arn FunctionName: !GetAtt SnapshotHandlerLambda.Arn LambdaInvokePermission: Type: AWS::Lambda::Permission Properties: Action: lambda:InvokeFunction Principal: sns.amazonaws.com SourceArn: !Ref SnapshotTopic FunctionName: !GetAtt SnapshotHandlerLambda.Arn SnapshotHandlerLambda: Type: AWS::Lambda::Function Properties: Description: Code that copies the snapshot and shares it with the target Environment: Variables: DR_ACCT: !Ref TargetAcct DR_TOPIC_ARN: !Sub "arn:aws:sns:${AWS::Region}:${TargetAcct}:DracoConsumer" TRANSIT_KEY_ARN: !GetAtt SnapshotEncryptionKey.Arn SM_COPY_ARN: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:wait4copy-${AWS::AccountId}" TAG_KEY: !Ref DrTagKey TAG_VALUE: !Ref DrTagValue Handler: producer.handler Runtime: nodejs12.x Timeout: 60 Role: !GetAtt LambdaExecutionRole.Arn Code: S3Bucket: !Sub "draco-${TargetAcct}-${AWS::Region}" S3Key: "draco/producer.zip" S3ObjectVersion: !GetAtt GetObjectVersions.producer.zip DracoLogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: !Sub "/aws/lambda/${SnapshotHandlerLambda}" RetentionInDays: 90 CloudWatchErrorMetric: Type: AWS::Logs::MetricFilter Properties: FilterPattern: "ERROR" LogGroupName: !Ref DracoLogGroup MetricTransformations: - MetricValue: "1" DefaultValue: 0 MetricNamespace: "Draco/SnapshotHandler" MetricName: "ErrorCount" CloudWatchWarnMetric: Type: AWS::Logs::MetricFilter Properties: FilterPattern: "WARN" LogGroupName: !Ref DracoLogGroup MetricTransformations: - MetricValue: "1" DefaultValue: 0 MetricNamespace: "Draco/SnapshotHandler" MetricName: "WarnCount" Wait4Copy: Type: AWS::CloudFormation::Stack Properties: TemplateURL: !Sub "https://draco-${TargetAcct}-${AWS::Region}.s3-${AWS::Region}.${AWS::URLSuffix}/draco/wait4copy.yaml" Parameters: CodeBucket: !Sub draco-${TargetAcct}-${AWS::Region} CodeKey: draco/wait4copy.zip CodeVersion: !GetAtt GetObjectVersions.wait4copy.zip NotifyTopicArn: !Ref SnapshotTopic Outputs: ProducerTopicArn: Value: !Ref SnapshotTopic # vim: sts=2 et sw=2: