├── .github └── demo-video.png ├── .gitignore ├── .npmignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets └── exporter │ └── main.py ├── bin └── cdk.ts ├── cdk.context.json ├── cdk.json ├── event.aurora.json ├── event.json ├── lib └── rds-snapshot-export-pipeline-stack.ts ├── package-lock.json ├── package.json └── tsconfig.json /.github/demo-video.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/rds-snapshot-export-to-s3-pipeline/dd886298e5600b0caa9ce6ed7ddeb9beee376aae/.github/demo-video.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | *.d.ts 3 | node_modules 4 | 5 | # CDK asset staging directory 6 | .cdk.staging 7 | cdk.out 8 | .DS_Store 9 | .vscode -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | !*.d.ts 3 | 4 | # CDK asset staging directory 5 | .cdk.staging 6 | cdk.out 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 4 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 5 | opensource-codeofconduct@amazon.com with any additional questions or comments. 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## RDS Snapshot Export to S3 Pipeline 2 | 3 | This repository creates the automation necessary to export Amazon RDS snapshots to S3 for a specific database whenever a snapshot is created, 4 | whether created by an automated snapshot, manual, or by AWS Backup service. 5 | 6 | ## Usage 7 | 8 | 1. Install the [Amazon Cloud Development Kit](https://aws.amazon.com/cdk/) (CDK). 9 | 2. Clone this repository and `cd` into it. 10 | 3. Modify the arguments to the `RdsSnapshotExportPipelineStack` constructor in `$/bin/cdk.ts` according to your environment. 11 | * `dbName`: This RDS database must already exist. 12 | * `rdsEvents`: This should be indicate the RDS event ID and corresponsing snapshot type, where: 13 | * `rdsEventId` should be: 14 | * `RdsEventId.DB_AUTOMATED_AURORA_SNAPSHOT_CREATED` for Amazon Aurora databases 15 | * `RdsEventId.DB_AUTOMATED_SNAPSHOT_CREATED` for RDS automated snapshots 16 | * `RdsEventId.DB_MANUAL_SNAPSHOT_CREATED` for AWS Backup service or otherwise. 17 | * `RdsEventId.DB_BACKUP_SNAPSHOT_FINISHED_COPY` for AWS Backup service snapshots, created shortly after a prior snapshot has been taken. 18 | * `rdsSnapshotType` should be: 19 | * `RdsSnapshotType.DB_AUTOMATED_SNAPSHOT` for Automated snapshots or 20 | * `RdsSnapshotType.DB_BACKUP_SNAPSHOT`for Backup service snapshots or 21 | * `RdsSnapshotType.DB_MANUAL_SNAPSHOT`for manual snapshots. 22 | * `s3BucketName`: An S3 bucket with the provided name will be created automatically for you. 23 | 24 | For example, the following configuration will automatically export all snapshots and snapshot-copies, created by both the Automated service 25 | and by AWS Backup of an existing RDS database named `my-rds-db`, to a new S3 bucket named `my-rds-db-snapshots-export`: 26 | ``` 27 | dbName: 'my-rds-db', 28 | rdsEvents: [ 29 | { 30 | rdsEventId: RdsEventId.DB_AUTOMATED_SNAPSHOT_CREATED, 31 | rdsSnapshotType: RdsSnapshotType.DB_AUTOMATED_SNAPSHOT 32 | }, 33 | { 34 | rdsEventId: RdsEventId.DB_MANUAL_SNAPSHOT_CREATED, 35 | rdsSnapshotType: RdsSnapshotType.DB_BACKUP_SNAPSHOT 36 | }, 37 | { 38 | rdsEventId: RdsEventId.DB_BACKUP_SNAPSHOT_FINISHED_COPY, 39 | rdsSnapshotType: RdsSnapshotType.DB_BACKUP_SNAPSHOT 40 | } 41 | ], 42 | s3BucketName: 'my-rds-db-snapshots-export' 43 | ``` 44 | 4. Execute the following: 45 | * `npm install` 46 | * `npm run cdk bootstrap` 47 | * `npm run cdk deploy` 48 | 5. Open up your `-rds-snapshot-exporter` function in the [AWS Lambda](https://console.aws.amazon.com/lambda/home) console and configure a test event using the contents of [$/event.json](./event.json) OR [$/event.aurora.json](./event.aurora.json) as a template, depending on whether or not you're using Amazon Aurora. 49 | * **NOTE:** The example content is a *subset* of an SNS event notification containing the minimum valid event data necessary to successfully trigger the Lambda function's execution. You should modify the `` value within the `Message` key to match an existing RDS snapshot (e.g. `rds:-YYYY-MM-DD-hh-mm`). You may also need to modify the `MessageId` if you are attempting to export the same snapshot more than once. 50 | 6. Click the **Test** button to start an export. 51 | 52 | You can check on the progress of the export in the [Exports in Amazon S3](https://console.aws.amazon.com/rds/home#snapshots-list:tab=exporttos3) listing. When that is finished, you can use the [AWS Glue Crawler](https://console.aws.amazon.com/glue/home#catalog:tab=crawlers) that was created for you to crawl the export, then use [Amazon Athena](https://console.aws.amazon.com/athena/home#query) to perform queries on the exported snapshot. 53 | 54 | ## Cleanup 55 | 56 | Execute `npm run cdk destroy` to delete resources pertaining to this example. 57 | 58 | You will also need to delete the following manually: 59 | * The S3 bucket that was created to store the snapshot exports. 60 | * The [CDKToolkit CloudFormation Stack](https://console.aws.amazon.com/cloudformation/home#/stacks?filteringText=CDKToolkit) created by `npm run cdk bootstrap`. 61 | * The `cdktoolkit-stagingbucket-<...>` bucket. 62 | 63 | ## Demo 64 | 65 | [![Demo](.github/demo-video.png)](https://www.youtube.com/watch?v=lyNGeDg6EII) 66 | 67 | ## License 68 | 69 | This library is licensed under the MIT-0 License. See the [LICENSE](./LICENSE) file. 70 | -------------------------------------------------------------------------------- /assets/exporter/main.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | import json 3 | import logging 4 | import os 5 | import re 6 | 7 | import boto3 8 | 9 | """ 10 | Evaluates whether or not the triggering event notification is for a Automated, 11 | Manual or AWS Backup service snapshot of the desired DB_NAME, then initiates an 12 | RDS snapshot export to S3 task of that snapshot if so. 13 | 14 | The function returns the response from the `start_export_task` API call if 15 | it was successful. The function execution will fail if any errors are produced 16 | when making the API call. Otherwise, if the triggering event does not correspond 17 | to the RDS_EVENT_ID or DB_NAME we are expecting to see, the function will return 18 | nothing. 19 | """ 20 | 21 | logger = logging.getLogger() 22 | logger.setLevel(os.getenv("LOG_LEVEL", logging.INFO)) 23 | 24 | AWS_REGION = os.environ["AWS_REGION"] 25 | RDS_EVENT_IDS = os.environ["RDS_EVENT_IDS"] 26 | RDS_SNAPSHOT_TYPES = os.environ["RDS_SNAPSHOT_TYPES"] 27 | DB_NAME = os.environ["DB_NAME"] 28 | SNAPSHOT_BUCKET_NAME = os.environ["SNAPSHOT_BUCKET_NAME"] 29 | SNAPSHOT_TASK_ROLE = os.environ["SNAPSHOT_TASK_ROLE"] 30 | SNAPSHOT_TASK_KEY = os.environ["SNAPSHOT_TASK_KEY"] 31 | DB_SNAPSHOT_TYPES = os.environ["DB_SNAPSHOT_TYPES"] 32 | 33 | # RDS_EVENT_IDS contains a string of Event IDs which should trigger the Lambda function 34 | # RDS_SNAPSHOT_TYPES contains a string of snapshot types which should correspond with the Event IDs 35 | rds_event_ids = RDS_EVENT_IDS.split(",") 36 | rds_snapshot_types = RDS_SNAPSHOT_TYPES.split(",") 37 | db_snapshot_types = DB_SNAPSHOT_TYPES.split(",") 38 | 39 | SNAPSHOT_KEY_STRING = ":snapshot:" 40 | 41 | class RdsSnapshotType(str, Enum): 42 | AUTOMATED = "AUTOMATED" 43 | BACKUP = "BACKUP" 44 | MANUAL = "MANUAL" 45 | 46 | def handler(event, context): 47 | 48 | if len(rds_event_ids) != len(rds_snapshot_types): 49 | logger.error("Configuration error. Number of event IDs doesn't " 50 | "match number of snapshot types. Recheck the function environment variables") 51 | 52 | return 53 | 54 | if event["Records"][0]["EventSource"] != "aws:sns": 55 | logger.warning( 56 | "This function only supports invocations via SNS events, " 57 | "but was triggered by the following:\n" 58 | f"{json.dumps(event)}" 59 | ) 60 | return 61 | 62 | logger.debug("EVENT INFO:") 63 | logger.debug(json.dumps(event)) 64 | 65 | message = json.loads(event["Records"][0]["Sns"]["Message"]) 66 | message_id = event["Records"][0]["Sns"]["MessageId"] 67 | 68 | handle_message(message, message_id) 69 | 70 | 71 | def handle_message(message, message_id): 72 | 73 | event_counter = 0 74 | 75 | # Lambda function can be triggered by multiple events, from manual, automanted or backup service generated snapshots. 76 | # Each snapshot type and source requires a slightly different handling 77 | for i, rds_event_id in enumerate(rds_event_ids): 78 | 79 | # Identify and process an automated RDS snapshot 80 | if message["Event ID"].endswith(rds_event_id) and re.match( 81 | "^rds:" + DB_NAME + "-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$", 82 | message["Source ID"], 83 | ) and rds_snapshot_types[i] == RdsSnapshotType.AUTOMATED: 84 | process_automated_snapshot(message, message_id, db_snapshot_types[i]) 85 | break 86 | # Identify and process an Manual RDS snapshot, which was not created by AWS Backup 87 | elif message["Event ID"].endswith(rds_event_id) and ( 88 | not re.match( 89 | "^rds:" + DB_NAME + "-\d{4}-\d{2}-\d{2}-\d{2}-\d{2}$", 90 | message["Source ID"], 91 | ) and 92 | not message["Source ID"].startswith("awsbackup:") 93 | ) and rds_snapshot_types[i] == RdsSnapshotType.MANUAL: 94 | process_manual_snapshot(message, message_id, db_snapshot_types[i]) 95 | break 96 | # Identify and process an AWS Backup snapshot 97 | elif (message["Event ID"].endswith(rds_event_id) and 98 | message["Source ID"].startswith("awsbackup:job-") and 99 | rds_snapshot_types[i] == RdsSnapshotType.BACKUP 100 | ): 101 | process_backup_snapshot(message, message_id, db_snapshot_types[i]) 102 | break 103 | else: 104 | event_counter += 1 105 | 106 | if (event_counter - 1) == i: 107 | logger.info(f"Ignoring event notification for {message['Source ID']}") 108 | logger.info( 109 | f"Function is configured to accept {RDS_EVENT_IDS} " 110 | f"notifications for {DB_NAME} only" 111 | ) 112 | 113 | 114 | def process_automated_snapshot(message, message_id, db_snapshot_type): 115 | export_task_identifier = message_id 116 | account_id = boto3.client("sts").get_caller_identity()["Account"] 117 | 118 | export_task_identifier = (message["Source ID"][4:27] + '-').replace("--", "-") + message_id 119 | source_arn = f"arn:aws:rds:{AWS_REGION}:{account_id}:{db_snapshot_type}:{message['Source ID']}" 120 | 121 | start_export_task(export_task_identifier, source_arn) 122 | 123 | 124 | def process_manual_snapshot(message, message_id, db_snapshot_type): 125 | account_id = boto3.client("sts").get_caller_identity()["Account"] 126 | 127 | export_task_identifier = (message["Source ID"][:24] + '-').replace("--", "-") + message_id 128 | source_arn = f"arn:aws:rds:{AWS_REGION}:{account_id}:{db_snapshot_type}:{message['Source ID']}" 129 | 130 | start_export_task(export_task_identifier, source_arn) 131 | 132 | 133 | """ 134 | An AWS Backup service snapshot notification does not contain the DB name, 135 | therefore an additional step is required to retrieve the snapshot details 136 | and extract the DB Identifier to compare it against the expected DB_NAME 137 | """ 138 | def process_backup_snapshot(message, message_id, db_snapshot_type): 139 | source_arn = message['Source ARN'] 140 | snapshot_id = source_arn[source_arn.rfind(SNAPSHOT_KEY_STRING) + len(SNAPSHOT_KEY_STRING):256] 141 | response = boto3.client("rds").describe_db_snapshots(DBSnapshotIdentifier=snapshot_id) 142 | 143 | logger.debug(response) 144 | 145 | if response and len(response["DBSnapshots"]) > 0: 146 | snapshot = response["DBSnapshots"][0] 147 | 148 | if ("SnapshotCreateTime" in snapshot): 149 | snapshot["SnapshotCreateTime"] = str(snapshot["SnapshotCreateTime"]) 150 | if ("InstanceCreateTime" in snapshot): 151 | snapshot["InstanceCreateTime"] = str(snapshot["InstanceCreateTime"]) 152 | if ("OriginalSnapshotCreateTime" in snapshot): 153 | snapshot["OriginalSnapshotCreateTime"] = str(snapshot["OriginalSnapshotCreateTime"]) 154 | 155 | logger.debug(f"describing snapshot: {snapshot_id}, of source_arn {source_arn}") 156 | logger.debug(snapshot) 157 | 158 | if (snapshot and "DBInstanceIdentifier" in snapshot and snapshot["DBInstanceIdentifier"] == DB_NAME): 159 | export_task_identifier = (snapshot["DBInstanceIdentifier"] + '-').replace("--", "-") + snapshot["SnapshotCreateTime"][:10] + '-' + message_id 160 | start_export_task(export_task_identifier, source_arn) 161 | else: 162 | logger.info(f"Ignoring event notification for {message['Source ID']}") 163 | logger.info( 164 | f"Function is configured to accept " 165 | f"notifications for backup jobs of {DB_NAME} only." 166 | ) 167 | else: 168 | logger.error(f"Could not describe snapshot of source {message['Source ID']}") 169 | raise Exception(f"Could not describe snapshot of source {message['Source ID']}, snapshot ID: {snapshot_id}") 170 | 171 | 172 | def start_export_task(export_task_identifier, source_arn): 173 | logger.debug(f"exportTaskIdentifier: {export_task_identifier}") 174 | logger.debug(f"sourceARN: {source_arn}") 175 | 176 | response = boto3.client("rds").start_export_task( 177 | ExportTaskIdentifier=( 178 | export_task_identifier[:60] 179 | ), 180 | SourceArn=source_arn, 181 | S3BucketName=SNAPSHOT_BUCKET_NAME, 182 | IamRoleArn=SNAPSHOT_TASK_ROLE, 183 | KmsKeyId=SNAPSHOT_TASK_KEY, 184 | ) 185 | 186 | response["SnapshotTime"] = str(response["SnapshotTime"]) 187 | 188 | logger.info("Snapshot export task started") 189 | logger.info(json.dumps(response)) 190 | 191 | return response 192 | 193 | -------------------------------------------------------------------------------- /bin/cdk.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { RdsSnapshotExportPipelineStack, RdsEventId, RdsSnapshotType } from '../lib/rds-snapshot-export-pipeline-stack'; 5 | 6 | const app = new cdk.App(); 7 | new RdsSnapshotExportPipelineStack(app, 'RdsSnapshotExportToS3Pipeline', { 8 | dbName: 'db-mysql-main', 9 | rdsEvents: 10 | [ 11 | { 12 | rdsEventId: RdsEventId.DB_AUTOMATED_SNAPSHOT_CREATED, 13 | rdsSnapshotType: RdsSnapshotType.DB_AUTOMATED_SNAPSHOT 14 | }, 15 | { 16 | rdsEventId: RdsEventId.DB_MANUAL_SNAPSHOT_CREATED, 17 | rdsSnapshotType: RdsSnapshotType.DB_MANUAL_SNAPSHOT 18 | }, 19 | { 20 | rdsEventId: RdsEventId.DB_BACKUP_SNAPSHOT_FINISHED_COPY, 21 | rdsSnapshotType: RdsSnapshotType.DB_BACKUP_SNAPSHOT 22 | } 23 | ], 24 | s3BucketName: 'db-mysql-main-2023-06-07', 25 | }); -------------------------------------------------------------------------------- /cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "aws-cdk:enableDiffNoFail": "true" 3 | } 4 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node bin/cdk.ts" 3 | } 4 | -------------------------------------------------------------------------------- /event.aurora.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "Sns": { 6 | "Type": "Notification", 7 | "MessageId": "00000000-0000-0000-0000-000000000000", 8 | "Message": "{\"Event Source\":\"db-cluster-snapshot\",\"Source ID\":\"\",\"Event ID\":\"http://docs.amazonwebservices.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0169\"}" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /event.json: -------------------------------------------------------------------------------- 1 | { 2 | "Records": [ 3 | { 4 | "EventSource": "aws:sns", 5 | "Sns": { 6 | "Type": "Notification", 7 | "MessageId": "00000000-0000-0000-0000-000000000000", 8 | "Message": "{\"Event Source\":\"db-snapshot\",\"Source ID\":\"\",\"Event ID\":\"http://docs.amazonwebservices.com/AmazonRDS/latest/UserGuide/USER_Events.html#RDS-EVENT-0091\"}" 9 | } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /lib/rds-snapshot-export-pipeline-stack.ts: -------------------------------------------------------------------------------- 1 | import { aws_lambda_event_sources, Stack, StackProps, Duration } from 'aws-cdk-lib'; 2 | import { Construct } from 'constructs'; 3 | import * as path from "path"; 4 | import { aws_s3, aws_glue, aws_iam, aws_lambda, aws_sns, aws_rds, aws_kms } from 'aws-cdk-lib'; 5 | import { Policy } from 'aws-cdk-lib/aws-iam'; 6 | 7 | export enum RdsEventId { 8 | /** 9 | * Event IDs for which the Lambda supports starting a snapshot export task. 10 | * 11 | * Note that with AWS Backup service, the service triggers a Manual snapshot created event (instead of automated), 12 | * where a new snapshot is created, or a finished copy notification when a prior snapshot of the same DB has been taken recently. 13 | * 14 | * See: 15 | * https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/USER_Events.Messages.html#USER_Events.Messages.cluster-snapshot 16 | * https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_Events.Messages.html#USER_Events.Messages.snapshot 17 | * https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html 18 | */ 19 | // For automated snapshots of Aurora RDS clusters 20 | DB_AUTOMATED_AURORA_SNAPSHOT_CREATED = "RDS-EVENT-0169", 21 | 22 | // For automated snapshots of non-Aurora RDS clusters 23 | DB_AUTOMATED_SNAPSHOT_CREATED = "RDS-EVENT-0091", 24 | 25 | // For manual snapshots and backup service snapshots of non-Aurora RDS clusters 26 | DB_MANUAL_SNAPSHOT_CREATED = "RDS-EVENT-0042", 27 | 28 | // For backup service snapshots copying () 29 | DB_BACKUP_SNAPSHOT_FINISHED_COPY = "RDS-EVENT-0197", 30 | } 31 | 32 | export enum RdsSnapshotType { 33 | /** 34 | * Snapshot Types supported by the Lambda. Each RdsEventId used should correlate with the corresponsing snapshot type. 35 | * For instance: Automated snapshot event ID should be configured to work with Automated snapshot type 36 | * 37 | * See: 38 | * https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_WorkingWithAutomatedBackups.html#AutomatedBackups.AWSBackup 39 | * 40 | */ 41 | // For automated snapshots (system snapshots) 42 | DB_AUTOMATED_SNAPSHOT = "AUTOMATED", 43 | 44 | // For Backup service snapshots 45 | DB_BACKUP_SNAPSHOT = "BACKUP", 46 | 47 | // For Backup service snapshots 48 | DB_MANUAL_SNAPSHOT = "MANUAL" 49 | } 50 | 51 | export interface RdsSnapshot { 52 | rdsEventId: RdsEventId; 53 | rdsSnapshotType: RdsSnapshotType; 54 | } 55 | 56 | export interface RdsSnapshotExportPipelineStackProps extends StackProps { 57 | /** 58 | * Name of the S3 bucket to which snapshot exports should be saved. 59 | * 60 | * NOTE: Bucket will be created if one does not already exist. 61 | */ 62 | readonly s3BucketName: string; 63 | 64 | /** 65 | * Name of the database cluster whose snapshots the function supports exporting. 66 | */ 67 | readonly dbName: string; 68 | 69 | /** 70 | * The RDS event ID and snapshot type for which the function should be triggered. 71 | */ 72 | readonly rdsEvents: Array; 73 | }; 74 | 75 | export class RdsSnapshotExportPipelineStack extends Stack { 76 | constructor(scope: Construct, id: string, props: RdsSnapshotExportPipelineStackProps) { 77 | super(scope, id, props); 78 | 79 | const bucket = new aws_s3.Bucket(this, "SnapshotExportBucket", { 80 | bucketName: props.s3BucketName, 81 | blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, 82 | }); 83 | 84 | const snapshotExportTaskRole = new aws_iam.Role(this, "SnapshotExportTaskRole", { 85 | assumedBy: new aws_iam.ServicePrincipal("export.rds.amazonaws.com"), 86 | description: "Role used by RDS to perform snapshot exports to S3", 87 | inlinePolicies: { 88 | "SnapshotExportTaskPolicy": aws_iam.PolicyDocument.fromJson({ 89 | "Version": "2012-10-17", 90 | "Statement": [ 91 | { 92 | "Action": [ 93 | "s3:PutObject*", 94 | "s3:ListBucket", 95 | "s3:GetObject*", 96 | "s3:DeleteObject*", 97 | "s3:GetBucketLocation" 98 | ], 99 | "Resource": [ 100 | `${bucket.bucketArn}`, 101 | `${bucket.bucketArn}/*`, 102 | ], 103 | "Effect": "Allow" 104 | } 105 | ], 106 | }) 107 | } 108 | }); 109 | 110 | const lambdaExecutionRole = new aws_iam.Role(this, "RdsSnapshotExporterLambdaExecutionRole", { 111 | assumedBy: new aws_iam.ServicePrincipal("lambda.amazonaws.com"), 112 | description: 'RdsSnapshotExportToS3 Lambda execution role for the "' + props.dbName + '" database.', 113 | inlinePolicies: { 114 | "SnapshotExporterLambdaPolicy": aws_iam.PolicyDocument.fromJson({ 115 | "Version": "2012-10-17", 116 | "Statement": [ 117 | { 118 | "Action": [ 119 | "rds:StartExportTask", 120 | "rds:DescribeDBSnapshots" 121 | ], 122 | "Resource": "*", 123 | "Effect": "Allow", 124 | }, 125 | { 126 | "Action": "iam:PassRole", 127 | "Resource": [snapshotExportTaskRole.roleArn], 128 | "Effect": "Allow", 129 | } 130 | ] 131 | }) 132 | }, 133 | managedPolicies: [ 134 | aws_iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole"), 135 | ], 136 | }); 137 | 138 | const snapshotExportGlueCrawlerRole = new aws_iam.Role(this, "SnapshotExportsGlueCrawlerRole", { 139 | assumedBy: new aws_iam.ServicePrincipal("glue.amazonaws.com"), 140 | description: "Role used by RDS to perform snapshot exports to S3", 141 | inlinePolicies: { 142 | "SnapshotExportsGlueCrawlerPolicy": aws_iam.PolicyDocument.fromJson({ 143 | "Version": "2012-10-17", 144 | "Statement": [ 145 | { 146 | "Effect": "Allow", 147 | "Action": [ 148 | "s3:GetObject", 149 | "s3:PutObject" 150 | ], 151 | "Resource": `${bucket.bucketArn}/*`, 152 | } 153 | ], 154 | }), 155 | }, 156 | managedPolicies: [ 157 | aws_iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSGlueServiceRole"), 158 | ], 159 | }); 160 | 161 | const snapshotExportEncryptionKey = new aws_kms.Key(this, "SnapshotExportEncryptionKey", { 162 | alias: props.dbName + "-snapshot-exports", 163 | policy: aws_iam.PolicyDocument.fromJson({ 164 | "Version": "2012-10-17", 165 | "Statement": [ 166 | { 167 | "Principal": { 168 | "AWS": [ 169 | (new aws_iam.AccountRootPrincipal()).arn 170 | ] 171 | }, 172 | "Action": [ 173 | "kms:*" 174 | ], 175 | "Resource": "*", 176 | "Effect": "Allow" 177 | }, 178 | { 179 | "Principal": { 180 | "AWS": [ 181 | lambdaExecutionRole.roleArn, 182 | snapshotExportGlueCrawlerRole.roleArn 183 | ] 184 | }, 185 | "Action": [ 186 | "kms:Encrypt", 187 | "kms:Decrypt", 188 | "kms:ReEncrypt*", 189 | "kms:GenerateDataKey*", 190 | "kms:DescribeKey" 191 | ], 192 | "Resource": "*", 193 | "Effect": "Allow" 194 | }, 195 | { 196 | "Principal": { "AWS": lambdaExecutionRole.roleArn }, 197 | "Action": [ 198 | "kms:CreateGrant", 199 | "kms:ListGrants", 200 | "kms:RevokeGrant" 201 | ], 202 | "Resource": "*", 203 | "Condition": { 204 | "Bool": { "kms:GrantIsForAWSResource": true } 205 | }, 206 | "Effect": "Allow" 207 | } 208 | ] 209 | }) 210 | }); 211 | 212 | const snapshotEventTopic = new aws_sns.Topic(this, "SnapshotEventTopic", { 213 | displayName: "rds-snapshot-creation" 214 | }); 215 | 216 | // Creates the appropriate RDS Event Subscription for RDS or Aurora clusters, to catch snapshot creation events 217 | props.rdsEvents.find(rdsEvent => 218 | rdsEvent.rdsEventId == RdsEventId.DB_AUTOMATED_AURORA_SNAPSHOT_CREATED) ? 219 | new aws_rds.CfnEventSubscription(this, 'RdsSnapshotEventNotification', { 220 | snsTopicArn: snapshotEventTopic.topicArn, 221 | enabled: true, 222 | eventCategories: ['backup'], 223 | sourceType: 'db-cluster-snapshot', 224 | }) : 225 | new aws_rds.CfnEventSubscription(this, 'RdsSnapshotEventNotification', { 226 | snsTopicArn: snapshotEventTopic.topicArn, 227 | enabled: true, 228 | eventCategories: ['creation'], 229 | sourceType: 'db-snapshot', 230 | } 231 | ); 232 | 233 | // With AWS Backup Service, if a prior recent snapshot exists (if created by the Automated snapshot) 234 | // the serivce will simply copy the existing snapshot, and trigger another notification 235 | props.rdsEvents.find(rdsEvent => 236 | rdsEvent.rdsEventId == RdsEventId.DB_BACKUP_SNAPSHOT_FINISHED_COPY) ? 237 | new aws_rds.CfnEventSubscription(this, 'RdsBackupCopyEventNotification', { 238 | snsTopicArn: snapshotEventTopic.topicArn, 239 | enabled: true, 240 | eventCategories: ['notification'], 241 | sourceType: 'db-snapshot', 242 | } 243 | ) : true; 244 | 245 | new aws_lambda.Function(this, "LambdaFunction", { 246 | functionName: props.dbName + "-rds-snapshot-exporter", 247 | runtime: aws_lambda.Runtime.PYTHON_3_8, 248 | handler: "main.handler", 249 | code: aws_lambda.Code.fromAsset(path.join(__dirname, "/../assets/exporter/")), 250 | environment: { 251 | RDS_EVENT_IDS: new Array(props.rdsEvents.map(e => { return e.rdsEventId })).join(), 252 | RDS_SNAPSHOT_TYPES: new Array(props.rdsEvents.map(e => { return e.rdsSnapshotType })).join(), 253 | DB_NAME: props.dbName, 254 | LOG_LEVEL: "INFO", 255 | SNAPSHOT_BUCKET_NAME: bucket.bucketName, 256 | SNAPSHOT_TASK_ROLE: snapshotExportTaskRole.roleArn, 257 | SNAPSHOT_TASK_KEY: snapshotExportEncryptionKey.keyArn, 258 | DB_SNAPSHOT_TYPES: new Array(props.rdsEvents.map(e => { return e.rdsEventId == RdsEventId.DB_AUTOMATED_AURORA_SNAPSHOT_CREATED ? "cluster-snapshot" : "snapshot" })).join() 259 | }, 260 | role: lambdaExecutionRole, 261 | timeout: Duration.seconds(30), 262 | events: [ 263 | new aws_lambda_event_sources.SnsEventSource(snapshotEventTopic) 264 | ] 265 | }); 266 | 267 | new aws_glue.CfnCrawler(this, "SnapshotExportCrawler", { 268 | name: props.dbName + "-rds-snapshot-crawler", 269 | role: snapshotExportGlueCrawlerRole.roleArn, 270 | targets: { 271 | s3Targets: [ 272 | {path: bucket.bucketName}, 273 | ] 274 | }, 275 | databaseName: props.dbName.replace(/[^a-zA-Z0-9_]/g, "_"), 276 | schemaChangePolicy: { 277 | deleteBehavior: 'DELETE_FROM_DATABASE' 278 | } 279 | }); 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rds-snapshot-export-to-s3-pipeline", 3 | "version": "0.2.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "rds-snapshot-export-to-s3-pipeline", 9 | "version": "0.2.0", 10 | "dependencies": { 11 | "@types/node": "13.9.1", 12 | "aws-cdk-lib": "^2.186.0", 13 | "constructs": "^10.2.7", 14 | "source-map-support": "^0.5.16", 15 | "ts-node": "^8.6.2", 16 | "typescript": "~3.8.3" 17 | }, 18 | "bin": { 19 | "cdk": "bin/cdk.js" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^13.13.52", 23 | "ts-node": "^8.10.2", 24 | "typescript": "~3.8.3" 25 | } 26 | }, 27 | "node_modules/@aws-cdk/asset-awscli-v1": { 28 | "version": "2.2.230", 29 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.230.tgz", 30 | "integrity": "sha512-kUnhKIYu42hqBa6a8x2/7o29ObpJgjYGQy28lZDq9awXyvpR62I2bRxrNKNR3uFUQz3ySuT9JXhGHhuZPdbnFw==", 31 | "license": "Apache-2.0" 32 | }, 33 | "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { 34 | "version": "2.1.0", 35 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", 36 | "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", 37 | "license": "Apache-2.0" 38 | }, 39 | "node_modules/@types/node": { 40 | "version": "13.13.52", 41 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", 42 | "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", 43 | "dev": true 44 | }, 45 | "node_modules/arg": { 46 | "version": "4.1.3", 47 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 48 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 49 | "dev": true 50 | }, 51 | "node_modules/aws-cdk-lib": { 52 | "version": "2.186.0", 53 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.186.0.tgz", 54 | "integrity": "sha512-y/DD4h8CbhwGyPTpoHELATavZe5FWcy1xSuLlReOd3+cCRZ9rAzVSFdPB8kSJUD4nBPrIeGkW1u8ItUOhms17w==", 55 | "bundleDependencies": [ 56 | "@balena/dockerignore", 57 | "case", 58 | "fs-extra", 59 | "ignore", 60 | "jsonschema", 61 | "minimatch", 62 | "punycode", 63 | "semver", 64 | "table", 65 | "yaml", 66 | "mime-types" 67 | ], 68 | "license": "Apache-2.0", 69 | "dependencies": { 70 | "@aws-cdk/asset-awscli-v1": "^2.2.227", 71 | "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", 72 | "@aws-cdk/cloud-assembly-schema": "^40.7.0", 73 | "@balena/dockerignore": "^1.0.2", 74 | "case": "1.6.3", 75 | "fs-extra": "^11.3.0", 76 | "ignore": "^5.3.2", 77 | "jsonschema": "^1.5.0", 78 | "mime-types": "^2.1.35", 79 | "minimatch": "^3.1.2", 80 | "punycode": "^2.3.1", 81 | "semver": "^7.7.1", 82 | "table": "^6.9.0", 83 | "yaml": "1.10.2" 84 | }, 85 | "engines": { 86 | "node": ">= 14.15.0" 87 | }, 88 | "peerDependencies": { 89 | "constructs": "^10.0.0" 90 | } 91 | }, 92 | "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema": { 93 | "version": "40.7.0", 94 | "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", 95 | "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", 96 | "bundleDependencies": [ 97 | "jsonschema", 98 | "semver" 99 | ], 100 | "license": "Apache-2.0", 101 | "dependencies": { 102 | "jsonschema": "~1.4.1", 103 | "semver": "^7.7.1" 104 | }, 105 | "engines": { 106 | "node": ">= 14.15.0" 107 | } 108 | }, 109 | "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { 110 | "version": "1.4.1", 111 | "inBundle": true, 112 | "license": "MIT", 113 | "engines": { 114 | "node": "*" 115 | } 116 | }, 117 | "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { 118 | "version": "7.7.1", 119 | "inBundle": true, 120 | "license": "ISC", 121 | "bin": { 122 | "semver": "bin/semver.js" 123 | }, 124 | "engines": { 125 | "node": ">=10" 126 | } 127 | }, 128 | "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { 129 | "version": "1.0.2", 130 | "inBundle": true, 131 | "license": "Apache-2.0" 132 | }, 133 | "node_modules/aws-cdk-lib/node_modules/ajv": { 134 | "version": "8.17.1", 135 | "inBundle": true, 136 | "license": "MIT", 137 | "dependencies": { 138 | "fast-deep-equal": "^3.1.3", 139 | "fast-uri": "^3.0.1", 140 | "json-schema-traverse": "^1.0.0", 141 | "require-from-string": "^2.0.2" 142 | }, 143 | "funding": { 144 | "type": "github", 145 | "url": "https://github.com/sponsors/epoberezkin" 146 | } 147 | }, 148 | "node_modules/aws-cdk-lib/node_modules/ansi-regex": { 149 | "version": "5.0.1", 150 | "inBundle": true, 151 | "license": "MIT", 152 | "engines": { 153 | "node": ">=8" 154 | } 155 | }, 156 | "node_modules/aws-cdk-lib/node_modules/ansi-styles": { 157 | "version": "4.3.0", 158 | "inBundle": true, 159 | "license": "MIT", 160 | "dependencies": { 161 | "color-convert": "^2.0.1" 162 | }, 163 | "engines": { 164 | "node": ">=8" 165 | }, 166 | "funding": { 167 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 168 | } 169 | }, 170 | "node_modules/aws-cdk-lib/node_modules/astral-regex": { 171 | "version": "2.0.0", 172 | "inBundle": true, 173 | "license": "MIT", 174 | "engines": { 175 | "node": ">=8" 176 | } 177 | }, 178 | "node_modules/aws-cdk-lib/node_modules/balanced-match": { 179 | "version": "1.0.2", 180 | "inBundle": true, 181 | "license": "MIT" 182 | }, 183 | "node_modules/aws-cdk-lib/node_modules/brace-expansion": { 184 | "version": "1.1.11", 185 | "inBundle": true, 186 | "license": "MIT", 187 | "dependencies": { 188 | "balanced-match": "^1.0.0", 189 | "concat-map": "0.0.1" 190 | } 191 | }, 192 | "node_modules/aws-cdk-lib/node_modules/case": { 193 | "version": "1.6.3", 194 | "inBundle": true, 195 | "license": "(MIT OR GPL-3.0-or-later)", 196 | "engines": { 197 | "node": ">= 0.8.0" 198 | } 199 | }, 200 | "node_modules/aws-cdk-lib/node_modules/color-convert": { 201 | "version": "2.0.1", 202 | "inBundle": true, 203 | "license": "MIT", 204 | "dependencies": { 205 | "color-name": "~1.1.4" 206 | }, 207 | "engines": { 208 | "node": ">=7.0.0" 209 | } 210 | }, 211 | "node_modules/aws-cdk-lib/node_modules/color-name": { 212 | "version": "1.1.4", 213 | "inBundle": true, 214 | "license": "MIT" 215 | }, 216 | "node_modules/aws-cdk-lib/node_modules/concat-map": { 217 | "version": "0.0.1", 218 | "inBundle": true, 219 | "license": "MIT" 220 | }, 221 | "node_modules/aws-cdk-lib/node_modules/emoji-regex": { 222 | "version": "8.0.0", 223 | "inBundle": true, 224 | "license": "MIT" 225 | }, 226 | "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { 227 | "version": "3.1.3", 228 | "inBundle": true, 229 | "license": "MIT" 230 | }, 231 | "node_modules/aws-cdk-lib/node_modules/fast-uri": { 232 | "version": "3.0.6", 233 | "funding": [ 234 | { 235 | "type": "github", 236 | "url": "https://github.com/sponsors/fastify" 237 | }, 238 | { 239 | "type": "opencollective", 240 | "url": "https://opencollective.com/fastify" 241 | } 242 | ], 243 | "inBundle": true, 244 | "license": "BSD-3-Clause" 245 | }, 246 | "node_modules/aws-cdk-lib/node_modules/fs-extra": { 247 | "version": "11.3.0", 248 | "inBundle": true, 249 | "license": "MIT", 250 | "dependencies": { 251 | "graceful-fs": "^4.2.0", 252 | "jsonfile": "^6.0.1", 253 | "universalify": "^2.0.0" 254 | }, 255 | "engines": { 256 | "node": ">=14.14" 257 | } 258 | }, 259 | "node_modules/aws-cdk-lib/node_modules/graceful-fs": { 260 | "version": "4.2.11", 261 | "inBundle": true, 262 | "license": "ISC" 263 | }, 264 | "node_modules/aws-cdk-lib/node_modules/ignore": { 265 | "version": "5.3.2", 266 | "inBundle": true, 267 | "license": "MIT", 268 | "engines": { 269 | "node": ">= 4" 270 | } 271 | }, 272 | "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { 273 | "version": "3.0.0", 274 | "inBundle": true, 275 | "license": "MIT", 276 | "engines": { 277 | "node": ">=8" 278 | } 279 | }, 280 | "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { 281 | "version": "1.0.0", 282 | "inBundle": true, 283 | "license": "MIT" 284 | }, 285 | "node_modules/aws-cdk-lib/node_modules/jsonfile": { 286 | "version": "6.1.0", 287 | "inBundle": true, 288 | "license": "MIT", 289 | "dependencies": { 290 | "universalify": "^2.0.0" 291 | }, 292 | "optionalDependencies": { 293 | "graceful-fs": "^4.1.6" 294 | } 295 | }, 296 | "node_modules/aws-cdk-lib/node_modules/jsonschema": { 297 | "version": "1.5.0", 298 | "inBundle": true, 299 | "license": "MIT", 300 | "engines": { 301 | "node": "*" 302 | } 303 | }, 304 | "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { 305 | "version": "4.4.2", 306 | "inBundle": true, 307 | "license": "MIT" 308 | }, 309 | "node_modules/aws-cdk-lib/node_modules/mime-db": { 310 | "version": "1.52.0", 311 | "inBundle": true, 312 | "license": "MIT", 313 | "engines": { 314 | "node": ">= 0.6" 315 | } 316 | }, 317 | "node_modules/aws-cdk-lib/node_modules/mime-types": { 318 | "version": "2.1.35", 319 | "inBundle": true, 320 | "license": "MIT", 321 | "dependencies": { 322 | "mime-db": "1.52.0" 323 | }, 324 | "engines": { 325 | "node": ">= 0.6" 326 | } 327 | }, 328 | "node_modules/aws-cdk-lib/node_modules/minimatch": { 329 | "version": "3.1.2", 330 | "inBundle": true, 331 | "license": "ISC", 332 | "dependencies": { 333 | "brace-expansion": "^1.1.7" 334 | }, 335 | "engines": { 336 | "node": "*" 337 | } 338 | }, 339 | "node_modules/aws-cdk-lib/node_modules/punycode": { 340 | "version": "2.3.1", 341 | "inBundle": true, 342 | "license": "MIT", 343 | "engines": { 344 | "node": ">=6" 345 | } 346 | }, 347 | "node_modules/aws-cdk-lib/node_modules/require-from-string": { 348 | "version": "2.0.2", 349 | "inBundle": true, 350 | "license": "MIT", 351 | "engines": { 352 | "node": ">=0.10.0" 353 | } 354 | }, 355 | "node_modules/aws-cdk-lib/node_modules/semver": { 356 | "version": "7.7.1", 357 | "inBundle": true, 358 | "license": "ISC", 359 | "bin": { 360 | "semver": "bin/semver.js" 361 | }, 362 | "engines": { 363 | "node": ">=10" 364 | } 365 | }, 366 | "node_modules/aws-cdk-lib/node_modules/slice-ansi": { 367 | "version": "4.0.0", 368 | "inBundle": true, 369 | "license": "MIT", 370 | "dependencies": { 371 | "ansi-styles": "^4.0.0", 372 | "astral-regex": "^2.0.0", 373 | "is-fullwidth-code-point": "^3.0.0" 374 | }, 375 | "engines": { 376 | "node": ">=10" 377 | }, 378 | "funding": { 379 | "url": "https://github.com/chalk/slice-ansi?sponsor=1" 380 | } 381 | }, 382 | "node_modules/aws-cdk-lib/node_modules/string-width": { 383 | "version": "4.2.3", 384 | "inBundle": true, 385 | "license": "MIT", 386 | "dependencies": { 387 | "emoji-regex": "^8.0.0", 388 | "is-fullwidth-code-point": "^3.0.0", 389 | "strip-ansi": "^6.0.1" 390 | }, 391 | "engines": { 392 | "node": ">=8" 393 | } 394 | }, 395 | "node_modules/aws-cdk-lib/node_modules/strip-ansi": { 396 | "version": "6.0.1", 397 | "inBundle": true, 398 | "license": "MIT", 399 | "dependencies": { 400 | "ansi-regex": "^5.0.1" 401 | }, 402 | "engines": { 403 | "node": ">=8" 404 | } 405 | }, 406 | "node_modules/aws-cdk-lib/node_modules/table": { 407 | "version": "6.9.0", 408 | "inBundle": true, 409 | "license": "BSD-3-Clause", 410 | "dependencies": { 411 | "ajv": "^8.0.1", 412 | "lodash.truncate": "^4.4.2", 413 | "slice-ansi": "^4.0.0", 414 | "string-width": "^4.2.3", 415 | "strip-ansi": "^6.0.1" 416 | }, 417 | "engines": { 418 | "node": ">=10.0.0" 419 | } 420 | }, 421 | "node_modules/aws-cdk-lib/node_modules/universalify": { 422 | "version": "2.0.1", 423 | "inBundle": true, 424 | "license": "MIT", 425 | "engines": { 426 | "node": ">= 10.0.0" 427 | } 428 | }, 429 | "node_modules/aws-cdk-lib/node_modules/yaml": { 430 | "version": "1.10.2", 431 | "inBundle": true, 432 | "license": "ISC", 433 | "engines": { 434 | "node": ">= 6" 435 | } 436 | }, 437 | "node_modules/buffer-from": { 438 | "version": "1.1.1", 439 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 440 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 441 | }, 442 | "node_modules/constructs": { 443 | "version": "10.2.7", 444 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.2.7.tgz", 445 | "integrity": "sha512-Cn8bZkZMK/jdeyoobnR/M48/+SSgCHe6nNTJXtbzu/dLaK+HiE6JSSjhtb9OO2jO/ZysZ1dPVUrzKs7HGZ7PUw==", 446 | "engines": { 447 | "node": ">= 14.17.0" 448 | } 449 | }, 450 | "node_modules/diff": { 451 | "version": "4.0.2", 452 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 453 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 454 | "dev": true, 455 | "engines": { 456 | "node": ">=0.3.1" 457 | } 458 | }, 459 | "node_modules/make-error": { 460 | "version": "1.3.6", 461 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 462 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 463 | "dev": true 464 | }, 465 | "node_modules/source-map": { 466 | "version": "0.6.1", 467 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 468 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 469 | "engines": { 470 | "node": ">=0.10.0" 471 | } 472 | }, 473 | "node_modules/source-map-support": { 474 | "version": "0.5.21", 475 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 476 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 477 | "dependencies": { 478 | "buffer-from": "^1.0.0", 479 | "source-map": "^0.6.0" 480 | } 481 | }, 482 | "node_modules/ts-node": { 483 | "version": "8.10.2", 484 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", 485 | "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", 486 | "dev": true, 487 | "dependencies": { 488 | "arg": "^4.1.0", 489 | "diff": "^4.0.1", 490 | "make-error": "^1.1.1", 491 | "source-map-support": "^0.5.17", 492 | "yn": "3.1.1" 493 | }, 494 | "bin": { 495 | "ts-node": "dist/bin.js", 496 | "ts-node-script": "dist/bin-script.js", 497 | "ts-node-transpile-only": "dist/bin-transpile.js", 498 | "ts-script": "dist/bin-script-deprecated.js" 499 | }, 500 | "engines": { 501 | "node": ">=6.0.0" 502 | }, 503 | "peerDependencies": { 504 | "typescript": ">=2.7" 505 | } 506 | }, 507 | "node_modules/typescript": { 508 | "version": "3.8.3", 509 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 510 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 511 | "dev": true, 512 | "bin": { 513 | "tsc": "bin/tsc", 514 | "tsserver": "bin/tsserver" 515 | }, 516 | "engines": { 517 | "node": ">=4.2.0" 518 | } 519 | }, 520 | "node_modules/yn": { 521 | "version": "3.1.1", 522 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 523 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 524 | "dev": true, 525 | "engines": { 526 | "node": ">=6" 527 | } 528 | } 529 | }, 530 | "dependencies": { 531 | "@aws-cdk/asset-awscli-v1": { 532 | "version": "2.2.230", 533 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.230.tgz", 534 | "integrity": "sha512-kUnhKIYu42hqBa6a8x2/7o29ObpJgjYGQy28lZDq9awXyvpR62I2bRxrNKNR3uFUQz3ySuT9JXhGHhuZPdbnFw==" 535 | }, 536 | "@aws-cdk/asset-node-proxy-agent-v6": { 537 | "version": "2.1.0", 538 | "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", 539 | "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==" 540 | }, 541 | "@types/node": { 542 | "version": "13.13.52", 543 | "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", 544 | "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==", 545 | "dev": true 546 | }, 547 | "arg": { 548 | "version": "4.1.3", 549 | "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", 550 | "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", 551 | "dev": true 552 | }, 553 | "aws-cdk-lib": { 554 | "version": "2.186.0", 555 | "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.186.0.tgz", 556 | "integrity": "sha512-y/DD4h8CbhwGyPTpoHELATavZe5FWcy1xSuLlReOd3+cCRZ9rAzVSFdPB8kSJUD4nBPrIeGkW1u8ItUOhms17w==", 557 | "requires": { 558 | "@aws-cdk/asset-awscli-v1": "^2.2.227", 559 | "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", 560 | "@aws-cdk/cloud-assembly-schema": "^40.7.0", 561 | "@balena/dockerignore": "^1.0.2", 562 | "case": "1.6.3", 563 | "fs-extra": "^11.3.0", 564 | "ignore": "^5.3.2", 565 | "jsonschema": "^1.5.0", 566 | "mime-types": "^2.1.35", 567 | "minimatch": "^3.1.2", 568 | "punycode": "^2.3.1", 569 | "semver": "^7.7.1", 570 | "table": "^6.9.0", 571 | "yaml": "1.10.2" 572 | }, 573 | "dependencies": { 574 | "@aws-cdk/cloud-assembly-schema": { 575 | "version": "40.7.0", 576 | "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-40.7.0.tgz", 577 | "integrity": "sha512-00wVKn9pOOGXbeNwA4E8FUFt0zIB4PmSO7PvIiDWgpaFX3G/sWyy0A3s6bg/n2Yvkghu8r4a8ckm+mAzkAYmfA==", 578 | "requires": { 579 | "jsonschema": "~1.4.1", 580 | "semver": "^7.7.1" 581 | }, 582 | "dependencies": { 583 | "jsonschema": { 584 | "version": "1.4.1", 585 | "bundled": true 586 | }, 587 | "semver": { 588 | "version": "7.7.1", 589 | "bundled": true 590 | } 591 | } 592 | }, 593 | "@balena/dockerignore": { 594 | "version": "1.0.2", 595 | "bundled": true 596 | }, 597 | "ajv": { 598 | "version": "8.17.1", 599 | "bundled": true, 600 | "requires": { 601 | "fast-deep-equal": "^3.1.3", 602 | "fast-uri": "^3.0.1", 603 | "json-schema-traverse": "^1.0.0", 604 | "require-from-string": "^2.0.2" 605 | } 606 | }, 607 | "ansi-regex": { 608 | "version": "5.0.1", 609 | "bundled": true 610 | }, 611 | "ansi-styles": { 612 | "version": "4.3.0", 613 | "bundled": true, 614 | "requires": { 615 | "color-convert": "^2.0.1" 616 | } 617 | }, 618 | "astral-regex": { 619 | "version": "2.0.0", 620 | "bundled": true 621 | }, 622 | "balanced-match": { 623 | "version": "1.0.2", 624 | "bundled": true 625 | }, 626 | "brace-expansion": { 627 | "version": "1.1.11", 628 | "bundled": true, 629 | "requires": { 630 | "balanced-match": "^1.0.0", 631 | "concat-map": "0.0.1" 632 | } 633 | }, 634 | "case": { 635 | "version": "1.6.3", 636 | "bundled": true 637 | }, 638 | "color-convert": { 639 | "version": "2.0.1", 640 | "bundled": true, 641 | "requires": { 642 | "color-name": "~1.1.4" 643 | } 644 | }, 645 | "color-name": { 646 | "version": "1.1.4", 647 | "bundled": true 648 | }, 649 | "concat-map": { 650 | "version": "0.0.1", 651 | "bundled": true 652 | }, 653 | "emoji-regex": { 654 | "version": "8.0.0", 655 | "bundled": true 656 | }, 657 | "fast-deep-equal": { 658 | "version": "3.1.3", 659 | "bundled": true 660 | }, 661 | "fast-uri": { 662 | "version": "3.0.6", 663 | "bundled": true 664 | }, 665 | "fs-extra": { 666 | "version": "11.3.0", 667 | "bundled": true, 668 | "requires": { 669 | "graceful-fs": "^4.2.0", 670 | "jsonfile": "^6.0.1", 671 | "universalify": "^2.0.0" 672 | } 673 | }, 674 | "graceful-fs": { 675 | "version": "4.2.11", 676 | "bundled": true 677 | }, 678 | "ignore": { 679 | "version": "5.3.2", 680 | "bundled": true 681 | }, 682 | "is-fullwidth-code-point": { 683 | "version": "3.0.0", 684 | "bundled": true 685 | }, 686 | "json-schema-traverse": { 687 | "version": "1.0.0", 688 | "bundled": true 689 | }, 690 | "jsonfile": { 691 | "version": "6.1.0", 692 | "bundled": true, 693 | "requires": { 694 | "graceful-fs": "^4.1.6", 695 | "universalify": "^2.0.0" 696 | } 697 | }, 698 | "jsonschema": { 699 | "version": "1.5.0", 700 | "bundled": true 701 | }, 702 | "lodash.truncate": { 703 | "version": "4.4.2", 704 | "bundled": true 705 | }, 706 | "mime-db": { 707 | "version": "1.52.0", 708 | "bundled": true 709 | }, 710 | "mime-types": { 711 | "version": "2.1.35", 712 | "bundled": true, 713 | "requires": { 714 | "mime-db": "1.52.0" 715 | } 716 | }, 717 | "minimatch": { 718 | "version": "3.1.2", 719 | "bundled": true, 720 | "requires": { 721 | "brace-expansion": "^1.1.7" 722 | } 723 | }, 724 | "punycode": { 725 | "version": "2.3.1", 726 | "bundled": true 727 | }, 728 | "require-from-string": { 729 | "version": "2.0.2", 730 | "bundled": true 731 | }, 732 | "semver": { 733 | "version": "7.7.1", 734 | "bundled": true 735 | }, 736 | "slice-ansi": { 737 | "version": "4.0.0", 738 | "bundled": true, 739 | "requires": { 740 | "ansi-styles": "^4.0.0", 741 | "astral-regex": "^2.0.0", 742 | "is-fullwidth-code-point": "^3.0.0" 743 | } 744 | }, 745 | "string-width": { 746 | "version": "4.2.3", 747 | "bundled": true, 748 | "requires": { 749 | "emoji-regex": "^8.0.0", 750 | "is-fullwidth-code-point": "^3.0.0", 751 | "strip-ansi": "^6.0.1" 752 | } 753 | }, 754 | "strip-ansi": { 755 | "version": "6.0.1", 756 | "bundled": true, 757 | "requires": { 758 | "ansi-regex": "^5.0.1" 759 | } 760 | }, 761 | "table": { 762 | "version": "6.9.0", 763 | "bundled": true, 764 | "requires": { 765 | "ajv": "^8.0.1", 766 | "lodash.truncate": "^4.4.2", 767 | "slice-ansi": "^4.0.0", 768 | "string-width": "^4.2.3", 769 | "strip-ansi": "^6.0.1" 770 | } 771 | }, 772 | "universalify": { 773 | "version": "2.0.1", 774 | "bundled": true 775 | }, 776 | "yaml": { 777 | "version": "1.10.2", 778 | "bundled": true 779 | } 780 | } 781 | }, 782 | "buffer-from": { 783 | "version": "1.1.1", 784 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", 785 | "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" 786 | }, 787 | "constructs": { 788 | "version": "10.2.7", 789 | "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.2.7.tgz", 790 | "integrity": "sha512-Cn8bZkZMK/jdeyoobnR/M48/+SSgCHe6nNTJXtbzu/dLaK+HiE6JSSjhtb9OO2jO/ZysZ1dPVUrzKs7HGZ7PUw==" 791 | }, 792 | "diff": { 793 | "version": "4.0.2", 794 | "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", 795 | "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", 796 | "dev": true 797 | }, 798 | "make-error": { 799 | "version": "1.3.6", 800 | "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", 801 | "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", 802 | "dev": true 803 | }, 804 | "source-map": { 805 | "version": "0.6.1", 806 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 807 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" 808 | }, 809 | "source-map-support": { 810 | "version": "0.5.21", 811 | "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", 812 | "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", 813 | "requires": { 814 | "buffer-from": "^1.0.0", 815 | "source-map": "^0.6.0" 816 | } 817 | }, 818 | "ts-node": { 819 | "version": "8.10.2", 820 | "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.10.2.tgz", 821 | "integrity": "sha512-ISJJGgkIpDdBhWVu3jufsWpK3Rzo7bdiIXJjQc0ynKxVOVcg2oIrf2H2cejminGrptVc6q6/uynAHNCuWGbpVA==", 822 | "dev": true, 823 | "requires": { 824 | "arg": "^4.1.0", 825 | "diff": "^4.0.1", 826 | "make-error": "^1.1.1", 827 | "source-map-support": "^0.5.17", 828 | "yn": "3.1.1" 829 | } 830 | }, 831 | "typescript": { 832 | "version": "3.8.3", 833 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz", 834 | "integrity": "sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w==", 835 | "dev": true 836 | }, 837 | "yn": { 838 | "version": "3.1.1", 839 | "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", 840 | "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", 841 | "dev": true 842 | } 843 | } 844 | } 845 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rds-snapshot-export-to-s3-pipeline", 3 | "version": "0.2.0", 4 | "bin": { 5 | "cdk": "bin/cdk.js" 6 | }, 7 | "scripts": { 8 | "build": "tsc", 9 | "watch": "tsc -w", 10 | "cdk": "cdk" 11 | }, 12 | "devDependencies": { 13 | "@types/node": "^13.13.52", 14 | "ts-node": "^8.10.2", 15 | "typescript": "~3.8.3" 16 | }, 17 | "dependencies": { 18 | "@types/node": "13.9.1", 19 | "ts-node": "^8.6.2", 20 | "typescript": "~3.8.3", 21 | "aws-cdk-lib": "^2.186.0", 22 | "constructs": "^10.2.7", 23 | "source-map-support": "^0.5.16" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target":"ES2018", 4 | "module": "commonjs", 5 | "lib": ["es2018"], 6 | "declaration": true, 7 | "strict": true, 8 | "noImplicitAny": true, 9 | "strictNullChecks": true, 10 | "noImplicitThis": true, 11 | "alwaysStrict": true, 12 | "noUnusedLocals": false, 13 | "noUnusedParameters": false, 14 | "noImplicitReturns": true, 15 | "noFallthroughCasesInSwitch": false, 16 | "inlineSourceMap": true, 17 | "inlineSources": true, 18 | "experimentalDecorators": true, 19 | "strictPropertyInitialization":false, 20 | "typeRoots": ["./node_modules/@types"] 21 | }, 22 | "exclude": ["cdk.out"] 23 | } 24 | --------------------------------------------------------------------------------