diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f0c2b8b --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +.DS_Store \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 8dada3e..0000000 --- a/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 8dada3e..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/NOTICE b/NOTICE deleted file mode 100644 index 65146ef..0000000 --- a/NOTICE +++ /dev/null @@ -1,2 +0,0 @@ -RDS Snapshot Tool -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/README.md b/README.md index c635a1e..1a0674d 100644 --- a/README.md +++ b/README.md @@ -1,110 +1,128 @@ -# Snapshot Tool for Amazon RDS - -The Snapshot Tool for RDS automates the task of creating manual snapshots, copying them into a different account and a different region, and deleting them after a specified number of days. It also allows you to specify the backup schedule (at what times and how often) and a retention period in days. This version will work with all Amazon RDS instances except Amazon Aurora. For a version that works with Amazon Aurora, please visit the [Snapshot Tool for Amazon Aurora](https://github.com/awslabs/aurora-snapshot-tool). - -**IMPORTANT** Run the Cloudformation templates on the same **region** where your RDS instances run (both in the source and destination accounts). If that is not possible because AWS Step Functions is not available, you will need to use the **SourceRegionOverride** parameter explained below. - - -## Getting Started - - -### Building From Source and Deploying - -You will need to build from source and deploy to your own bucket in your own account. To build, you need to be on a unix-like system (e.g., macOS or some flavour of Linux) and you need to have `make` and `zip`. - -1. Create an S3 bucket to hold the Lambda function zip files. The bucket must be in the same region where the Lambda functions will run. And the Lambda functions must run in the same region as the RDS instances. - -1. Clone the repository - -1. Edit the `Makefile` file and set `S3DEST` to be the bucket name where you want the functions to go. Set the `AWSARGS`, `AWSCMD` and `ZIPCMD` variables as well. - -1. Type `make` at the command line. It will call `zip` to make the zip files, and then it will call `aws s3 cp` to copy the zip files to the bucket you named. - -1. Be sure to use the correct bucket name in the `CodeBucket` parameter when launching the stack in both accounts. - - -To deploy on your accounts, you will need to use the Cloudformation templates provided. -* Deploy `snapshot_tool_rds_source.json` in the source account (the account that runs the RDS instances) -* Deploy `snapshot_tool_rds_dest.json` in the destination account (the account where you'd like to keep your snapshots) - - -### Source Account -#### Components -The following components will be created in the source account: -* 3 Lambda functions (TakeSnapshotsRDS, ShareSnapshotsRDS, DeleteOldSnapshotsRDS) -* 3 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineTakeSnapshotRDS, stateMachineShareSnapshotRDS, stateMachineDeleteOldSnapshotsRDS) -* 3 Cloudwatch Event Rules to trigger the state functions -* 3 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures -* A Cloudformation stack containing all these resources - -#### Installing in the source account -Run snapshot_tool_RDS_source.json on the Cloudformation console. -You wil need to specify the different parameters. The default values will back up all RDS instances in the region at 1AM UTC, once a day. -If your instances are encrypted, you will need to provide access to the KMS Key to the destination account. You can read more on how to do that here: https://aws.amazon.com/premiumsupport/knowledge-center/share-cmk-account/ - -Here is a break down of each parameter for the source template: - -* **BackupInterval** - how many hours between backup -* **BackupSchedule** - at what times and how often to run backups. Set in accordance with **BackupInterval**. For example, set **BackupInterval** to 8 hours and **BackupSchedule** 0 0,8,16 * * ? * if you want backups to run at 0, 8 and 16 UTC. If your backups run more often than **BackupInterval**, snapshots will only be created when the latest snapshot is older than **BackupInterval**. If you set BackupInterval to more than once a day, make sure to adjust BackupSchedule accordingly or backups will only be taken at the times specified in the CRON expression. -* **InstanceNamePattern** - set to the names of the instances you want this tool to back up. You can use a Python regex that will be searched in the instance identifier. For example, if your instances are named *prod-01*, *prod-02*, etc, you can set **InstanceNamePattern** to *prod*. The string you specify will be searched anywhere in the name unless you use an anchor such as ^ or $. In most cases, a simple name like "prod" or "dev" will suffice. More information on Python regular expressions here: https://docs.python.org/2/howto/regex.html -* **DestinationAccount** - the account where you want snapshots to be copied to -* **LogLevel** - The log level you want as output to the Lambda functions. ERROR is usually enough. You can increase to INFO or DEBUG. -* **RetentionDays** - the amount of days you want your snapshots to be kept. Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CreatedBy, Value: Snapshot Tool for RDS) -* **ShareSnapshots** - Set to TRUE if you are sharing snapshots with a different account. If you set to FALSE, StateMachine, Lambda functions and associated Cloudwatch Alarms related to sharing across accounts will not be created. It is useful if you only want to take backups and manage the retention, but do not need to copy them across accounts or regions. -* **SourceRegionOverride** - if you are running RDS on a region where Step Functions is not available, this parameter will allow you to override the source region. For example, at the time of this writing, you may be running RDS in Northern California (us-west-1) and would like to copy your snapshots to Montreal (ca-central-1). Neither region supports Step Functions at the time of this writing so deploying this tool there will not work. The solution is to run this template in a region that supports Step Functions (such as North Virginia or Ohio) and set **SourceRegionOverride** to *us-west-1*. -**IMPORTANT**: deploy to the closest regions for best results. - -* **CodeBucket** - this parameter specifies the bucket where the code for the Lambda functions is located. The Lambda function code is located in the ```lambda``` directory in zip format. These files need to be on the **root* of the bucket or the CloudFormation templates will fail. Please follow the instructions to build source (earlier on this README file) -* **DeleteOldSnapshots** - Set to TRUE to enable functionality that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete **all snapshots**, older than **RetentionDays**, created by this tool; not just the ones created after **DeleteOldSnapshots** is set to TRUE. -* **TaggedInstance** - Set to TRUE to enable functionality that will only take snapshots for RDS Instances with tag CopyDBSnapshot set to True. The settings in InstanceNamePattern and TaggedInstance both need to evaluate successfully for a snapshot to be created (logical AND). - -### Destination Account -#### Components -The following components will be created in the destination account: -* 2 Lambda functions (CopySnapshotsDestRDS, DeleteOldSnapshotsDestRDS) -* 2 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineCopySnapshotsDestRDS, stateMachineDeleteOldSnapshotsDestRDS) -* 2 Cloudwatch Event Rules to trigger the state functions -* 2 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures -* A Cloudformation stack containing all these resources - -On your destination account, you will need to run snapshot_tool_RDS_dest.json on the Cloudformation. As before, you will need to run it in a region where Step Functions is available. -The following parameters are available: - -* **DestinationRegion** - the region where you want your snapshots to be copied. If you set it to the same as the source region, the snapshots will be copied from the source account but will be kept in the source region. This is useful if you would like to keep a copy of your snapshots in a different account but would prefer not to copy them to a different region. -* **SnapshotPattern** - similar to InstanceNamePattern. See above -* **DeleteOldSnapshots** - Set to TRUE to enable functionanility that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete ALL SNAPSHOTS older than RetentionDays created by this tool, not just the ones created after **DeleteOldSnapshots** is set to TRUE. -* **CrossAccountCopy** - if you only need to copy snapshots across regions and not to a different account, set this to FALSE. When set to false, the no-x-account version of the Lambda functions will be deployed and will expect snapshots to be in the same account as they run. -* **KmsKeySource** KMS Key to be used for copying encrypted snapshots on the source region. If you are copying to a different region, you will also need to provide a second key in the destination region. -* **KmsKeyDestination** KMS Key to be used for copying encrypted snapshots to the destination region. If you are not copying to a different region, this parameter is not necessary. -* **RetentionDays** - as in the source account, the amount of days you want your snapshots to be kept. **Do not set this parameter to a value lower than the source account.** Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CopiedBy, Value: Snapshot Tool for RDS) - -## How it Works - -There are two sets of Lambda Step Functions that take regular snapshots and copy them across. Snapshots can take time, and they do not signal when they're complete. Snapshots are scheduled to *begin* at a certain time using CloudWatch Events. Then different Lambda Step Functions run periodically to look for new snapshots. When they find new snapshots, they do the sharing and the copying functions. - -### In the Source Account - -A CloudWatch Event is scheduled to trigger Lambda Step Function State Machine named `stateMachineTakeSnapshotsRDS`. That state machine invokes a function named `lambdaTakeSnapshotsRDS`. That function triggers a snapshot and applies some standard tags. It matches RDS instances using a regular expression on their names. - -There are two other state machines and lambda functions. The `statemachineShareSnapshotsRDS` looks for new snapshots created by the `lambdaTakeSnapshotsRDS` function. When it finds them, it shares them with the destination account. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventShareSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a new snapshot that is intended to be shared, it shares the snapshot. - -The other state machine is the `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. - -### In the Destination Account - -There are two state machines and corresponding lambda functions. The `statemachineCopySnapshotsDestRDS` looks for new snapshots that have been shared but have not yet been copied. When it finds them, it creates a copy in the destination account, encrypted with the KMS key that has been stipulated. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventCopySnapshotsRDS` resource in `snapshots_tool_rds_dest.json`). - -The other state machine is just like the corresponding state machine and function in the source account. The state machine is `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. - - -## Updating - -This tool is fundamentally stateless. The state is mainly in the tags on the snapshots themselves and the parameters to the CloudFormation stack. If you make changes to the parameters or make changes to the Lambda function code, it is best to delete the stack and then launch the stack again. - -## Authors - -* **Marcelo Coronel** - [mrcoronel](https://github.com/mrcoronel) - -## License - -This project is licensed under the Apache License - see the [LICENSE.txt](LICENSE.txt) file for details + +# Snapshot Tool for Amazon RDS +​ +The Snapshot Tool for RDS automates the task of creating manual snapshots, copying them into a different account and a different region, and deleting them after a specified number of days. It also allows you to specify the backup schedule (at what times and how often) and a retention period in days. This version will work with all Amazon RDS instances except Amazon Aurora. For a version that works with Amazon Aurora, please visit the [Snapshot Tool for Amazon Aurora](https://github.com/awslabs/aurora-snapshot-tool). +​ +**IMPORTANT** Run the tf in the same **region** where your RDS instances run (both in the source and destination accounts). If that is not possible because AWS Step Functions is not available, you will need to use the **SourceRegionOverride** parameter explained below. +​ +​ +## Getting Started +​ +​ +### Building From Source and Deploying +​ +1. Create an S3 bucket to hold the Lambda function zip files. The bucket must be in the same region where the Lambda functions will run and the Lambda functions must run in the same region as the RDS instances. If you are using a secondary AWS account to copy the snapshots to, create the S3 bucket in the secondary account as well +2. Clone the repository +3. Create Lambda Zip using the 'Lambda Code' folder; the subfolders represent the lambda functions. While zipping, only zip the python files from each of the subfolders. Do not zip the folder itself. +4. Upload the Lambda zip files to S3 Bucket(s). S3 Buckets should have the following files: + * take_snapshots_rds.zip + * share_snapshots_rds.zip + * delete_old_snapshots_rds.zip + * copy_snapshots_dest_rds.zip + * delete_old_snapshots_dest_rds.zip + * copy_snapshots_no_x_account_rds.zip + * delete_old_snapshots_no_x_account_rds.zip +5. Refer the variables.tf file to identify input parameters based on the requirements. +6. For the deployment in source account, the ‘code_bucket’ variable should refer to the Bucket name where the Source Lambda file are uploaded. And the ‘destination_account’ variable should refer to the AWS Account number of the target account to share the Snapshots with. +7. For the deployment in destination account, the ‘code_bucket’ variable should refer to the Bucket name where the Destination Lambda file are uploaded. And ‘destination_region’ variable should refer to the AWS Region where the snapshot to be copied over. +8. If you only need to copy snapshots across regions and not to a different account, set the ‘CrossAccountCopy’ variable to FALSE. When set to false, the no-x-account version of the Lambda functions will be deployed and will expect snapshots to be in the same account as they run. +9. Run ```terraform apply --auto-approve``` but be sure to use the correct bucket name in the `CodeBucket` parameter when applying terraform +​ +​ +### Source Account +#### Components +The following components will be created in the source account: +* 3 Lambda functions (TakeSnapshotsRDS, ShareSnapshotsRDS, DeleteOldSnapshotsRDS) +* 3 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineTakeSnapshotRDS, stateMachineShareSnapshotRDS, stateMachineDeleteOldSnapshotsRDS) +* 3 Cloudwatch Event Rules to trigger the state functions +* 3 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures + +​ +#### Installing in the source account +Deploy terraform code from the 'rds_snapshot_tool_source' directory +You will need to specify the different input parameters based on your requirements. The default values will back up all RDS instances in the region at 1AM UTC, once a day. +If your instances are encrypted, you will need to provide access to the KMS Key to the destination account. You can read more on how to do that here: https://aws.amazon.com/premiumsupport/knowledge-center/share-cmk-account/ +​ +Here is a break down of each parameter for the source template: +​ +* **BackupInterval** - how many hours between backup +* **BackupSchedule** - at what times and how often to run backups. Set in accordance with **BackupInterval**. For example, set **BackupInterval** to 8 hours and **BackupSchedule** 0 0,8,16 * * ? * if you want backups to run at 0, 8 and 16 UTC. If your backups run more often than **BackupInterval**, snapshots will only be created when the latest snapshot is older than **BackupInterval**. If you set BackupInterval to more than once a day, make sure to adjust BackupSchedule accordingly or backups will only be taken at the times specified in the CRON expression. +* **InstanceNamePattern** - set to the names of the instances you want this tool to back up. You can use a Python regex that will be searched in the instance identifier. For example, if your instances are named *prod-01*, *prod-02*, etc, you can set **InstanceNamePattern** to *prod*. The string you specify will be searched anywhere in the name unless you use an anchor such as ^ or $. In most cases, a simple name like "prod" or "dev" will suffice. More information on Python regular expressions here: https://docs.python.org/2/howto/regex.html +* **DestinationAccount** - the account where you want snapshots to be copied to +* **LogLevel** - The log level you want as output to the Lambda functions. ERROR is usually enough. You can increase to INFO or DEBUG. +* **RetentionDays** - the amount of days you want your snapshots to be kept. Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CreatedBy, Value: Snapshot Tool for RDS) +* **ShareSnapshots** - Set to TRUE if you are sharing snapshots with a different account. If you set to FALSE, StateMachine, Lambda functions and associated Cloudwatch Alarms related to sharing across accounts will not be created. It is useful if you only want to take backups and manage the retention, but do not need to copy them across accounts or regions. +* **SourceRegionOverride** - if you are running RDS on a region where Step Functions is not available, this parameter will allow you to override the source region. For example, at the time of this writing, you may be running RDS in Northern California (us-west-1) and would like to copy your snapshots to Montreal (ca-central-1). Neither region supports Step Functions at the time of this writing so deploying this tool there will not work. The solution is to run this template in a region that supports Step Functions (such as North Virginia or Ohio) and set **SourceRegionOverride** to *us-west-1*. +**IMPORTANT**: deploy to the closest regions for best results. +​ +* **CodeBucket** - this parameter specifies the bucket where the code for the Lambda functions is located. The Lambda function code is located in the ```lambda``` directory in zip format. These files need to be on the **root* of the bucket or the CloudFormation templates will fail. Please follow the instructions to build source (earlier on this README file) +* **DeleteOldSnapshots** - Set to TRUE to enable functionality that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete **all snapshots**, older than **RetentionDays**, created by this tool; not just the ones created after **DeleteOldSnapshots** is set to TRUE. +* **TaggedInstance** - Set to TRUE to enable functionality that will only take snapshots for RDS Instances with tag CopyDBSnapshot set to True. The settings in InstanceNamePattern and TaggedInstance both need to evaluate successfully for a snapshot to be created (logical AND). +​ +### Destination Account +#### Components +The following components will be created in the destination account: +* 2 Lambda functions (CopySnapshotsDestRDS, DeleteOldSnapshotsDestRDS) +* 2 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineCopySnapshotsDestRDS, stateMachineDeleteOldSnapshotsDestRDS) +* 2 Cloudwatch Event Rules to trigger the state functions +* 2 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures +​ +​ +On your destination account, you will need to deploy terraform code "rds_snapshot_tool_destination". As before, you will need to run it in a region where Step Functions is available. +You will need to specify the different input parameters based on your requirements. The following parameters are available: +​ +* **DestinationRegion** - the region where you want your snapshots to be copied. If you set it to the same as the source region, the snapshots will be copied from the source account but will be kept in the source region. This is useful if you would like to keep a copy of your snapshots in a different account but would prefer not to copy them to a different region. +* **SnapshotPattern** - similar to InstanceNamePattern. See above +* **DeleteOldSnapshots** - Set to TRUE to enable functionanility that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete ALL SNAPSHOTS older than RetentionDays created by this tool, not just the ones created after **DeleteOldSnapshots** is set to TRUE. +* **CrossAccountCopy** - if you only need to copy snapshots across regions and not to a different account, set this to FALSE. When set to false, the no-x-account version of the Lambda functions will be deployed and will expect snapshots to be in the same account as they run. +* **KmsKeySource** KMS Key to be used for copying encrypted snapshots on the source region. If you are copying to a different region, you will also need to provide a second key in the destination region. +* **KmsKeyDestination** KMS Key to be used for copying encrypted snapshots to the destination region. If you are not copying to a different region, this parameter is not necessary. +* **RetentionDays** - as in the source account, the amount of days you want your snapshots to be kept. **Do not set this parameter to a value lower than the source account.** Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CopiedBy, Value: Snapshot Tool for RDS) +​ +## How it Works +​ +There are two sets of Lambda Step Functions that take regular snapshots and copy them across. Snapshots can take time, and they do not signal when they're complete. Snapshots are scheduled to *begin* at a certain time using CloudWatch Events. Then different Lambda Step Functions run periodically to look for new snapshots. When they find new snapshots, they do the sharing and the copying functions. +​ +### In the Source Account +​ +A CloudWatch Event is scheduled to trigger Lambda Step Function State Machine named `stateMachineTakeSnapshotsRDS`. That state machine invokes a function named `lambdaTakeSnapshotsRDS`. That function triggers a snapshot and applies some standard tags. It matches RDS instances using a regular expression on their names. +​ +There are two other state machines and lambda functions. The `statemachineShareSnapshotsRDS` looks for new snapshots created by the `lambdaTakeSnapshotsRDS` function. When it finds them, it shares them with the destination account. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventShareSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a new snapshot that is intended to be shared, it shares the snapshot. +​ +The other state machine is the `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. +​ +### In the Destination Account +​ +There are two state machines and corresponding lambda functions. The `statemachineCopySnapshotsDestRDS` looks for new snapshots that have been shared but have not yet been copied. When it finds them, it creates a copy in the destination account, encrypted with the KMS key that has been stipulated. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventCopySnapshotsRDS` resource in `snapshots_tool_rds_dest.json`). +​ +The other state machine is just like the corresponding state machine and function in the source account. The state machine is `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. +Collapse + + + + + + + + + + + + + + + + + + + +Message Praneth Meas, Prasuna Sangela + + + + + + + + +Shift + Return to add a new line \ No newline at end of file diff --git a/cftemplates/snapshots_tool_rds_dest.json b/cftemplates/snapshots_tool_rds_dest.json deleted file mode 100644 index 2a370e1..0000000 --- a/cftemplates/snapshots_tool_rds_dest.json +++ /dev/null @@ -1,588 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Parameters": { - "CodeBucket": { - "Type": "String", - "Description": "Name of the bucket that contains the lambda functions to deploy." - }, - "SnapshotPattern": { - "Type": "String", - "Default": "ALL_SNAPSHOTS", - "Description": "Python regex for matching instance names to backup. Use \"ALL_SNAPSHOTS\" to back up every RDS instance in the region." - }, - "RetentionDays": { - "Type": "Number", - "Default": "7", - "Description": "Number of days to keep snapshots in retention before deleting them" - }, - "DestinationRegion": { - "Type": "String", - "Description": "Destination region for snapshots." - }, - "LogLevel": { - "Type": "String", - "Default": "ERROR", - "Description": "Log level for Lambda functions (DEBUG, INFO, WARN, ERROR, CRITICAL are valid values)." - }, - "LambdaCWLogRetention": { - "Type": "Number", - "Default": "7", - "Description": "Number of days to retain logs from the lambda functions in CloudWatch Logs" - }, - "SourceRegionOverride": { - "Type": "String", - "Default": "NO", - "Description": "Set to the region where your RDS instances run, only if such region does not support Step Functions. Leave as NO otherwise" - }, - "KmsKeyDestination": { - "Type": "String", - "Default": "None", - "Description": "Set to the ARN for the KMS key in the destination region to re-encrypt encrypted snapshots. Leave None if you are not using encryption" - }, - "KmsKeySource": { - "Type": "String", - "Default": "None", - "Description": "Set to the ARN for the KMS key in the SOURCE region to re-encrypt encrypted snapshots. Leave None if you are not using encryption" - }, - "DeleteOldSnapshots": { - "Type": "String", - "Default": "TRUE", - "Description": "Set to TRUE to enable deletion of snapshot based on RetentionDays. Set to FALSE to disable", - "AllowedValues": ["TRUE", "FALSE"] - }, - "CrossAccountCopy": { - "Type": "String", - "AllowedValues": ["TRUE", "FALSE"], - "Default": "TRUE", - "Description": "Enable copying snapshots across accounts. Set to FALSE if your source snapshosts are not on a different account" - }, - "LogGroupName": { - "Type": "String", - "Default": "lambdaDeleteOldSnapshotsRDS-dest", - "Description": "Name for RDS snapshot log group." - } - }, - "Conditions": { - "DeleteOld": { - "Fn::Equals": [{ - "Ref": "DeleteOldSnapshots" - }, "TRUE"] - }, - "CrossAccount": { - "Fn::Equals": [{ - "Ref": "CrossAccountCopy" - }, "TRUE" ] - } - }, - "Resources": { - "topicCopyFailedDest": { - "Type": "AWS::SNS::Topic", - "Properties": { - "DisplayName": "copies_failed_dest_rds" - } - }, - "topicDeleteOldFailedDest": { - "Type": "AWS::SNS::Topic", - "Properties": { - "DisplayName": "delete_old_failed_dest_rds" - } - }, - "snspolicyCopyFailedDest": { - "Type": "AWS::SNS::TopicPolicy", - "Properties": { - "Topics": [{ - "Ref": "topicCopyFailedDest" - }, { - "Ref": "topicDeleteOldFailedDest" - }], - "PolicyDocument": { - "Version": "2008-10-17", - "Id": "__default_policy_ID", - "Statement": [{ - "Sid": "__default_statement_ID", - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": [ - "SNS:GetTopicAttributes", - "SNS:SetTopicAttributes", - "SNS:AddPermission", - "SNS:RemovePermission", - "SNS:DeleteTopic", - "SNS:Subscribe", - "SNS:ListSubscriptionsByTopic", - "SNS:Publish", - "SNS:Receive" - ], - "Resource": "*", - "Condition": { - "StringEquals": { - "AWS:SourceOwner": { - "Ref": "AWS::AccountId" - } - } - } - }] - } - } - }, - "alarmcwCopyFailedDest": { - "Type": "AWS::CloudWatch::Alarm", - "Properties": { - "ActionsEnabled": "true", - "ComparisonOperator": "GreaterThanOrEqualToThreshold", - "EvaluationPeriods": "1", - "MetricName": "ExecutionsFailed", - "Namespace": "AWS/States", - "Period": "300", - "Statistic": "Sum", - "Threshold": "1.0", - "AlarmActions": [{ - "Ref": "topicCopyFailedDest" - }], - "Dimensions": [{ - "Name": "StateMachineArn", - "Value": { - "Ref": "statemachineCopySnapshotsDestRDS" - } - }] - } - }, - "alarmcwDeleteOldFailedDest": { - "Type": "AWS::CloudWatch::Alarm", - "Condition": "DeleteOld", - "Properties": { - "ActionsEnabled": "true", - "ComparisonOperator": "GreaterThanOrEqualToThreshold", - "EvaluationPeriods": "2", - "MetricName": "ExecutionsFailed", - "Namespace": "AWS/States", - "Period": "3600", - "Statistic": "Sum", - "Threshold": "2.0", - "AlarmActions": [{ - "Ref": "topicDeleteOldFailedDest" - }], - "Dimensions": [{ - "Name": "StateMachineArn", - "Value": { - "Ref": "statemachineDeleteOldSnapshotsDestRDS" - } - }] - } - }, - "iamroleSnapshotsRDS": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_snapshots_rds_cw_logs", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": "arn:aws:logs:*:*:*" - }] - } - }, - { - "PolicyName": "inline_policy_snapshots_rds", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "rds:CreateDBSnapshot", - "rds:DeleteDBSnapshot", - "rds:DescribeDBInstances", - "rds:DescribeDBSnapshots", - "rds:ModifyDBSnapshotAttribute", - "rds:DescribeDBSnapshotAttributes", - "rds:CopyDBSnapshot", - "rds:ListTagsForResource", - "rds:AddTagsToResource" - ], - "Resource": "*" - }] - } - - }, - { - "PolicyName": "inline_policy_snapshot_rds_kms_access", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Sid": "AllowUseOfTheKey", - "Effect": "Allow", - "Action": [ - "kms:Encrypt", - "kms:Decrypt", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:DescribeKey" - ], - "Resource": [ - "*" - ] - }, - { - "Sid": "AllowAttachmentOfPersistentResources", - "Effect": "Allow", - "Action": [ - "kms:CreateGrant", - "kms:ListGrants", - "kms:RevokeGrant" - ], - "Resource": [ - "*" - ], - "Condition": { - "Bool": { - "kms:GrantIsForAWSResource": true - } - } - } - ] - } - } - ] - } - }, - "lambdaCopySnapshotsRDS": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "CodeBucket" - }, - "S3Key": { "Fn::If" : [ "CrossAccount", "copy_snapshots_dest_rds.zip", "copy_snapshots_no_x_account_rds.zip" ]} - }, - "MemorySize" : 512, - "Description": "This functions copies snapshots for RDS Instances shared with this account. It checks for existing snapshots following the pattern specified in the environment variables with the following format: -YYYY-MM-DD-HH-MM", - "Environment": { - "Variables": { - "SNAPSHOT_PATTERN": { - "Ref": "SnapshotPattern" - }, - "DEST_REGION": { - "Ref": "DestinationRegion" - }, - "LOG_LEVEL": { - "Ref": "LogLevel" - }, - "REGION_OVERRIDE": { - "Ref": "SourceRegionOverride" - }, - "KMS_KEY_DEST_REGION": { - "Ref": "KmsKeyDestination" - }, - "KMS_KEY_SOURCE_REGION": { - "Ref": "KmsKeySource" - }, - "RETENTION_DAYS": { - "Ref": "RetentionDays" - } - } - }, - "Role": { - "Fn::GetAtt": ["iamroleSnapshotsRDS", "Arn"] - }, - "Runtime": "python3.7", - "Handler": "lambda_function.lambda_handler", - "Timeout": 300 - } - }, - "lambdaDeleteOldDestRDS": { - "Type": "AWS::Lambda::Function", - "Condition": "DeleteOld", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "CodeBucket" - }, - "S3Key": {"Fn::If" : [ "CrossAccount", "delete_old_snapshots_dest_rds.zip", "delete_old_snapshots_no_x_account_rds.zip" ]} - }, - "MemorySize" : 512, - "Description": "This function enforces retention on the snapshots shared with the destination account. ", - "Environment": { - "Variables": { - "SNAPSHOT_PATTERN": { - "Ref": "SnapshotPattern" - }, - "DEST_REGION": { - "Ref": "DestinationRegion" - }, - "RETENTION_DAYS": { - "Ref": "RetentionDays" - }, - "LOG_LEVEL": { - "Ref": "LogLevel" - } - } - }, - "Role": { - "Fn::GetAtt": ["iamroleSnapshotsRDS", "Arn"] - }, - "Runtime": "python3.7", - "Handler": "lambda_function.lambda_handler", - "Timeout": 300 - } - }, - "iamroleStateExecution": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": ["", ["states.", { - "Ref": "AWS::Region" - }, ".amazonaws.com"]] - } - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_rds_snapshot", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "lambda:InvokeFunction" - ], - "Resource": "*" - }] - } - }] - } - }, - "statemachineCopySnapshotsDestRDS": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "DefinitionString": { - "Fn::Join": ["", [{ - "Fn::Join": ["\n", [ - " {\"Comment\":\"Copies snapshots locally and then to DEST_REGION\",", - " \"StartAt\":\"CopySnapshots\",", - " \"States\":{", - " \"CopySnapshots\":{", - " \"Type\":\"Task\",", - " \"Resource\": " - ]] - }, - "\"", - { - "Fn::GetAtt": ["lambdaCopySnapshotsRDS", "Arn"] - }, "\"\n,", - { - "Fn::Join": ["\n", [ - " \"Retry\":[", - " {", - " \"ErrorEquals\":[ ", - " \"SnapshotToolException\"", - " ],", - " \"IntervalSeconds\":300,", - " \"MaxAttempts\":5,", - " \"BackoffRate\":1", - " },", - " {", - " \"ErrorEquals\":[ ", - " \"States.ALL\"], ", - " \"IntervalSeconds\": 30,", - " \"MaxAttempts\": 20,", - " \"BackoffRate\": 1", - " }", - " ],", - " \"End\": true ", - " }", - " }}" - ]] - } - ]] - }, - "RoleArn": { - "Fn::GetAtt": ["iamroleStateExecution", "Arn"] - } - } - }, - "statemachineDeleteOldSnapshotsDestRDS": { - "Type": "AWS::StepFunctions::StateMachine", - "Condition": "DeleteOld", - "Properties": { - "DefinitionString": { - "Fn::Join": ["", [{ - "Fn::Join": ["\n", [ - " {\"Comment\":\"DeleteOld for RDS snapshots in destination region\",", - " \"StartAt\":\"DeleteOldDestRegion\",", - " \"States\":{", - " \"DeleteOldDestRegion\":{", - " \"Type\":\"Task\",", - " \"Resource\": " - ]] - }, - "\"", - { - "Fn::GetAtt": ["lambdaDeleteOldDestRDS", "Arn"] - }, "\"\n,", - { - "Fn::Join": ["\n", [ - " \"Retry\":[", - " {", - " \"ErrorEquals\":[ ", - " \"SnapshotToolException\"", - " ],", - " \"IntervalSeconds\":600,", - " \"MaxAttempts\":5,", - " \"BackoffRate\":1", - " },", - " {", - " \"ErrorEquals\":[ ", - " \"States.ALL\"], ", - " \"IntervalSeconds\": 30,", - " \"MaxAttempts\": 20,", - " \"BackoffRate\": 1", - " }", - " ],", - " \"End\": true ", - " }", - " }}" - ]] - } - ]] - }, - "RoleArn": { - "Fn::GetAtt": ["iamroleStateExecution", "Arn"] - } - } - }, - "iamroleStepInvocation": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "events.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_state_invocation", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "states:StartExecution" - ], - "Resource": "*" - }] - } - }] - } - }, - "cwEventCopySnapshotsRDS": { - "Type": "AWS::Events::Rule", - "Properties": { - "Description": "Triggers the RDS Copy state machine in the destination account", - "ScheduleExpression": { - "Fn::Join": ["", ["cron(", "/30 * * * ? *", ")"]] - }, - "State": "ENABLED", - "Targets": [{ - "Arn": { - "Ref": "statemachineCopySnapshotsDestRDS" - }, - "Id": "Target1", - "RoleArn": { - "Fn::GetAtt": ["iamroleStepInvocation", "Arn"] - } - }] - } - }, - "cwEventDeleteOldSnapshotsRDS": { - "Type": "AWS::Events::Rule", - "Condition": "DeleteOld", - "Properties": { - "Description": "Triggers the RDS DeleteOld state machine in the destination account", - "ScheduleExpression": { - "Fn::Join": ["", ["cron(", "0 /1 * * ? *", ")"]] - }, - "State": "ENABLED", - "Targets": [{ - "Arn": { - "Ref": "statemachineDeleteOldSnapshotsDestRDS" - }, - "Id": "Target1", - "RoleArn": { - "Fn::GetAtt": ["iamroleStepInvocation", "Arn"] - } - }] - } - }, - "cwloggroupDeleteOldSnapshotsDestRDS":{ - "Type": "AWS::Logs::LogGroup", - "Description": "Log group for the lambdaCopySnapshotsRDS function's logs", - "Condition": "DeleteOld", - "DependsOn": "lambdaDeleteOldDestRDS", - "Properties": { - "RetentionInDays": { "Ref": "LambdaCWLogRetention" }, - "LogGroupName": { - "Fn::Sub": [ "/aws/lambda/${func}", { "func": { "Ref" : "LogGroupName" } } ] - } - } - }, - "cwloggrouplambdaCopySnapshotsRDS":{ - "Type": "AWS::Logs::LogGroup", - "Description": "Log group for the lambdaCopySnapshotsRDS function's logs", - "DependsOn": "lambdaCopySnapshotsRDS", - "Properties": { - "RetentionInDays": { "Ref": "LambdaCWLogRetention" }, - "LogGroupName": { - "Fn::Sub": [ "/aws/lambda/${func}", { "func": { "Ref" : "lambdaCopySnapshotsRDS" } } ] - } - } - } - }, - "Outputs": { - "CopyFailedTopic": { - "Description": "Subscribe to this topic to receive alerts of failed copies", - "Value": { - "Ref": "topicCopyFailedDest" - } - }, - "DeleteOldFailedTopic": { - "Condition": "DeleteOld", - "Description": "Subscribe to this topic to receive alerts of failures at deleting old snapshots", - "Value": { - "Ref": "topicDeleteOldFailedDest" - } - }, - "SourceURL": { - "Description": "For more information and documentation, see the source repository at GitHub.", - "Value": "https://github.com/awslabs/rds-snapshot-tool" - } - }, - "Description": "Snapshots Tool for RDS cross-region and cross-account (destination account stack)" -} diff --git a/cftemplates/snapshots_tool_rds_dest.yaml b/cftemplates/snapshots_tool_rds_dest.yaml new file mode 100644 index 0000000..d047fdc --- /dev/null +++ b/cftemplates/snapshots_tool_rds_dest.yaml @@ -0,0 +1,496 @@ +--- +AWSTemplateFormatVersion: '2010-09-09' +Parameters: + CodeBucket: + Type: String + Description: Name of the bucket that contains the lambda functions to deploy. + SnapshotPattern: + Type: String + Default: ALL_SNAPSHOTS + Description: Python regex for matching instance names to backup. Use "ALL_SNAPSHOTS" + to back up every RDS instance in the region. + RetentionDays: + Type: Number + Default: '7' + Description: Number of days to keep snapshots in retention before deleting them + DestinationRegion: + Type: String + Description: Destination region for snapshots. + LogLevel: + Type: String + Default: ERROR + Description: Log level for Lambda functions (DEBUG, INFO, WARN, ERROR, CRITICAL + are valid values). + LambdaCWLogRetention: + Type: Number + Default: '7' + Description: Number of days to retain logs from the lambda functions in CloudWatch + Logs + SourceRegionOverride: + Type: String + Default: 'NO' + Description: Set to the region where your RDS instances run, only if such region + does not support Step Functions. Leave as NO otherwise + KmsKeyDestination: + Type: String + Default: None + Description: Set to the ARN for the KMS key in the destination region to re-encrypt + encrypted snapshots. Leave None if you are not using encryption + KmsKeySource: + Type: String + Default: None + Description: Set to the ARN for the KMS key in the SOURCE region to re-encrypt + encrypted snapshots. Leave None if you are not using encryption + DeleteOldSnapshots: + Type: String + Default: 'TRUE' + Description: Set to TRUE to enable deletion of snapshot based on RetentionDays. + Set to FALSE to disable + AllowedValues: + - 'TRUE' + - 'FALSE' + CrossAccountCopy: + Type: String + AllowedValues: + - 'TRUE' + - 'FALSE' + Default: 'TRUE' + Description: Enable copying snapshots across accounts. Set to FALSE if your source + snapshosts are not on a different account + LogGroupName: + Type: String + Default: lambdaDeleteOldSnapshotsRDS-dest + Description: Name for RDS snapshot log group. +Conditions: + DeleteOld: + Fn::Equals: + - Ref: DeleteOldSnapshots + - 'TRUE' + CrossAccount: + Fn::Equals: + - Ref: CrossAccountCopy + - 'TRUE' +Resources: + topicCopyFailedDest: + Type: AWS::SNS::Topic + Properties: + DisplayName: copies_failed_dest_rds + topicDeleteOldFailedDest: + Type: AWS::SNS::Topic + Properties: + DisplayName: delete_old_failed_dest_rds + snspolicyCopyFailedDest: + Type: AWS::SNS::TopicPolicy + Properties: + Topics: + - Ref: topicCopyFailedDest + - Ref: topicDeleteOldFailedDest + PolicyDocument: + Version: '2008-10-17' + Id: __default_policy_ID + Statement: + - Sid: __default_statement_ID + Effect: Allow + Principal: + AWS: "*" + Action: + - SNS:GetTopicAttributes + - SNS:SetTopicAttributes + - SNS:AddPermission + - SNS:RemovePermission + - SNS:DeleteTopic + - SNS:Subscribe + - SNS:ListSubscriptionsByTopic + - SNS:Publish + - SNS:Receive + Resource: "*" + Condition: + StringEquals: + AWS:SourceOwner: + Ref: AWS::AccountId + alarmcwCopyFailedDest: + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: 'true' + ComparisonOperator: GreaterThanOrEqualToThreshold + EvaluationPeriods: '1' + MetricName: ExecutionsFailed + Namespace: AWS/States + Period: '300' + Statistic: Sum + Threshold: '1.0' + AlarmActions: + - Ref: topicCopyFailedDest + Dimensions: + - Name: StateMachineArn + Value: + Ref: statemachineCopySnapshotsDestRDS + alarmcwDeleteOldFailedDest: + Type: AWS::CloudWatch::Alarm + Condition: DeleteOld + Properties: + ActionsEnabled: 'true' + ComparisonOperator: GreaterThanOrEqualToThreshold + EvaluationPeriods: '2' + MetricName: ExecutionsFailed + Namespace: AWS/States + Period: '3600' + Statistic: Sum + Threshold: '2.0' + AlarmActions: + - Ref: topicDeleteOldFailedDest + Dimensions: + - Name: StateMachineArn + Value: + Ref: statemachineDeleteOldSnapshotsDestRDS + iamroleSnapshotsRDS: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_snapshots_rds_cw_logs + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: arn:aws:logs:*:*:* + - PolicyName: inline_policy_snapshots_rds + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - rds:CreateDBSnapshot + - rds:DeleteDBSnapshot + - rds:DescribeDBInstances + - rds:DescribeDBSnapshots + - rds:ModifyDBSnapshotAttribute + - rds:DescribeDBSnapshotAttributes + - rds:CopyDBSnapshot + - rds:ListTagsForResource + - rds:AddTagsToResource + Resource: "*" + - PolicyName: inline_policy_snapshot_rds_kms_access + PolicyDocument: + Version: '2012-10-17' + Statement: + - Sid: AllowUseOfTheKey + Effect: Allow + Action: + - kms:Encrypt + - kms:Decrypt + - kms:ReEncrypt* + - kms:GenerateDataKey* + - kms:DescribeKey + Resource: + - "*" + - Sid: AllowAttachmentOfPersistentResources + Effect: Allow + Action: + - kms:CreateGrant + - kms:ListGrants + - kms:RevokeGrant + Resource: + - "*" + Condition: + Bool: + kms:GrantIsForAWSResource: true + lambdaCopySnapshotsRDS: + Type: AWS::Lambda::Function + Properties: + Code: + S3Bucket: + Ref: CodeBucket + S3Key: + Fn::If: + - CrossAccount + - copy_snapshots_dest_rds.zip + - copy_snapshots_no_x_account_rds.zip + MemorySize: 512 + Description: 'This functions copies snapshots for RDS Instances shared with + this account. It checks for existing snapshots following the pattern specified + in the environment variables with the following format: -YYYY-MM-DD-HH-MM' + Environment: + Variables: + SNAPSHOT_PATTERN: + Ref: SnapshotPattern + DEST_REGION: + Ref: DestinationRegion + LOG_LEVEL: + Ref: LogLevel + REGION_OVERRIDE: + Ref: SourceRegionOverride + KMS_KEY_DEST_REGION: + Ref: KmsKeyDestination + KMS_KEY_SOURCE_REGION: + Ref: KmsKeySource + RETENTION_DAYS: + Ref: RetentionDays + Role: + Fn::GetAtt: + - iamroleSnapshotsRDS + - Arn + Runtime: python3.7 + Handler: lambda_function.lambda_handler + Timeout: 300 + lambdaDeleteOldDestRDS: + Type: AWS::Lambda::Function + Condition: DeleteOld + Properties: + Code: + S3Bucket: + Ref: CodeBucket + S3Key: + Fn::If: + - CrossAccount + - delete_old_snapshots_dest_rds.zip + - delete_old_snapshots_no_x_account_rds.zip + MemorySize: 512 + Description: 'This function enforces retention on the snapshots shared with + the destination account. ' + Environment: + Variables: + SNAPSHOT_PATTERN: + Ref: SnapshotPattern + DEST_REGION: + Ref: DestinationRegion + RETENTION_DAYS: + Ref: RetentionDays + LOG_LEVEL: + Ref: LogLevel + Role: + Fn::GetAtt: + - iamroleSnapshotsRDS + - Arn + Runtime: python3.7 + Handler: lambda_function.lambda_handler + Timeout: 300 + iamroleStateExecution: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + Fn::Join: + - '' + - - states. + - Ref: AWS::Region + - ".amazonaws.com" + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_rds_snapshot + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: "*" + statemachineCopySnapshotsDestRDS: + Type: AWS::StepFunctions::StateMachine + Properties: + DefinitionString: + Fn::Join: + - '' + - - Fn::Join: + - "\n" + - - ' {"Comment":"Copies snapshots locally and then to DEST_REGION",' + - ' "StartAt":"CopySnapshots",' + - ' "States":{' + - ' "CopySnapshots":{' + - ' "Type":"Task",' + - ' "Resource": ' + - "\"" + - Fn::GetAtt: + - lambdaCopySnapshotsRDS + - Arn + - |- + " + , + - Fn::Join: + - "\n" + - - ' "Retry":[' + - " {" + - ' "ErrorEquals":[ ' + - ' "SnapshotToolException"' + - " ]," + - ' "IntervalSeconds":300,' + - ' "MaxAttempts":5,' + - ' "BackoffRate":1' + - " }," + - " {" + - ' "ErrorEquals":[ ' + - ' "States.ALL"], ' + - ' "IntervalSeconds": 30,' + - ' "MaxAttempts": 20,' + - ' "BackoffRate": 1' + - " }" + - " ]," + - ' "End": true ' + - " }" + - " }}" + RoleArn: + Fn::GetAtt: + - iamroleStateExecution + - Arn + statemachineDeleteOldSnapshotsDestRDS: + Type: AWS::StepFunctions::StateMachine + Condition: DeleteOld + Properties: + DefinitionString: + Fn::Join: + - '' + - - Fn::Join: + - "\n" + - - ' {"Comment":"DeleteOld for RDS snapshots in destination region",' + - ' "StartAt":"DeleteOldDestRegion",' + - ' "States":{' + - ' "DeleteOldDestRegion":{' + - ' "Type":"Task",' + - ' "Resource": ' + - "\"" + - Fn::GetAtt: + - lambdaDeleteOldDestRDS + - Arn + - |- + " + , + - Fn::Join: + - "\n" + - - ' "Retry":[' + - " {" + - ' "ErrorEquals":[ ' + - ' "SnapshotToolException"' + - " ]," + - ' "IntervalSeconds":600,' + - ' "MaxAttempts":5,' + - ' "BackoffRate":1' + - " }," + - " {" + - ' "ErrorEquals":[ ' + - ' "States.ALL"], ' + - ' "IntervalSeconds": 30,' + - ' "MaxAttempts": 20,' + - ' "BackoffRate": 1' + - " }" + - " ]," + - ' "End": true ' + - " }" + - " }}" + RoleArn: + Fn::GetAtt: + - iamroleStateExecution + - Arn + iamroleStepInvocation: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: events.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_state_invocation + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - states:StartExecution + Resource: "*" + cwEventCopySnapshotsRDS: + Type: AWS::Events::Rule + Properties: + Description: Triggers the RDS Copy state machine in the destination account + ScheduleExpression: + Fn::Join: + - '' + - - cron( + - "/30 * * * ? *" + - ")" + State: ENABLED + Targets: + - Arn: + Ref: statemachineCopySnapshotsDestRDS + Id: Target1 + RoleArn: + Fn::GetAtt: + - iamroleStepInvocation + - Arn + cwEventDeleteOldSnapshotsRDS: + Type: AWS::Events::Rule + Condition: DeleteOld + Properties: + Description: Triggers the RDS DeleteOld state machine in the destination account + ScheduleExpression: + Fn::Join: + - '' + - - cron( + - 0 /1 * * ? * + - ")" + State: ENABLED + Targets: + - Arn: + Ref: statemachineDeleteOldSnapshotsDestRDS + Id: Target1 + RoleArn: + Fn::GetAtt: + - iamroleStepInvocation + - Arn + cwloggroupDeleteOldSnapshotsDestRDS: + Type: AWS::Logs::LogGroup + Description: Log group for the lambdaCopySnapshotsRDS function's logs + Condition: DeleteOld + DependsOn: lambdaDeleteOldDestRDS + Properties: + RetentionInDays: + Ref: LambdaCWLogRetention + LogGroupName: + Fn::Sub: + - "/aws/lambda/${func}" + - func: + Ref: LogGroupName + cwloggrouplambdaCopySnapshotsRDS: + Type: AWS::Logs::LogGroup + Description: Log group for the lambdaCopySnapshotsRDS function's logs + DependsOn: lambdaCopySnapshotsRDS + Properties: + RetentionInDays: + Ref: LambdaCWLogRetention + LogGroupName: + Fn::Sub: + - "/aws/lambda/${func}" + - func: + Ref: lambdaCopySnapshotsRDS +Outputs: + CopyFailedTopic: + Description: Subscribe to this topic to receive alerts of failed copies + Value: + Ref: topicCopyFailedDest + DeleteOldFailedTopic: + Condition: DeleteOld + Description: Subscribe to this topic to receive alerts of failures at deleting + old snapshots + Value: + Ref: topicDeleteOldFailedDest + SourceURL: + Description: For more information and documentation, see the source repository + at GitHub. + Value: https://github.com/awslabs/rds-snapshot-tool +Description: Snapshots Tool for RDS cross-region and cross-account (destination account + stack) diff --git a/cftemplates/snapshots_tool_rds_source.json b/cftemplates/snapshots_tool_rds_source.json deleted file mode 100644 index a0a08f3..0000000 --- a/cftemplates/snapshots_tool_rds_source.json +++ /dev/null @@ -1,707 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Parameters": { - "CodeBucket": { - "Type": "String", - "Description": "Name of the bucket that contains the lambda functions to deploy." - }, - "InstanceNamePattern": { - "Type": "String", - "Default": "ALL_INSTANCES", - "Description": "Python regex for matching cluster identifiers to backup. Use \"ALL_INSTANCES\" to back up every RDS instance in the region." - }, - "BackupInterval": { - "Type": "Number", - "Default": "24", - "Description": "Interval for backups in hours. Default is 24" - }, - "DestinationAccount": { - "Type": "Number", - "Default": "000000000000", - "Description": "Destination account with no dashes." - }, - "ShareSnapshots": { - "Type": "String", - "Default": "TRUE", - "AllowedValues": ["TRUE", "FALSE"] - }, - "BackupSchedule": { - "Type": "String", - "Default": "0 1 * * ? *", - "Description": "Backup schedule in Cloudwatch Event cron format. Needs to run at least once for every Interval. The default value runs once every at 1AM UTC. More information: http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html" - }, - "RetentionDays": { - "Type": "Number", - "Default": "7", - "Description": "Number of days to keep snapshots in retention before deleting them" - }, - "LogLevel": { - "Type": "String", - "Default": "ERROR", - "Description": "Log level for Lambda functions (DEBUG, INFO, WARN, ERROR, CRITICAL are valid values)." - }, - "LambdaCWLogRetention": { - "Type": "Number", - "Default": "7", - "Description": "Number of days to retain logs from the lambda functions in CloudWatch Logs" - }, - "SourceRegionOverride": { - "Type": "String", - "Default": "NO", - "Description": "Set to the region where your RDS instances run, only if such region does not support Step Functions. Leave as NO otherwise" - }, - "DeleteOldSnapshots": { - "Type": "String", - "Default": "TRUE", - "Description": "Set to TRUE to enable deletion of snapshot based on RetentionDays. Set to FALSE to disable", - "AllowedValues": ["TRUE", "FALSE"] - }, - "TaggedInstance": { - "Type": "String", - "Default": "FALSE", - "Description": "Set to TRUE to filter instances that have tag CopyDBSnapshot set to True. Set to FALSE to disable", - "AllowedValues": ["TRUE", "FALSE"] - }, - "LogGroupName": { - "Type": "String", - "Default": "lambdaDeleteOldSnapshotsRDS-source", - "Description": "Name for RDS snapshot log group." - } - }, - "Conditions": { - "Share": { - "Fn::Equals": [{ - "Ref": "ShareSnapshots" - }, "TRUE"] - }, - "DeleteOld": { - "Fn::Equals": [{ - "Ref": "DeleteOldSnapshots" - }, "TRUE"] - } - }, - "Resources": { - "topicBackupsFailed": { - "Type": "AWS::SNS::Topic", - "Properties": { - "DisplayName": "backups_failed_rds" - } - }, - "topicShareFailed": { - "Type": "AWS::SNS::Topic", - "Properties": { - "DisplayName": "share_failed_rds" - } - }, - "topicDeleteOldFailed": { - "Type": "AWS::SNS::Topic", - "Properties": { - "DisplayName": "delete_old_failed_rds" - } - }, - "snspolicySnapshotsRDS": { - "Type": "AWS::SNS::TopicPolicy", - "Properties": { - "Topics": [{ - "Ref": "topicBackupsFailed" - }, - { - "Ref": "topicShareFailed" - }, { - "Ref": "topicDeleteOldFailed" - } - ], - "PolicyDocument": { - "Version": "2008-10-17", - "Id": "__default_policy_ID", - "Statement": [{ - "Sid": "__default_statement_ID", - "Effect": "Allow", - "Principal": { - "AWS": "*" - }, - "Action": [ - "SNS:GetTopicAttributes", - "SNS:SetTopicAttributes", - "SNS:AddPermission", - "SNS:RemovePermission", - "SNS:DeleteTopic", - "SNS:Subscribe", - "SNS:ListSubscriptionsByTopic", - "SNS:Publish", - "SNS:Receive" - ], - "Resource": "*", - "Condition": { - "StringEquals": { - "AWS:SourceOwner": { - "Ref": "AWS::AccountId" - } - } - } - }] - } - } - }, - "alarmcwBackupsFailed": { - "Type": "AWS::CloudWatch::Alarm", - "Properties": { - "ActionsEnabled": "true", - "ComparisonOperator": "GreaterThanOrEqualToThreshold", - "EvaluationPeriods": "1", - "MetricName": "ExecutionsFailed", - "Namespace": "AWS/States", - "Period": "300", - "Statistic": "Sum", - "Threshold": "1.0", - "AlarmActions": [{ - "Ref": "topicBackupsFailed" - }], - "Dimensions": [{ - "Name": "StateMachineArn", - "Value": { - "Ref": "stateMachineTakeSnapshotsRDS" - } - }] - } - }, - "alarmcwShareFailed": { - "Condition": "Share", - "Type": "AWS::CloudWatch::Alarm", - "Properties": { - "ActionsEnabled": "true", - "ComparisonOperator": "GreaterThanOrEqualToThreshold", - "EvaluationPeriods": "2", - "MetricName": "ExecutionsFailed", - "Namespace": "AWS/States", - "Period": "3600", - "Statistic": "Sum", - "Threshold": "2.0", - "AlarmActions": [{ - "Ref": "topicShareFailed" - }], - "Dimensions": [{ - "Name": "StateMachineArn", - "Value": { - "Ref": "statemachineShareSnapshotsRDS" - } - }] - } - }, - "alarmcwDeleteOldFailed": { - "Condition": "DeleteOld", - "Type": "AWS::CloudWatch::Alarm", - "Properties": { - "ActionsEnabled": "true", - "ComparisonOperator": "GreaterThanOrEqualToThreshold", - "EvaluationPeriods": "2", - "MetricName": "ExecutionsFailed", - "Namespace": "AWS/States", - "Period": "3600", - "Statistic": "Sum", - "Threshold": "2.0", - "AlarmActions": [{ - "Ref": "topicDeleteOldFailed" - }], - "Dimensions": [{ - "Name": "StateMachineArn", - "Value": { - "Ref": "statemachineDeleteOldSnapshotsRDS" - } - }] - } - }, - "iamroleSnapshotsRDS": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "lambda.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_snapshots_rds_cw_logs", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents" - ], - "Resource": "arn:aws:logs:*:*:*" - }] - } - }, - { - "PolicyName": "inline_policy_snapshots_rds", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "rds:CreateDBSnapshot", - "rds:DeleteDBSnapshot", - "rds:DescribeDBInstances", - "rds:DescribeDBSnapshots", - "rds:ModifyDBSnapshotAttribute", - "rds:DescribeDBSnapshotAttributes", - "rds:ListTagsForResource", - "rds:AddTagsToResource" - ], - "Resource": "*" - }] - } - - } - ] - } - }, - "lambdaTakeSnapshotsRDS": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "CodeBucket" - }, - "S3Key": "take_snapshots_rds.zip" - }, - "MemorySize" : 512, - "Description": "This functions triggers snapshots creation for RDS instances. It checks for existing snapshots following the pattern and interval specified in the environment variables with the following format: -YYYY-MM-DD-HH-MM", - "Environment": { - "Variables": { - "INTERVAL": { - "Ref": "BackupInterval" - }, - "PATTERN": { - "Ref": "InstanceNamePattern" - }, - "LOG_LEVEL": { - "Ref": "LogLevel" - }, - "REGION_OVERRIDE": { - "Ref": "SourceRegionOverride" - }, - "TAGGEDINSTANCE": { - "Ref": "TaggedInstance" - } - } - }, - "Role": { - "Fn::GetAtt": ["iamroleSnapshotsRDS", "Arn"] - }, - "Runtime": "python3.7", - "Handler": "lambda_function.lambda_handler", - "Timeout": 300 - } - }, - "lambdaShareSnapshotsRDS": { - "Type": "AWS::Lambda::Function", - "Condition": "Share", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "CodeBucket" - }, - "S3Key": "share_snapshots_rds.zip" - }, - "MemorySize" : 512, - "Description": "This function shares snapshots created by the take_snapshots_rds function with DEST_ACCOUNT specified in the environment variables. ", - "Environment": { - "Variables": { - "DEST_ACCOUNT": { - "Ref": "DestinationAccount" - }, - "LOG_LEVEL": { - "Ref": "LogLevel" - }, - "PATTERN": { - "Ref": "InstanceNamePattern" - }, - "REGION_OVERRIDE": { - "Ref": "SourceRegionOverride" - } - } - }, - "Role": { - "Fn::GetAtt": ["iamroleSnapshotsRDS", "Arn"] - }, - "Runtime": "python3.7", - "Handler": "lambda_function.lambda_handler", - "Timeout": 300 - } - }, - "lambdaDeleteOldSnapshotsRDS": { - "Type": "AWS::Lambda::Function", - "Condition": "DeleteOld", - "Properties": { - "Code": { - "S3Bucket": { - "Ref": "CodeBucket" - }, - "S3Key": "delete_old_snapshots_rds.zip" - }, - "MemorySize" : 512, - "Description": "This function deletes snapshots created by the take_snapshots_rds function. ", - "Environment": { - "Variables": { - "RETENTION_DAYS": { - "Ref": "RetentionDays" - }, - "PATTERN": { - "Ref": "InstanceNamePattern" - }, - "LOG_LEVEL": { - "Ref": "LogLevel" - }, - "REGION_OVERRIDE": { - "Ref": "SourceRegionOverride" - } - } - }, - "Role": { - "Fn::GetAtt": ["iamroleSnapshotsRDS", "Arn"] - }, - "Runtime": "python3.7", - "Handler": "lambda_function.lambda_handler", - "Timeout": 300 - } - }, - "iamroleStateExecution": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": { - "Fn::Join": ["", ["states.", { - "Ref": "AWS::Region" - }, ".amazonaws.com"]] - } - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_snapshots_rds", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "lambda:InvokeFunction" - ], - "Resource": "*" - }] - } - }] - } - }, - "stateMachineTakeSnapshotsRDS": { - "Type": "AWS::StepFunctions::StateMachine", - "Properties": { - "DefinitionString": { - "Fn::Join": ["", [{ - "Fn::Join": ["\n", [ - " {\"Comment\":\"Triggers snapshot backup for RDS instances\",", - " \"StartAt\":\"TakeSnapshots\",", - " \"States\":{", - " \"TakeSnapshots\":{", - " \"Type\":\"Task\",", - " \"Resource\": " - ]] - }, - "\"", - { - "Fn::GetAtt": ["lambdaTakeSnapshotsRDS", "Arn"] - }, "\"\n,", - { - "Fn::Join": ["\n", [ - " \"Retry\":[", - " {", - " \"ErrorEquals\":[ ", - " \"SnapshotToolException\"", - " ],", - " \"IntervalSeconds\":300,", - " \"MaxAttempts\":20,", - " \"BackoffRate\":1", - " },", - " {", - " \"ErrorEquals\":[ ", - " \"States.ALL\"], ", - " \"IntervalSeconds\": 30,", - " \"MaxAttempts\": 20,", - " \"BackoffRate\": 1", - " }", - " ],", - " \"End\": true ", - " }", - " }}" - ]] - } - ]] - }, - "RoleArn": { - "Fn::GetAtt": ["iamroleStateExecution", "Arn"] - } - } - }, - "statemachineShareSnapshotsRDS": { - "Type": "AWS::StepFunctions::StateMachine", - "Condition": "Share", - "Properties": { - "DefinitionString": { - "Fn::Join": ["", [{ - "Fn::Join": ["\n", [ - " {\"Comment\":\"Shares snapshots with DEST_ACCOUNT\",", - " \"StartAt\":\"ShareSnapshots\",", - " \"States\":{", - " \"ShareSnapshots\":{", - " \"Type\":\"Task\",", - " \"Resource\": " - ]] - }, - "\"", - { - "Fn::GetAtt": ["lambdaShareSnapshotsRDS", "Arn"] - }, "\"\n,", - { - "Fn::Join": ["\n", [ - " \"Retry\":[", - " {", - " \"ErrorEquals\":[ ", - " \"SnapshotToolException\"", - " ],", - " \"IntervalSeconds\":300,", - " \"MaxAttempts\":3,", - " \"BackoffRate\":1", - " },", - " {", - " \"ErrorEquals\":[ ", - " \"States.ALL\"], ", - " \"IntervalSeconds\": 30,", - " \"MaxAttempts\": 20,", - " \"BackoffRate\": 1", - " }", - " ],", - " \"End\": true ", - " }", - " }}" - ]] - } - ]] - }, - "RoleArn": { - "Fn::GetAtt": ["iamroleStateExecution", "Arn"] - } - } - }, - "statemachineDeleteOldSnapshotsRDS": { - "Type": "AWS::StepFunctions::StateMachine", - "Condition": "DeleteOld", - "Properties": { - "DefinitionString": { - "Fn::Join": ["", [{ - "Fn::Join": ["\n", [ - " {\"Comment\":\"DeleteOld management for RDS snapshots\",", - " \"StartAt\":\"DeleteOld\",", - " \"States\":{", - " \"DeleteOld\":{", - " \"Type\":\"Task\",", - " \"Resource\": " - ]] - }, - "\"", - { - "Fn::GetAtt": ["lambdaDeleteOldSnapshotsRDS", "Arn"] - }, "\"\n,", - { - "Fn::Join": ["\n", [ - " \"Retry\":[", - " {", - " \"ErrorEquals\":[ ", - " \"SnapshotToolException\"", - " ],", - " \"IntervalSeconds\":300,", - " \"MaxAttempts\":7,", - " \"BackoffRate\":1", - " },", - " {", - " \"ErrorEquals\":[ ", - " \"States.ALL\"], ", - " \"IntervalSeconds\": 30,", - " \"MaxAttempts\": 20,", - " \"BackoffRate\": 1", - " }", - " ],", - " \"End\": true ", - " }", - " }}" - ]] - } - ]] - }, - "RoleArn": { - "Fn::GetAtt": ["iamroleStateExecution", "Arn"] - } - } - }, - "iamroleStepInvocation": { - "Type": "AWS::IAM::Role", - "Properties": { - "AssumeRolePolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Principal": { - "Service": "events.amazonaws.com" - }, - "Action": "sts:AssumeRole" - }] - }, - "Policies": [{ - "PolicyName": "inline_policy_state_invocation", - "PolicyDocument": { - "Version": "2012-10-17", - "Statement": [{ - "Effect": "Allow", - "Action": [ - "states:StartExecution" - ], - "Resource": "*" - }] - } - }] - } - }, - "cwEventBackupRDS": { - "Type": "AWS::Events::Rule", - "Properties": { - "Description": "Triggers the TakeSnapshotsRDS state machine", - "ScheduleExpression": { - "Fn::Join": ["", ["cron(", { - "Ref": "BackupSchedule" - }, ")"]] - }, - "State": "ENABLED", - "Targets": [{ - "Arn": { - "Ref": "stateMachineTakeSnapshotsRDS" - }, - "Id": "Target1", - "RoleArn": { - "Fn::GetAtt": ["iamroleStepInvocation", "Arn"] - } - }] - } - }, - "cwEventShareSnapshotsRDS": { - "Type": "AWS::Events::Rule", - "Condition": "Share", - "Properties": { - "Description": "Triggers the ShareSnapshotsRDS state machine", - "ScheduleExpression": { - "Fn::Join": ["", ["cron(", "/10 * * * ? *", ")"]] - }, - "State": "ENABLED", - "Targets": [{ - "Arn": { - "Ref": "statemachineShareSnapshotsRDS" - }, - "Id": "Target1", - "RoleArn": { - "Fn::GetAtt": ["iamroleStepInvocation", "Arn"] - } - }] - } - }, - "cwEventDeleteOldSnapshotsRDS": { - "Type": "AWS::Events::Rule", - "Condition": "DeleteOld", - "Properties": { - "Description": "Triggers the DeleteOldSnapshotsRDS state machine", - "ScheduleExpression": { - "Fn::Join": ["", ["cron(", "0 /1 * * ? *", ")"]] - }, - "State": "ENABLED", - "Targets": [{ - "Arn": { - "Ref": "statemachineDeleteOldSnapshotsRDS" - }, - "Id": "Target1", - "RoleArn": { - "Fn::GetAtt": ["iamroleStepInvocation", "Arn"] - } - }] - } - }, - "cwloggrouplambdaTakeSnapshotsRDS":{ - "Type": "AWS::Logs::LogGroup", - "Description": "Log group for the lambdaTakeSnapshotsRDS function's logs", - "DependsOn": "lambdaTakeSnapshotsRDS", - "Properties": { - "RetentionInDays": { "Ref": "LambdaCWLogRetention" }, - "LogGroupName": { - "Fn::Sub": [ "/aws/lambda/${func}", { "func": { "Ref" : "lambdaTakeSnapshotsRDS" } } ] - } - } - }, - "cwloggrouplambdaShareSnapshotsRDS":{ - "Condition": "Share", - "Type": "AWS::Logs::LogGroup", - "Description": "Log group for the lambdaShareSnapshotsRDS function's logs", - "DependsOn": "lambdaShareSnapshotsRDS", - "Properties": { - "RetentionInDays": { "Ref": "LambdaCWLogRetention" }, - "LogGroupName": { - "Fn::Sub": [ "/aws/lambda/${func}", { "func": { "Ref" : "lambdaShareSnapshotsRDS" } } ] - } - } - }, - "cwloggrouplambdaDeleteOldSnapshotsRDS":{ - "Type": "AWS::Logs::LogGroup", - "Description": "Log group for the lambdaDeleteOldSnapshotsRDS function's logs", - "Properties": { - "RetentionInDays": { "Ref": "LambdaCWLogRetention" }, - "LogGroupName": { - "Fn::Sub": [ "/aws/lambda/${func}", { "func": { "Ref" : "LogGroupName" } } ] - } - } - } - }, - "Outputs": { - "BackupFailedTopic": { - "Description": "Subscribe to this topic to receive alerts of failed backups", - "Value": { - "Ref": "topicBackupsFailed" - } - }, - "ShareFailedTopic": { - "Condition": "Share", - "Description": "Subscribe to this topic to receive alerts of failures at sharing snapshots with destination account", - "Value": { - "Ref": "topicShareFailed" - } - }, - "DeleteOldFailedTopic": { - "Condition": "DeleteOld", - "Description": "Subscribe to this topic to receive alerts of failures at deleting old snapshots", - "Value": { - "Ref": "topicDeleteOldFailed" - } - }, - "SourceURL": { - "Description": "For more information and documentation, see the source repository at GitHub.", - "Value": "https://github.com/awslabs/rds-snapshot-tool" - } - }, - "Description": "Snapshots Tool for RDS cross-region and cross-account (source account stack)" -} diff --git a/cftemplates/snapshots_tool_rds_source.yaml b/cftemplates/snapshots_tool_rds_source.yaml new file mode 100644 index 0000000..bb0a3a9 --- /dev/null +++ b/cftemplates/snapshots_tool_rds_source.yaml @@ -0,0 +1,601 @@ +--- +AWSTemplateFormatVersion: '2010-09-09' +Parameters: + CodeBucket: + Type: String + Description: Name of the bucket that contains the lambda functions to deploy. + InstanceNamePattern: + Type: String + Default: ALL_INSTANCES + Description: Python regex for matching cluster identifiers to backup. Use "ALL_INSTANCES" + to back up every RDS instance in the region. + BackupInterval: + Type: Number + Default: '24' + Description: Interval for backups in hours. Default is 24 + DestinationAccount: + Type: Number + Default: '000000000000' + Description: Destination account with no dashes. + ShareSnapshots: + Type: String + Default: 'TRUE' + AllowedValues: + - 'TRUE' + - 'FALSE' + BackupSchedule: + Type: String + Default: 0 1 * * ? * + Description: 'Backup schedule in Cloudwatch Event cron format. Needs to run at + least once for every Interval. The default value runs once every at 1AM UTC. + More information: http://docs.aws.amazon.com/AmazonCloudWatch/latest/events/ScheduledEvents.html' + RetentionDays: + Type: Number + Default: '7' + Description: Number of days to keep snapshots in retention before deleting them + LogLevel: + Type: String + Default: ERROR + Description: Log level for Lambda functions (DEBUG, INFO, WARN, ERROR, CRITICAL + are valid values). + LambdaCWLogRetention: + Type: Number + Default: '7' + Description: Number of days to retain logs from the lambda functions in CloudWatch + Logs + SourceRegionOverride: + Type: String + Default: 'NO' + Description: Set to the region where your RDS instances run, only if such region + does not support Step Functions. Leave as NO otherwise + DeleteOldSnapshots: + Type: String + Default: 'TRUE' + Description: Set to TRUE to enable deletion of snapshot based on RetentionDays. + Set to FALSE to disable + AllowedValues: + - 'TRUE' + - 'FALSE' + TaggedInstance: + Type: String + Default: 'FALSE' + Description: Set to TRUE to filter instances that have tag CopyDBSnapshot set + to True. Set to FALSE to disable + AllowedValues: + - 'TRUE' + - 'FALSE' + LogGroupName: + Type: String + Default: lambdaDeleteOldSnapshotsRDS-source + Description: Name for RDS snapshot log group. +Conditions: + Share: + Fn::Equals: + - Ref: ShareSnapshots + - 'TRUE' + DeleteOld: + Fn::Equals: + - Ref: DeleteOldSnapshots + - 'TRUE' +Resources: + topicBackupsFailed: + Type: AWS::SNS::Topic + Properties: + DisplayName: backups_failed_rds + topicShareFailed: + Type: AWS::SNS::Topic + Properties: + DisplayName: share_failed_rds + topicDeleteOldFailed: + Type: AWS::SNS::Topic + Properties: + DisplayName: delete_old_failed_rds + snspolicySnapshotsRDS: + Type: AWS::SNS::TopicPolicy + Properties: + Topics: + - Ref: topicBackupsFailed + - Ref: topicShareFailed + - Ref: topicDeleteOldFailed + PolicyDocument: + Version: '2008-10-17' + Id: __default_policy_ID + Statement: + - Sid: __default_statement_ID + Effect: Allow + Principal: + AWS: "*" + Action: + - SNS:GetTopicAttributes + - SNS:SetTopicAttributes + - SNS:AddPermission + - SNS:RemovePermission + - SNS:DeleteTopic + - SNS:Subscribe + - SNS:ListSubscriptionsByTopic + - SNS:Publish + - SNS:Receive + Resource: "*" + Condition: + StringEquals: + AWS:SourceOwner: + Ref: AWS::AccountId + alarmcwBackupsFailed: + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: 'true' + ComparisonOperator: GreaterThanOrEqualToThreshold + EvaluationPeriods: '1' + MetricName: ExecutionsFailed + Namespace: AWS/States + Period: '300' + Statistic: Sum + Threshold: '1.0' + AlarmActions: + - Ref: topicBackupsFailed + Dimensions: + - Name: StateMachineArn + Value: + Ref: stateMachineTakeSnapshotsRDS + alarmcwShareFailed: + Condition: Share + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: 'true' + ComparisonOperator: GreaterThanOrEqualToThreshold + EvaluationPeriods: '2' + MetricName: ExecutionsFailed + Namespace: AWS/States + Period: '3600' + Statistic: Sum + Threshold: '2.0' + AlarmActions: + - Ref: topicShareFailed + Dimensions: + - Name: StateMachineArn + Value: + Ref: statemachineShareSnapshotsRDS + alarmcwDeleteOldFailed: + Condition: DeleteOld + Type: AWS::CloudWatch::Alarm + Properties: + ActionsEnabled: 'true' + ComparisonOperator: GreaterThanOrEqualToThreshold + EvaluationPeriods: '2' + MetricName: ExecutionsFailed + Namespace: AWS/States + Period: '3600' + Statistic: Sum + Threshold: '2.0' + AlarmActions: + - Ref: topicDeleteOldFailed + Dimensions: + - Name: StateMachineArn + Value: + Ref: statemachineDeleteOldSnapshotsRDS + iamroleSnapshotsRDS: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_snapshots_rds_cw_logs + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - logs:CreateLogGroup + - logs:CreateLogStream + - logs:PutLogEvents + Resource: arn:aws:logs:*:*:* + - PolicyName: inline_policy_snapshots_rds + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - rds:CreateDBSnapshot + - rds:DeleteDBSnapshot + - rds:DescribeDBInstances + - rds:DescribeDBSnapshots + - rds:ModifyDBSnapshotAttribute + - rds:DescribeDBSnapshotAttributes + - rds:ListTagsForResource + - rds:AddTagsToResource + Resource: "*" + lambdaTakeSnapshotsRDS: + Type: AWS::Lambda::Function + Properties: + Code: + S3Bucket: + Ref: CodeBucket + S3Key: take_snapshots_rds.zip + MemorySize: 512 + Description: 'This functions triggers snapshots creation for RDS instances. + It checks for existing snapshots following the pattern and interval specified + in the environment variables with the following format: -YYYY-MM-DD-HH-MM' + Environment: + Variables: + INTERVAL: + Ref: BackupInterval + PATTERN: + Ref: InstanceNamePattern + LOG_LEVEL: + Ref: LogLevel + REGION_OVERRIDE: + Ref: SourceRegionOverride + TAGGEDINSTANCE: + Ref: TaggedInstance + Role: + Fn::GetAtt: + - iamroleSnapshotsRDS + - Arn + Runtime: python3.7 + Handler: lambda_function.lambda_handler + Timeout: 300 + lambdaShareSnapshotsRDS: + Type: AWS::Lambda::Function + Condition: Share + Properties: + Code: + S3Bucket: + Ref: CodeBucket + S3Key: share_snapshots_rds.zip + MemorySize: 512 + Description: 'This function shares snapshots created by the take_snapshots_rds + function with DEST_ACCOUNT specified in the environment variables. ' + Environment: + Variables: + DEST_ACCOUNT: + Ref: DestinationAccount + LOG_LEVEL: + Ref: LogLevel + PATTERN: + Ref: InstanceNamePattern + REGION_OVERRIDE: + Ref: SourceRegionOverride + Role: + Fn::GetAtt: + - iamroleSnapshotsRDS + - Arn + Runtime: python3.7 + Handler: lambda_function.lambda_handler + Timeout: 300 + lambdaDeleteOldSnapshotsRDS: + Type: AWS::Lambda::Function + Condition: DeleteOld + Properties: + Code: + S3Bucket: + Ref: CodeBucket + S3Key: delete_old_snapshots_rds.zip + MemorySize: 512 + Description: 'This function deletes snapshots created by the take_snapshots_rds + function. ' + Environment: + Variables: + RETENTION_DAYS: + Ref: RetentionDays + PATTERN: + Ref: InstanceNamePattern + LOG_LEVEL: + Ref: LogLevel + REGION_OVERRIDE: + Ref: SourceRegionOverride + Role: + Fn::GetAtt: + - iamroleSnapshotsRDS + - Arn + Runtime: python3.7 + Handler: lambda_function.lambda_handler + Timeout: 300 + iamroleStateExecution: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: + Fn::Join: + - '' + - - states. + - Ref: AWS::Region + - ".amazonaws.com" + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_snapshots_rds + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: "*" + stateMachineTakeSnapshotsRDS: + Type: AWS::StepFunctions::StateMachine + Properties: + DefinitionString: + Fn::Join: + - '' + - - Fn::Join: + - "\n" + - - ' {"Comment":"Triggers snapshot backup for RDS instances",' + - ' "StartAt":"TakeSnapshots",' + - ' "States":{' + - ' "TakeSnapshots":{' + - ' "Type":"Task",' + - ' "Resource": ' + - "\"" + - Fn::GetAtt: + - lambdaTakeSnapshotsRDS + - Arn + - |- + " + , + - Fn::Join: + - "\n" + - - ' "Retry":[' + - " {" + - ' "ErrorEquals":[ ' + - ' "SnapshotToolException"' + - " ]," + - ' "IntervalSeconds":300,' + - ' "MaxAttempts":20,' + - ' "BackoffRate":1' + - " }," + - " {" + - ' "ErrorEquals":[ ' + - ' "States.ALL"], ' + - ' "IntervalSeconds": 30,' + - ' "MaxAttempts": 20,' + - ' "BackoffRate": 1' + - " }" + - " ]," + - ' "End": true ' + - " }" + - " }}" + RoleArn: + Fn::GetAtt: + - iamroleStateExecution + - Arn + statemachineShareSnapshotsRDS: + Type: AWS::StepFunctions::StateMachine + Condition: Share + Properties: + DefinitionString: + Fn::Join: + - '' + - - Fn::Join: + - "\n" + - - ' {"Comment":"Shares snapshots with DEST_ACCOUNT",' + - ' "StartAt":"ShareSnapshots",' + - ' "States":{' + - ' "ShareSnapshots":{' + - ' "Type":"Task",' + - ' "Resource": ' + - "\"" + - Fn::GetAtt: + - lambdaShareSnapshotsRDS + - Arn + - |- + " + , + - Fn::Join: + - "\n" + - - ' "Retry":[' + - " {" + - ' "ErrorEquals":[ ' + - ' "SnapshotToolException"' + - " ]," + - ' "IntervalSeconds":300,' + - ' "MaxAttempts":3,' + - ' "BackoffRate":1' + - " }," + - " {" + - ' "ErrorEquals":[ ' + - ' "States.ALL"], ' + - ' "IntervalSeconds": 30,' + - ' "MaxAttempts": 20,' + - ' "BackoffRate": 1' + - " }" + - " ]," + - ' "End": true ' + - " }" + - " }}" + RoleArn: + Fn::GetAtt: + - iamroleStateExecution + - Arn + statemachineDeleteOldSnapshotsRDS: + Type: AWS::StepFunctions::StateMachine + Condition: DeleteOld + Properties: + DefinitionString: + Fn::Join: + - '' + - - Fn::Join: + - "\n" + - - ' {"Comment":"DeleteOld management for RDS snapshots",' + - ' "StartAt":"DeleteOld",' + - ' "States":{' + - ' "DeleteOld":{' + - ' "Type":"Task",' + - ' "Resource": ' + - "\"" + - Fn::GetAtt: + - lambdaDeleteOldSnapshotsRDS + - Arn + - |- + " + , + - Fn::Join: + - "\n" + - - ' "Retry":[' + - " {" + - ' "ErrorEquals":[ ' + - ' "SnapshotToolException"' + - " ]," + - ' "IntervalSeconds":300,' + - ' "MaxAttempts":7,' + - ' "BackoffRate":1' + - " }," + - " {" + - ' "ErrorEquals":[ ' + - ' "States.ALL"], ' + - ' "IntervalSeconds": 30,' + - ' "MaxAttempts": 20,' + - ' "BackoffRate": 1' + - " }" + - " ]," + - ' "End": true ' + - " }" + - " }}" + RoleArn: + Fn::GetAtt: + - iamroleStateExecution + - Arn + iamroleStepInvocation: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: events.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: inline_policy_state_invocation + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - states:StartExecution + Resource: "*" + cwEventBackupRDS: + Type: AWS::Events::Rule + Properties: + Description: Triggers the TakeSnapshotsRDS state machine + ScheduleExpression: + Fn::Join: + - '' + - - cron( + - Ref: BackupSchedule + - ")" + State: ENABLED + Targets: + - Arn: + Ref: stateMachineTakeSnapshotsRDS + Id: Target1 + RoleArn: + Fn::GetAtt: + - iamroleStepInvocation + - Arn + cwEventShareSnapshotsRDS: + Type: AWS::Events::Rule + Condition: Share + Properties: + Description: Triggers the ShareSnapshotsRDS state machine + ScheduleExpression: + Fn::Join: + - '' + - - cron( + - "/10 * * * ? *" + - ")" + State: ENABLED + Targets: + - Arn: + Ref: statemachineShareSnapshotsRDS + Id: Target1 + RoleArn: + Fn::GetAtt: + - iamroleStepInvocation + - Arn + cwEventDeleteOldSnapshotsRDS: + Type: AWS::Events::Rule + Condition: DeleteOld + Properties: + Description: Triggers the DeleteOldSnapshotsRDS state machine + ScheduleExpression: + Fn::Join: + - '' + - - cron( + - 0 /1 * * ? * + - ")" + State: ENABLED + Targets: + - Arn: + Ref: statemachineDeleteOldSnapshotsRDS + Id: Target1 + RoleArn: + Fn::GetAtt: + - iamroleStepInvocation + - Arn + cwloggrouplambdaTakeSnapshotsRDS: + Type: AWS::Logs::LogGroup + Description: Log group for the lambdaTakeSnapshotsRDS function's logs + DependsOn: lambdaTakeSnapshotsRDS + Properties: + RetentionInDays: + Ref: LambdaCWLogRetention + LogGroupName: + Fn::Sub: + - "/aws/lambda/${func}" + - func: + Ref: lambdaTakeSnapshotsRDS + cwloggrouplambdaShareSnapshotsRDS: + Condition: Share + Type: AWS::Logs::LogGroup + Description: Log group for the lambdaShareSnapshotsRDS function's logs + DependsOn: lambdaShareSnapshotsRDS + Properties: + RetentionInDays: + Ref: LambdaCWLogRetention + LogGroupName: + Fn::Sub: + - "/aws/lambda/${func}" + - func: + Ref: lambdaShareSnapshotsRDS + cwloggrouplambdaDeleteOldSnapshotsRDS: + Type: AWS::Logs::LogGroup + Description: Log group for the lambdaDeleteOldSnapshotsRDS function's logs + Properties: + RetentionInDays: + Ref: LambdaCWLogRetention + LogGroupName: + Fn::Sub: + - "/aws/lambda/${func}" + - func: + Ref: LogGroupName +Outputs: + BackupFailedTopic: + Description: Subscribe to this topic to receive alerts of failed backups + Value: + Ref: topicBackupsFailed + ShareFailedTopic: + Condition: Share + Description: Subscribe to this topic to receive alerts of failures at sharing + snapshots with destination account + Value: + Ref: topicShareFailed + DeleteOldFailedTopic: + Condition: DeleteOld + Description: Subscribe to this topic to receive alerts of failures at deleting + old snapshots + Value: + Ref: topicDeleteOldFailed + SourceURL: + Description: For more information and documentation, see the source repository + at GitHub. + Value: https://github.com/awslabs/rds-snapshot-tool +Description: Snapshots Tool for RDS cross-region and cross-account (source account + stack) diff --git a/lambda/.gitignore b/lambda/.gitignore deleted file mode 100644 index b7b8780..0000000 --- a/lambda/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -._* -*.zip diff --git a/lambda/Makefile b/lambda/Makefile deleted file mode 100644 index f284746..0000000 --- a/lambda/Makefile +++ /dev/null @@ -1,51 +0,0 @@ -# -# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. -# Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at -# http://aws.amazon.com/apache2.0/ -# or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - -# Makefile for generating zip files for lambda functions and then copying them -# to S3 for deployment. This Makefile will NOT WORK unless you fill in the S3DEST -# and AWSARGS variables below. Once those parameters are established, simply type -# 'make' or 'gmake' (depending on your UNIX-like OS) and it will build. -# -# Behaviour: -# Creates a file named ._foo.whatever based on foo.whatever.Uploads foo.whatever to -# the S3 bucket. The ._ file is a hack to figure out whether the file has -# been modified since the last time we uploaded to s3. - -# Override S3 destination by changing this variable or setting it in the -# environment -S3DEST?=[YOUR BUCKET HERE] - -# Set these if, for example, you use profiles on the AWS command line -# or if your 'aws' executable is in a weird place. -AWSARGS=--region [YOUR REGION] --profile [YOUR PROFILE, or 'default', or remove this] -AWSCMD=aws -ZIPCMD=zip - -# disable all implicit make rules -.SUFFIXES: - -# if you define "._foo" as a file on this line, then it will zip up a -# folder called foo, adding a standard file into it to make foo.zip. -all: ._copy_snapshots_dest_rds \ - ._copy_snapshots_no_x_account_rds \ - ._delete_old_snapshots_dest_rds \ - ._delete_old_snapshots_no_x_account_rds \ - ._delete_old_snapshots_rds \ - ._share_snapshots_rds \ - ._take_snapshots_rds - -clean: - rm -f ._* - -._%: %.zip - "$(AWSCMD)" $(AWSARGS) s3 cp "$<" "s3://$(S3DEST)" \ - --grants read=uri=http://acs.amazonaws.com/groups/global/AllUsers - cp "$<" "$@" - -# This rule is a BSD make style rule that says "to make foo.zip, call -# 'zip -jqr foo snapshot_tool_utils.py'" -%.zip: % - $(ZIPCMD) -jqr "$@" "$<" snapshots_tool_utils.py \ No newline at end of file diff --git a/lambda/copy_snapshots_dest_rds/.DS_Store b/lambda/copy_snapshots_dest_rds/.DS_Store new file mode 100644 index 0000000..16bdc96 Binary files /dev/null and b/lambda/copy_snapshots_dest_rds/.DS_Store differ diff --git a/lambda/snapshots_tool_utils.py b/lambda/copy_snapshots_dest_rds/snapshots_tool_utils.py similarity index 100% rename from lambda/snapshots_tool_utils.py rename to lambda/copy_snapshots_dest_rds/snapshots_tool_utils.py diff --git a/lambda/copy_snapshots_no_x_account_rds/.DS_Store b/lambda/copy_snapshots_no_x_account_rds/.DS_Store new file mode 100644 index 0000000..a36d3b6 Binary files /dev/null and b/lambda/copy_snapshots_no_x_account_rds/.DS_Store differ diff --git a/lambda/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py b/lambda/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda/delete_old_snapshots_dest_rds/.DS_Store b/lambda/delete_old_snapshots_dest_rds/.DS_Store new file mode 100644 index 0000000..7c18289 Binary files /dev/null and b/lambda/delete_old_snapshots_dest_rds/.DS_Store differ diff --git a/lambda/delete_old_snapshots_dest_rds/snapshots_tool_utils.py b/lambda/delete_old_snapshots_dest_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/delete_old_snapshots_dest_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda/delete_old_snapshots_no_x_account_rds/.DS_Store b/lambda/delete_old_snapshots_no_x_account_rds/.DS_Store new file mode 100644 index 0000000..0d3e114 Binary files /dev/null and b/lambda/delete_old_snapshots_no_x_account_rds/.DS_Store differ diff --git a/lambda/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py b/lambda/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda/delete_old_snapshots_rds/.DS_Store b/lambda/delete_old_snapshots_rds/.DS_Store new file mode 100644 index 0000000..d86f46b Binary files /dev/null and b/lambda/delete_old_snapshots_rds/.DS_Store differ diff --git a/lambda/delete_old_snapshots_rds/snapshots_tool_utils.py b/lambda/delete_old_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/delete_old_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda/packaged_lambdas/.DS_Store b/lambda/packaged_lambdas/.DS_Store new file mode 100644 index 0000000..5a8b486 Binary files /dev/null and b/lambda/packaged_lambdas/.DS_Store differ diff --git a/lambda/share_snapshots_rds/.DS_Store b/lambda/share_snapshots_rds/.DS_Store new file mode 100644 index 0000000..7c049ee Binary files /dev/null and b/lambda/share_snapshots_rds/.DS_Store differ diff --git a/lambda/share_snapshots_rds/snapshots_tool_utils.py b/lambda/share_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/share_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda/take_snapshots_rds/.DS_Store b/lambda/take_snapshots_rds/.DS_Store new file mode 100644 index 0000000..8973dbb Binary files /dev/null and b/lambda/take_snapshots_rds/.DS_Store differ diff --git a/lambda/take_snapshots_rds/snapshots_tool_utils.py b/lambda/take_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..5a797d9 --- /dev/null +++ b/lambda/take_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response diff --git a/lambda_code/.DS_Store b/lambda_code/.DS_Store new file mode 100644 index 0000000..f8ff79e Binary files /dev/null and b/lambda_code/.DS_Store differ diff --git a/lambda_code/copy_snapshots_dest_rds/.DS_Store b/lambda_code/copy_snapshots_dest_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/copy_snapshots_dest_rds/.DS_Store differ diff --git a/lambda/copy_snapshots_dest_rds/lambda_function.py b/lambda_code/copy_snapshots_dest_rds/lambda_function.py similarity index 97% rename from lambda/copy_snapshots_dest_rds/lambda_function.py rename to lambda_code/copy_snapshots_dest_rds/lambda_function.py index 56fd7fb..ad28fe7 100644 --- a/lambda/copy_snapshots_dest_rds/lambda_function.py +++ b/lambda_code/copy_snapshots_dest_rds/lambda_function.py @@ -1,116 +1,116 @@ -''' -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -''' - -# copy_snapshots_dest_rds -# This lambda function will copy shared RDS snapshots that match the regex specified in the environment variable SNAPSHOT_PATTERN, into the account where it runs. If the snapshot is shared and exists in the local region, it will copy it to the region specified in the environment variable DEST_REGION. If it finds that the snapshots are shared, exist in the local and destination regions, it will delete them from the local region. Copying snapshots cross-account and cross-region need to be separate operations. This function will need to run as many times necessary for the workflow to complete. -# Set SNAPSHOT_PATTERN to a regex that matches your RDS Instance identifiers -# Set DEST_REGION to the destination AWS region -import boto3 -from datetime import datetime -import time -import os -import logging -import re -from snapshots_tool_utils import * - -# Initialize everything -LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() -PATTERN = os.getenv('SNAPSHOT_PATTERN', 'ALL_SNAPSHOTS') -DESTINATION_REGION = os.getenv('DEST_REGION').strip() -RETENTION_DAYS = int(os.getenv('RETENTION_DAYS')) - -if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': - REGION = os.getenv('REGION_OVERRIDE').strip() -else: - REGION = os.getenv('AWS_DEFAULT_REGION') - - -logger = logging.getLogger() -logger.setLevel(LOGLEVEL.upper()) - - -def lambda_handler(event, context): - # Describe all snapshots - pending_copies = 0 - client = boto3.client('rds', region_name=REGION) - response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots', IncludeShared=True) - - shared_snapshots = get_shared_snapshots(PATTERN, response) - own_snapshots = get_own_snapshots_dest(PATTERN, response) - - # Get list of snapshots in DEST_REGION - client_dest = boto3.client('rds', region_name=DESTINATION_REGION) - response_dest = paginate_api_call(client_dest, 'describe_db_snapshots', 'DBSnapshots') - own_dest_snapshots = get_own_snapshots_dest(PATTERN, response_dest) - - for shared_identifier, shared_attributes in shared_snapshots.items(): - - if shared_identifier not in own_snapshots.keys() and shared_identifier not in own_dest_snapshots.keys(): - # Check date - creation_date = get_timestamp(shared_identifier, shared_snapshots) - if creation_date: - time_difference = datetime.now() - creation_date - days_difference = time_difference.total_seconds() / 3600 / 24 - - # Only copy if it's newer than RETENTION_DAYS - if days_difference < RETENTION_DAYS: - - # Copy to own account - try: - copy_local(shared_identifier, shared_attributes) - - except Exception as e: - pending_copies += 1 - logger.error('Local copy pending: %s (%s)' % (shared_identifier, e)) - - else: - if REGION != DESTINATION_REGION: - pending_copies += 1 - logger.error('Remote copy pending: %s' % shared_identifier) - - else: - logger.info('Not copying %s locally. Older than %s days' % (shared_identifier, RETENTION_DAYS)) - - else: - logger.info('Not copying %s locally. No valid timestamp' % shared_identifier) - - - # Copy to DESTINATION_REGION - elif shared_identifier not in own_dest_snapshots.keys() and shared_identifier in own_snapshots.keys() and REGION != DESTINATION_REGION: - if own_snapshots[shared_identifier]['Status'] == 'available': - try: - copy_remote(shared_identifier, own_snapshots[shared_identifier]) - - except Exception as e: - pending_copies += 1 - logger.error('Remote copy pending: %s: %s (%s)' % ( - shared_identifier, own_snapshots[shared_identifier]['Arn'], e)) - else: - pending_copies += 1 - logger.error('Remote copy pending: %s: %s' % ( - shared_identifier, own_snapshots[shared_identifier]['Arn'])) - - # Delete local snapshots - elif shared_identifier in own_dest_snapshots.keys() and shared_identifier in own_snapshots.keys() and own_dest_snapshots[shared_identifier]['Status'] == 'available' and REGION != DESTINATION_REGION: - - response = client.delete_db_snapshot( - DBSnapshotIdentifier=shared_identifier - ) - - logger.info('Deleting local snapshot: %s' % shared_identifier) - - if pending_copies > 0: - log_message = 'Copies pending: %s. Needs retrying' % pending_copies - logger.error(log_message) - raise SnapshotToolException(log_message) - - -if __name__ == '__main__': - lambda_handler(None, None) +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + +# copy_snapshots_dest_rds +# This lambda function will copy shared RDS snapshots that match the regex specified in the environment variable SNAPSHOT_PATTERN, into the account where it runs. If the snapshot is shared and exists in the local region, it will copy it to the region specified in the environment variable DEST_REGION. If it finds that the snapshots are shared, exist in the local and destination regions, it will delete them from the local region. Copying snapshots cross-account and cross-region need to be separate operations. This function will need to run as many times necessary for the workflow to complete. +# Set SNAPSHOT_PATTERN to a regex that matches your RDS Instance identifiers +# Set DEST_REGION to the destination AWS region +import boto3 +from datetime import datetime +import time +import os +import logging +import re +from snapshots_tool_utils import * + +# Initialize everything +LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() +PATTERN = os.getenv('SNAPSHOT_PATTERN', 'ALL_SNAPSHOTS') +DESTINATION_REGION = os.getenv('DEST_REGION').strip() +RETENTION_DAYS = int(os.getenv('RETENTION_DAYS')) + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + REGION = os.getenv('REGION_OVERRIDE').strip() +else: + REGION = os.getenv('AWS_DEFAULT_REGION') + + +logger = logging.getLogger() +logger.setLevel(LOGLEVEL.upper()) + + +def lambda_handler(event, context): + # Describe all snapshots + pending_copies = 0 + client = boto3.client('rds', region_name=REGION) + response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots', IncludeShared=True) + + shared_snapshots = get_shared_snapshots(PATTERN, response) + own_snapshots = get_own_snapshots_dest(PATTERN, response) + + # Get list of snapshots in DEST_REGION + client_dest = boto3.client('rds', region_name=DESTINATION_REGION) + response_dest = paginate_api_call(client_dest, 'describe_db_snapshots', 'DBSnapshots') + own_dest_snapshots = get_own_snapshots_dest(PATTERN, response_dest) + + for shared_identifier, shared_attributes in shared_snapshots.items(): + + if shared_identifier not in own_snapshots.keys() and shared_identifier not in own_dest_snapshots.keys(): + # Check date + creation_date = get_timestamp(shared_identifier, shared_snapshots) + if creation_date: + time_difference = datetime.now() - creation_date + days_difference = time_difference.total_seconds() / 3600 / 24 + + # Only copy if it's newer than RETENTION_DAYS + if days_difference < RETENTION_DAYS: + + # Copy to own account + try: + copy_local(shared_identifier, shared_attributes) + + except Exception as e: + pending_copies += 1 + logger.error('Local copy pending: %s (%s)' % (shared_identifier, e)) + + else: + if REGION != DESTINATION_REGION: + pending_copies += 1 + logger.error('Remote copy pending: %s' % shared_identifier) + + else: + logger.info('Not copying %s locally. Older than %s days' % (shared_identifier, RETENTION_DAYS)) + + else: + logger.info('Not copying %s locally. No valid timestamp' % shared_identifier) + + + # Copy to DESTINATION_REGION + elif shared_identifier not in own_dest_snapshots.keys() and shared_identifier in own_snapshots.keys() and REGION != DESTINATION_REGION: + if own_snapshots[shared_identifier]['Status'] == 'available': + try: + copy_remote(shared_identifier, own_snapshots[shared_identifier]) + + except Exception as e: + pending_copies += 1 + logger.error('Remote copy pending: %s: %s (%s)' % ( + shared_identifier, own_snapshots[shared_identifier]['Arn'], e)) + else: + pending_copies += 1 + logger.error('Remote copy pending: %s: %s' % ( + shared_identifier, own_snapshots[shared_identifier]['Arn'])) + + # Delete local snapshots + elif shared_identifier in own_dest_snapshots.keys() and shared_identifier in own_snapshots.keys() and own_dest_snapshots[shared_identifier]['Status'] == 'available' and REGION != DESTINATION_REGION: + + response = client.delete_db_snapshot( + DBSnapshotIdentifier=shared_identifier + ) + + logger.info('Deleting local snapshot: %s' % shared_identifier) + + if pending_copies > 0: + log_message = 'Copies pending: %s. Needs retrying' % pending_copies + logger.error(log_message) + raise SnapshotToolException(log_message) + + +if __name__ == '__main__': + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/copy_snapshots_dest_rds/snapshots_tool_utils.py b/lambda_code/copy_snapshots_dest_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/copy_snapshots_dest_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda_code/copy_snapshots_no_x_account_rds/.DS_Store b/lambda_code/copy_snapshots_no_x_account_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/copy_snapshots_no_x_account_rds/.DS_Store differ diff --git a/lambda/copy_snapshots_no_x_account_rds/lambda_function.py b/lambda_code/copy_snapshots_no_x_account_rds/lambda_function.py similarity index 99% rename from lambda/copy_snapshots_no_x_account_rds/lambda_function.py rename to lambda_code/copy_snapshots_no_x_account_rds/lambda_function.py index 007f908..4542ef2 100644 --- a/lambda/copy_snapshots_no_x_account_rds/lambda_function.py +++ b/lambda_code/copy_snapshots_no_x_account_rds/lambda_function.py @@ -92,4 +92,4 @@ def lambda_handler(event, context): if __name__ == '__main__': - lambda_handler(None, None) + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py b/lambda_code/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/copy_snapshots_no_x_account_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda_code/delete_old_snapshots_dest_rds/.DS_Store b/lambda_code/delete_old_snapshots_dest_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/delete_old_snapshots_dest_rds/.DS_Store differ diff --git a/lambda/delete_old_snapshots_dest_rds/lambda_function.py b/lambda_code/delete_old_snapshots_dest_rds/lambda_function.py similarity index 96% rename from lambda/delete_old_snapshots_dest_rds/lambda_function.py rename to lambda_code/delete_old_snapshots_dest_rds/lambda_function.py index 48b1704..9a8f464 100644 --- a/lambda/delete_old_snapshots_dest_rds/lambda_function.py +++ b/lambda_code/delete_old_snapshots_dest_rds/lambda_function.py @@ -1,88 +1,88 @@ -''' -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -''' - -# delete_old_snapshots_dest_rds -# This lambda function will delete manual RDS snapshots that have expired in the region specified in the environment variable DEST_REGION, and according to the environment variables SNAPSHOT_PATTERN and RETENTION_DAYS. -# Set SNAPSHOT_PATTERN to a regex that matches your RDS Instance identifiers -# Set DEST_REGION to the destination AWS region -# Set RETENTION_DAYS to the amount of days snapshots need to be kept before deleting -import boto3 -import time -import os -import logging -from datetime import datetime -import re -from snapshots_tool_utils import * - - -# Initialize everything -DEST_REGION = os.getenv('DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() -LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() -PATTERN = os.getenv('SNAPSHOT_PATTERN', 'ALL_SNAPSHOTS') -RETENTION_DAYS = int(os.getenv('RETENTION_DAYS')) -TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' - -logger = logging.getLogger() -logger.setLevel(LOGLEVEL.upper()) - - - -def lambda_handler(event, context): - delete_pending = 0 - # Search for all snapshots - client = boto3.client('rds', region_name=DEST_REGION) - response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots') - - # Filter out the ones not created automatically or with other methods - filtered_list = get_own_snapshots_dest(PATTERN, response) - - for snapshot in filtered_list.keys(): - - creation_date = get_timestamp(snapshot, filtered_list) - - if creation_date: - snapshot_arn = filtered_list[snapshot]['Arn'] - response_tags = client.list_tags_for_resource( - ResourceName=snapshot_arn) - - if search_tag_copied(response_tags): - difference = datetime.now() - creation_date - days_difference = difference.total_seconds() / 3600 / 24 - - # if we are past RETENTION_DAYS - if days_difference > RETENTION_DAYS: - # delete it - logger.info('Deleting %s. %s days old' % - (snapshot, days_difference)) - - try: - client.delete_db_snapshot( - DBSnapshotIdentifier=snapshot) - - except Exception as e: - delete_pending += 1 - logger.info('Could not delete %s (%s)' % (snapshot, e)) - - else: - logger.info('Not deleting %s. Only %s days old' % - (snapshot, days_difference)) - - else: - logger.info( - 'Not deleting %s. Did not find correct tag' % snapshot) - - if delete_pending > 0: - log_message = 'Snapshots pending delete: %s' % delete_pending - logger.error(log_message) - raise SnapshotToolException(log_message) - - -if __name__ == '__main__': - lambda_handler(None, None) +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + +# delete_old_snapshots_dest_rds +# This lambda function will delete manual RDS snapshots that have expired in the region specified in the environment variable DEST_REGION, and according to the environment variables SNAPSHOT_PATTERN and RETENTION_DAYS. +# Set SNAPSHOT_PATTERN to a regex that matches your RDS Instance identifiers +# Set DEST_REGION to the destination AWS region +# Set RETENTION_DAYS to the amount of days snapshots need to be kept before deleting +import boto3 +import time +import os +import logging +from datetime import datetime +import re +from snapshots_tool_utils import * + + +# Initialize everything +DEST_REGION = os.getenv('DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() +LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() +PATTERN = os.getenv('SNAPSHOT_PATTERN', 'ALL_SNAPSHOTS') +RETENTION_DAYS = int(os.getenv('RETENTION_DAYS')) +TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +logger = logging.getLogger() +logger.setLevel(LOGLEVEL.upper()) + + + +def lambda_handler(event, context): + delete_pending = 0 + # Search for all snapshots + client = boto3.client('rds', region_name=DEST_REGION) + response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots') + + # Filter out the ones not created automatically or with other methods + filtered_list = get_own_snapshots_dest(PATTERN, response) + + for snapshot in filtered_list.keys(): + + creation_date = get_timestamp(snapshot, filtered_list) + + if creation_date: + snapshot_arn = filtered_list[snapshot]['Arn'] + response_tags = client.list_tags_for_resource( + ResourceName=snapshot_arn) + + if search_tag_copied(response_tags): + difference = datetime.now() - creation_date + days_difference = difference.total_seconds() / 3600 / 24 + + # if we are past RETENTION_DAYS + if days_difference > RETENTION_DAYS: + # delete it + logger.info('Deleting %s. %s days old' % + (snapshot, days_difference)) + + try: + client.delete_db_snapshot( + DBSnapshotIdentifier=snapshot) + + except Exception as e: + delete_pending += 1 + logger.info('Could not delete %s (%s)' % (snapshot, e)) + + else: + logger.info('Not deleting %s. Only %s days old' % + (snapshot, days_difference)) + + else: + logger.info( + 'Not deleting %s. Did not find correct tag' % snapshot) + + if delete_pending > 0: + log_message = 'Snapshots pending delete: %s' % delete_pending + logger.error(log_message) + raise SnapshotToolException(log_message) + + +if __name__ == '__main__': + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/delete_old_snapshots_dest_rds/snapshots_tool_utils.py b/lambda_code/delete_old_snapshots_dest_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/delete_old_snapshots_dest_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda_code/delete_old_snapshots_no_x_account_rds/.DS_Store b/lambda_code/delete_old_snapshots_no_x_account_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/delete_old_snapshots_no_x_account_rds/.DS_Store differ diff --git a/lambda/delete_old_snapshots_no_x_account_rds/lambda_function.py b/lambda_code/delete_old_snapshots_no_x_account_rds/lambda_function.py similarity index 99% rename from lambda/delete_old_snapshots_no_x_account_rds/lambda_function.py rename to lambda_code/delete_old_snapshots_no_x_account_rds/lambda_function.py index 3c12478..1c1c0ec 100644 --- a/lambda/delete_old_snapshots_no_x_account_rds/lambda_function.py +++ b/lambda_code/delete_old_snapshots_no_x_account_rds/lambda_function.py @@ -95,4 +95,4 @@ def lambda_handler(event, context): if __name__ == '__main__': - lambda_handler(None, None) + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py b/lambda_code/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/delete_old_snapshots_no_x_account_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda/delete_old_snapshots_rds/lambda_function.py b/lambda_code/delete_old_snapshots_rds/lambda_function.py similarity index 97% rename from lambda/delete_old_snapshots_rds/lambda_function.py rename to lambda_code/delete_old_snapshots_rds/lambda_function.py index d6aee96..a6c51ec 100644 --- a/lambda/delete_old_snapshots_rds/lambda_function.py +++ b/lambda_code/delete_old_snapshots_rds/lambda_function.py @@ -1,80 +1,79 @@ -''' -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -''' - -# delete_old_snapshots_rds -# This Lambda function will delete snapshots that have expired and match the regex set in the PATTERN environment variable. It will also look for a matching timestamp in the following format: YYYY-MM-DD-HH-mm -# Set PATTERN to a regex that matches your RDS Instance identifiers -import boto3 -from datetime import datetime -import time -import os -import logging -import re -from snapshots_tool_utils import * - -LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() -PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') -RETENTION_DAYS = int(os.getenv('RETENTION_DAYS', '7')) -TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' - -if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': - REGION = os.getenv('REGION_OVERRIDE').strip() -else: - REGION = os.getenv('AWS_DEFAULT_REGION') - - -logger = logging.getLogger() -logger.setLevel(LOGLEVEL.upper()) - - -def lambda_handler(event, context): - pending_delete = 0 - client = boto3.client('rds', region_name=REGION) - response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots') - - filtered_list = get_own_snapshots_source(PATTERN, response) - - for snapshot in filtered_list.keys(): - - creation_date = get_timestamp(snapshot, filtered_list) - - if creation_date: - difference = datetime.now() - creation_date - days_difference = difference.total_seconds() / 3600 / 24 - logger.debug('%s created %s days ago' % - (snapshot, days_difference)) - - # if we are past RETENTION_DAYS - if days_difference > RETENTION_DAYS: - # delete it - logger.info('Deleting %s' % snapshot) - - try: - client.delete_db_snapshot( - DBSnapshotIdentifier=snapshot) - - except Exception as e: - pending_delete += 1 - logger.info('Could not delete %s (%s)' % (snapshot, e)) - - else: - logger.info('Not deleting %s. Created only %s' % (snapshot, days_difference)) - - - if pending_delete > 0: - message = 'Snapshots pending delete: %s' % pending_delete - logger.error(message) - raise SnapshotToolException(message) - - -if __name__ == '__main__': - lambda_handler(None, None) - - +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + +# delete_old_snapshots_rds +# This Lambda function will delete snapshots that have expired and match the regex set in the PATTERN environment variable. It will also look for a matching timestamp in the following format: YYYY-MM-DD-HH-mm +# Set PATTERN to a regex that matches your RDS Instance identifiers +import boto3 +from datetime import datetime +import time +import os +import logging +import re +from snapshots_tool_utils import * + +LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() +PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') +RETENTION_DAYS = int(os.getenv('RETENTION_DAYS', '7')) +TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + REGION = os.getenv('REGION_OVERRIDE').strip() +else: + REGION = os.getenv('AWS_DEFAULT_REGION') + + +logger = logging.getLogger() +logger.setLevel(LOGLEVEL.upper()) + + +def lambda_handler(event, context): + pending_delete = 0 + client = boto3.client('rds', region_name=REGION) + response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots') + + filtered_list = get_own_snapshots_source(PATTERN, response) + + for snapshot in filtered_list.keys(): + + creation_date = get_timestamp(snapshot, filtered_list) + + if creation_date: + difference = datetime.now() - creation_date + days_difference = difference.total_seconds() / 3600 / 24 + logger.debug('%s created %s days ago' % + (snapshot, days_difference)) + + # if we are past RETENTION_DAYS + if days_difference > RETENTION_DAYS: + # delete it + logger.info('Deleting %s' % snapshot) + + try: + client.delete_db_snapshot( + DBSnapshotIdentifier=snapshot) + + except Exception as e: + pending_delete += 1 + logger.info('Could not delete %s (%s)' % (snapshot, e)) + + else: + logger.info('Not deleting %s. Created only %s' % (snapshot, days_difference)) + + + if pending_delete > 0: + message = 'Snapshots pending delete: %s' % pending_delete + logger.error(message) + raise SnapshotToolException(message) + + +if __name__ == '__main__': + lambda_handler(None, None) + diff --git a/lambda_code/delete_old_snapshots_rds/snapshots_tool_utils.py b/lambda_code/delete_old_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/delete_old_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda_code/share_snapshots_rds/.DS_Store b/lambda_code/share_snapshots_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/share_snapshots_rds/.DS_Store differ diff --git a/lambda/share_snapshots_rds/lambda_function.py b/lambda_code/share_snapshots_rds/lambda_function.py similarity index 96% rename from lambda/share_snapshots_rds/lambda_function.py rename to lambda_code/share_snapshots_rds/lambda_function.py index 37ec37d..d47de19 100644 --- a/lambda/share_snapshots_rds/lambda_function.py +++ b/lambda_code/share_snapshots_rds/lambda_function.py @@ -1,73 +1,73 @@ -''' -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -''' - -# share_snapshots_rds -# This Lambda function shares snapshots created by aurora_take_snapshot with the account set in the environment variable DEST_ACCOUNT -# It will only share snapshots tagged with shareAndCopy and a value of YES -import boto3 -from datetime import datetime -import time -import os -import logging -import re -from snapshots_tool_utils import * - - -# Initialize from environment variable -LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() -DEST_ACCOUNTID = str(os.getenv('DEST_ACCOUNT')).strip() -PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') - -if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': - REGION = os.getenv('REGION_OVERRIDE').strip() -else: - REGION = os.getenv('AWS_DEFAULT_REGION') - -SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] - -logger = logging.getLogger() -logger.setLevel(LOGLEVEL.upper()) - - - -def lambda_handler(event, context): - pending_snapshots = 0 - client = boto3.client('rds', region_name=REGION) - response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots', SnapshotType='manual') - filtered = get_own_snapshots_source(PATTERN, response) - - # Search all snapshots for the correct tag - for snapshot_identifier,snapshot_object in filtered.items(): - snapshot_arn = snapshot_object['Arn'] - response_tags = client.list_tags_for_resource( - ResourceName=snapshot_arn) - - if snapshot_object['Status'].lower() == 'available' and search_tag_shared(response_tags): - try: - # Share snapshot with dest_account - response_modify = client.modify_db_snapshot_attribute( - DBSnapshotIdentifier=snapshot_identifier, - AttributeName='restore', - ValuesToAdd=[ - DEST_ACCOUNTID - ] - ) - except Exception as e: - logger.error('Exception sharing %s (%s)' % (snapshot_identifier, e)) - pending_snapshots += 1 - - if pending_snapshots > 0: - log_message = 'Could not share all snapshots. Pending: %s' % pending_snapshots - logger.error(log_message) - raise SnapshotToolException(log_message) - - -if __name__ == '__main__': - lambda_handler(None, None) +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + +# share_snapshots_rds +# This Lambda function shares snapshots created by aurora_take_snapshot with the account set in the environment variable DEST_ACCOUNT +# It will only share snapshots tagged with shareAndCopy and a value of YES +import boto3 +from datetime import datetime +import time +import os +import logging +import re +from snapshots_tool_utils import * + + +# Initialize from environment variable +LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() +DEST_ACCOUNTID = str(os.getenv('DEST_ACCOUNT')).strip() +PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + REGION = os.getenv('REGION_OVERRIDE').strip() +else: + REGION = os.getenv('AWS_DEFAULT_REGION') + +SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + +logger = logging.getLogger() +logger.setLevel(LOGLEVEL.upper()) + + + +def lambda_handler(event, context): + pending_snapshots = 0 + client = boto3.client('rds', region_name=REGION) + response = paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots', SnapshotType='manual') + filtered = get_own_snapshots_source(PATTERN, response) + + # Search all snapshots for the correct tag + for snapshot_identifier,snapshot_object in filtered.items(): + snapshot_arn = snapshot_object['Arn'] + response_tags = client.list_tags_for_resource( + ResourceName=snapshot_arn) + + if snapshot_object['Status'].lower() == 'available' and search_tag_shared(response_tags): + try: + # Share snapshot with dest_account + response_modify = client.modify_db_snapshot_attribute( + DBSnapshotIdentifier=snapshot_identifier, + AttributeName='restore', + ValuesToAdd=[ + DEST_ACCOUNTID + ] + ) + except Exception as e: + logger.error('Exception sharing %s (%s)' % (snapshot_identifier, e)) + pending_snapshots += 1 + + if pending_snapshots > 0: + log_message = 'Could not share all snapshots. Pending: %s' % pending_snapshots + logger.error(log_message) + raise SnapshotToolException(log_message) + + +if __name__ == '__main__': + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/share_snapshots_rds/snapshots_tool_utils.py b/lambda_code/share_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/share_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/lambda_code/take_snapshots_rds/.DS_Store b/lambda_code/take_snapshots_rds/.DS_Store new file mode 100644 index 0000000..5008ddf Binary files /dev/null and b/lambda_code/take_snapshots_rds/.DS_Store differ diff --git a/lambda/take_snapshots_rds/lambda_function.py b/lambda_code/take_snapshots_rds/lambda_function.py similarity index 96% rename from lambda/take_snapshots_rds/lambda_function.py rename to lambda_code/take_snapshots_rds/lambda_function.py index 10f6a46..af9d023 100644 --- a/lambda/take_snapshots_rds/lambda_function.py +++ b/lambda_code/take_snapshots_rds/lambda_function.py @@ -1,98 +1,98 @@ -''' -Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. - -Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at - - http://aws.amazon.com/apache2.0/ - -or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. -''' - - -# take_snapshots_rds -# This lambda function takes a snapshot of RDS instances according to the environment variable PATTERN and INTERVAL -# Set PATTERN to a regex that matches your RDS Instance identifiers -# Set INTERVAL to the amount of hours between backups. This function will list available manual snapshots and only trigger a new one if the latest is older than INTERVAL hours -# Set FILTERINSTANCE to True to only take snapshots for RDS Instances with tag CopyDBSnapshot set to True -import boto3 -from datetime import datetime -import time -import os -import logging -import re -from snapshots_tool_utils import * - -# Initialize everything -LOGLEVEL = os.getenv('LOG_LEVEL').strip() -BACKUP_INTERVAL = int(os.getenv('INTERVAL', '24')) -PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') -TAGGEDINSTANCE = os.getenv('TAGGEDINSTANCE', 'FALSE') - -if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': - REGION = os.getenv('REGION_OVERRIDE').strip() -else: - REGION = os.getenv('AWS_DEFAULT_REGION') - -TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' - -logger = logging.getLogger() -logger.setLevel(LOGLEVEL.upper()) - - -def lambda_handler(event, context): - - client = boto3.client('rds', region_name=REGION) - response = paginate_api_call(client, 'describe_db_instances', 'DBInstances') - now = datetime.now() - pending_backups = 0 - filtered_instances = filter_instances(TAGGEDINSTANCE, PATTERN, response) - filtered_snapshots = get_own_snapshots_source(PATTERN, paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots'), BACKUP_INTERVAL) - - for db_instance in filtered_instances: - - timestamp_format = now.strftime(TIMESTAMP_FORMAT) - - if requires_backup(BACKUP_INTERVAL, db_instance, filtered_snapshots): - - backup_age = get_latest_snapshot_ts( - db_instance['DBInstanceIdentifier'], - filtered_snapshots) - - if backup_age is not None: - logger.info('Backing up %s. Backed up %s minutes ago' % ( - db_instance['DBInstanceIdentifier'], ((now - backup_age).total_seconds() / 60))) - - else: - logger.info('Backing up %s. No previous backup found' % - db_instance['DBInstanceIdentifier']) - - snapshot_identifier = '%s-%s' % ( - db_instance['DBInstanceIdentifier'], timestamp_format) - - try: - response = client.create_db_snapshot( - DBSnapshotIdentifier=snapshot_identifier, - DBInstanceIdentifier=db_instance['DBInstanceIdentifier'], - Tags=[{'Key': 'CreatedBy', 'Value': 'Snapshot Tool for RDS'}, { - 'Key': 'CreatedOn', 'Value': timestamp_format}, {'Key': 'shareAndCopy', 'Value': 'YES'}] - ) - except Exception as e: - pending_backups += 1 - logger.info('Could not create snapshot %s (%s)' % (snapshot_identifier, e)) - else: - - backup_age = get_latest_snapshot_ts( - db_instance['DBInstanceIdentifier'], - filtered_snapshots) - - logger.info('Skipped %s. Does not require backup. Backed up %s minutes ago' % ( - db_instance['DBInstanceIdentifier'], (now - backup_age).total_seconds() / 60)) - - if pending_backups > 0: - log_message = 'Could not back up every instance. Backups pending: %s' % pending_backups - logger.error(log_message) - raise SnapshotToolException(log_message) - - -if __name__ == '__main__': - lambda_handler(None, None) +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# take_snapshots_rds +# This lambda function takes a snapshot of RDS instances according to the environment variable PATTERN and INTERVAL +# Set PATTERN to a regex that matches your RDS Instance identifiers +# Set INTERVAL to the amount of hours between backups. This function will list available manual snapshots and only trigger a new one if the latest is older than INTERVAL hours +# Set FILTERINSTANCE to True to only take snapshots for RDS Instances with tag CopyDBSnapshot set to True +import boto3 +from datetime import datetime +import time +import os +import logging +import re +from snapshots_tool_utils import * + +# Initialize everything +LOGLEVEL = os.getenv('LOG_LEVEL').strip() +BACKUP_INTERVAL = int(os.getenv('INTERVAL', '24')) +PATTERN = os.getenv('PATTERN', 'ALL_INSTANCES') +TAGGEDINSTANCE = os.getenv('TAGGEDINSTANCE', 'FALSE') + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + REGION = os.getenv('REGION_OVERRIDE').strip() +else: + REGION = os.getenv('AWS_DEFAULT_REGION') + +TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +logger = logging.getLogger() +logger.setLevel(LOGLEVEL.upper()) + + +def lambda_handler(event, context): + + client = boto3.client('rds', region_name=REGION) + response = paginate_api_call(client, 'describe_db_instances', 'DBInstances') + now = datetime.now() + pending_backups = 0 + filtered_instances = filter_instances(TAGGEDINSTANCE, PATTERN, response) + filtered_snapshots = get_own_snapshots_source(PATTERN, paginate_api_call(client, 'describe_db_snapshots', 'DBSnapshots'), BACKUP_INTERVAL) + + for db_instance in filtered_instances: + + timestamp_format = now.strftime(TIMESTAMP_FORMAT) + + if requires_backup(BACKUP_INTERVAL, db_instance, filtered_snapshots): + + backup_age = get_latest_snapshot_ts( + db_instance['DBInstanceIdentifier'], + filtered_snapshots) + + if backup_age is not None: + logger.info('Backing up %s. Backed up %s minutes ago' % ( + db_instance['DBInstanceIdentifier'], ((now - backup_age).total_seconds() / 60))) + + else: + logger.info('Backing up %s. No previous backup found' % + db_instance['DBInstanceIdentifier']) + + snapshot_identifier = '%s-%s' % ( + db_instance['DBInstanceIdentifier'], timestamp_format) + + try: + response = client.create_db_snapshot( + DBSnapshotIdentifier=snapshot_identifier, + DBInstanceIdentifier=db_instance['DBInstanceIdentifier'], + Tags=[{'Key': 'CreatedBy', 'Value': 'Snapshot Tool for RDS'}, { + 'Key': 'CreatedOn', 'Value': timestamp_format}, {'Key': 'shareAndCopy', 'Value': 'YES'}] + ) + except Exception as e: + pending_backups += 1 + logger.info('Could not create snapshot %s (%s)' % (snapshot_identifier, e)) + else: + + backup_age = get_latest_snapshot_ts( + db_instance['DBInstanceIdentifier'], + filtered_snapshots) + + logger.info('Skipped %s. Does not require backup. Backed up %s minutes ago' % ( + db_instance['DBInstanceIdentifier'], (now - backup_age).total_seconds() / 60)) + + if pending_backups > 0: + log_message = 'Could not back up every instance. Backups pending: %s' % pending_backups + logger.error(log_message) + raise SnapshotToolException(log_message) + + +if __name__ == '__main__': + lambda_handler(None, None) \ No newline at end of file diff --git a/lambda_code/take_snapshots_rds/snapshots_tool_utils.py b/lambda_code/take_snapshots_rds/snapshots_tool_utils.py new file mode 100644 index 0000000..eeaeee8 --- /dev/null +++ b/lambda_code/take_snapshots_rds/snapshots_tool_utils.py @@ -0,0 +1,364 @@ +''' +Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +''' + + +# snapshots_tool_utils +# Support module for the Snapshot Tool for RDS + +import boto3 +from datetime import datetime, timedelta +import os +import logging +import re + + +# Initialize everything +_LOGLEVEL = os.getenv('LOG_LEVEL', 'ERROR').strip() + +_DESTINATION_REGION = os.getenv( + 'DEST_REGION', os.getenv('AWS_DEFAULT_REGION')).strip() + +_KMS_KEY_DEST_REGION = os.getenv('KMS_KEY_DEST_REGION', 'None').strip() + +_KMS_KEY_SOURCE_REGION = os.getenv('KMS_KEY_SOURCE_REGION', 'None').strip() + +_TIMESTAMP_FORMAT = '%Y-%m-%d-%H-%M' + +if os.getenv('REGION_OVERRIDE', 'NO') != 'NO': + _REGION = os.getenv('REGION_OVERRIDE').strip() +else: + _REGION = os.getenv('AWS_DEFAULT_REGION') + +_SUPPORTED_ENGINES = [ 'mariadb', 'sqlserver-se', 'sqlserver-ee', 'sqlserver-ex', 'sqlserver-web', 'mysql', 'oracle-se', 'oracle-se1', 'oracle-se2', 'oracle-ee', 'postgres' ] + + +logger = logging.getLogger() +logger.setLevel(_LOGLEVEL.upper()) + + +class SnapshotToolException(Exception): + pass + + +def search_tag_copydbsnapshot(response): +# Takes a list_tags_for_resource response and searches for our CopyDBSnapshot tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CopyDBSnapshot' and tag['Value'] == 'True': return True + + except Exception: return False + + else: return False + + + +def search_tag_created(response): +# Takes a describe_db_snapshots response and searches for our CreatedBy tag + try: + + for tag in response['TagList']: + if tag['Key'] == 'CreatedBy' and tag['Value'] == 'Snapshot Tool for RDS': return True + + except Exception: return False + + else: return False + + + +def search_tag_shared(response): +# Takes a describe_db_snapshots response and searches for our shareAndCopy tag + try: + for tag in response['TagList']: + if tag['Key'] == 'shareAndCopy' and tag['Value'] == 'YES': + for tag2 in response['TagList']: + if tag2['Key'] == 'CreatedBy' and tag2['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + + + +def search_tag_copied(response): +# Search for a tag indicating we copied this snapshot + try: + for tag in response['TagList']: + if tag['Key'] == 'CopiedBy' and tag['Value'] == 'Snapshot Tool for RDS': + return True + + except Exception: + return False + + return False + +def get_own_snapshots_no_x_account(pattern, response, REGION): + # Filters our own snapshots + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + #Changed the next line to search for ALL_CLUSTERS or ALL_SNAPSHOTS so it will work with no-x-account + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_shared_snapshots(pattern, response): +# Returns a dict with only shared snapshots filtered by pattern, with DBSnapshotIdentifier as key and the response as attribute + filtered = {} + for snapshot in response['DBSnapshots']: + if snapshot['SnapshotType'] == 'shared' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'shared' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[get_snapshot_identifier(snapshot)] = { + 'Arn': snapshot['DBSnapshotIdentifier'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + if snapshot['Encrypted'] is True: + filtered[get_snapshot_identifier(snapshot)]['KmsKeyId'] = snapshot['KmsKeyId'] + return filtered + + + +def get_snapshot_identifier(snapshot): +# Function that will return the RDS Snapshot identifier given an ARN + match = re.match('arn:aws:rds:.*:.*:snapshot:(.+)', + snapshot['DBSnapshotArn']) + return match.group(1) + + +def get_own_snapshots_dest(pattern, response): +# Returns a dict with local snapshots, filtered by pattern, with DBSnapshotIdentifier as key and Arn, Status as attributes + filtered = {} + for snapshot in response['DBSnapshots']: + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + elif snapshot['SnapshotType'] == 'manual' and pattern == 'ALL_SNAPSHOTS' and snapshot['Engine'] in _SUPPORTED_ENGINES: + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'Encrypted': snapshot['Encrypted'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier'] } + + if snapshot['Encrypted'] is True: + filtered[snapshot['DBSnapshotIdentifier']]['KmsKeyId'] = snapshot['KmsKeyId'] + + return filtered + +def filter_instances(taggedinstance, pattern, instance_list): +# Takes the response from describe-db-instances and filters according to pattern in DBInstanceIdentifier + filtered_list = [] + + for instance in instance_list['DBInstances']: + + if taggedinstance == 'TRUE': + client = boto3.client('rds', region_name=_REGION) + response = client.list_tags_for_resource(ResourceName=instance['DBInstanceArn']) + + if pattern == 'ALL_INSTANCES' and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + else: + match = re.search(pattern, instance['DBInstanceIdentifier']) + + if match and instance['Engine'] in _SUPPORTED_ENGINES: + if (taggedinstance == 'TRUE' and search_tag_copydbsnapshot(response)) or taggedinstance == 'FALSE': + filtered_list.append(instance) + + return filtered_list + + +def get_own_snapshots_source(pattern, response, backup_interval=None): +# Filters our own snapshots + filtered = {} + + for snapshot in response['DBSnapshots']: + + # No need to consider snapshots that are still in progress + if 'SnapshotCreateTime' not in snapshot: + continue + + # No need to get tags for snapshots outside of the backup interval + if backup_interval and snapshot['SnapshotCreateTime'].replace(tzinfo=None) < datetime.utcnow().replace(tzinfo=None) - timedelta(hours=backup_interval): + continue + + if snapshot['SnapshotType'] == 'manual' and re.search(pattern, snapshot['DBInstanceIdentifier']) and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + elif snapshot['SnapshotType'] == 'manual' and (pattern == 'ALL_CLUSTERS' or pattern == 'ALL_SNAPSHOTS' or pattern == 'ALL_INSTANCES') and snapshot['Engine'] in _SUPPORTED_ENGINES: + client = boto3.client('rds', region_name=_REGION) + response_tags = client.list_tags_for_resource( + ResourceName=snapshot['DBSnapshotArn']) + + if search_tag_created(response_tags): + filtered[snapshot['DBSnapshotIdentifier']] = { + 'Arn': snapshot['DBSnapshotArn'], 'Status': snapshot['Status'], 'DBInstanceIdentifier': snapshot['DBInstanceIdentifier']} + + return filtered + + +def get_timestamp_no_minute(snapshot_identifier, snapshot_list): +# Get a timestamp from the name of a snapshot and strip out the minutes + pattern = '%s-(.+)-\d{2}' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + timestamp_format = '%Y-%m-%d-%H' + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + return datetime.strptime(date_time.group(1), timestamp_format) + + +def get_timestamp(snapshot_identifier, snapshot_list): +# Searches for a timestamp on a snapshot name + pattern = '%s-(.+)' % snapshot_list[snapshot_identifier]['DBInstanceIdentifier'] + date_time = re.search(pattern, snapshot_identifier) + + if date_time is not None: + + try: + return datetime.strptime(date_time.group(1), _TIMESTAMP_FORMAT) + + except Exception: + return None + + return None + + + +def get_latest_snapshot_ts(instance_identifier, filtered_snapshots): +# Get latest snapshot for a specific DBInstanceIdentifier + timestamps = [] + + for snapshot,snapshot_object in filtered_snapshots.items(): + + if snapshot_object['DBInstanceIdentifier'] == instance_identifier: + timestamp = get_timestamp_no_minute(snapshot, filtered_snapshots) + + if timestamp is not None: + timestamps.append(timestamp) + + if len(timestamps) > 0: + return max(timestamps) + + else: + return None + + + +def requires_backup(backup_interval, instance, filtered_snapshots): +# Returns True if latest snapshot is older than INTERVAL + latest = get_latest_snapshot_ts(instance['DBInstanceIdentifier'], filtered_snapshots) + + if latest is not None: + backup_age = datetime.now() - latest + + if backup_age.total_seconds() >= (backup_interval * 60 * 60): + return True + + else: + return False + + elif latest is None: + return True + + +def paginate_api_call(client, api_call, objecttype, *args, **kwargs): +#Takes an RDS boto client and paginates through api_call calls and returns a list of objects of objecttype + response = {} + response[objecttype] = [] + + # Create a paginator + paginator = client.get_paginator(api_call) + + # Create a PageIterator from the Paginator + page_iterator = paginator.paginate(**kwargs) + for page in page_iterator: + for item in page[objecttype]: + response[objecttype].append(item) + + return response + + +def copy_local(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_REGION) + + tags = [{ + 'Key': 'CopiedBy', + 'Value': 'Snapshot Tool for RDS' + }] + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s locally' % snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_SOURCE_REGION, + Tags = tags) + + else: + logger.info('Copying snapshot %s locally' %snapshot_identifier) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + Tags = tags) + + return response + + + +def copy_remote(snapshot_identifier, snapshot_object): + client = boto3.client('rds', region_name=_DESTINATION_REGION) + + if snapshot_object['Encrypted']: + logger.info('Copying encrypted snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + KmsKeyId = _KMS_KEY_DEST_REGION, + SourceRegion = _REGION, + CopyTags = True) + + else: + logger.info('Copying snapshot %s to remote region %s' % (snapshot_object['Arn'], _DESTINATION_REGION)) + response = client.copy_db_snapshot( + SourceDBSnapshotIdentifier = snapshot_object['Arn'], + TargetDBSnapshotIdentifier = snapshot_identifier, + SourceRegion = _REGION, + CopyTags = True) + + return response \ No newline at end of file diff --git a/rds_snapshot_tool_destination/.DS_Store b/rds_snapshot_tool_destination/.DS_Store new file mode 100644 index 0000000..3e89736 Binary files /dev/null and b/rds_snapshot_tool_destination/.DS_Store differ diff --git a/rds_snapshot_tool_destination/README.md b/rds_snapshot_tool_destination/README.md new file mode 100644 index 0000000..dc6e2bd --- /dev/null +++ b/rds_snapshot_tool_destination/README.md @@ -0,0 +1,84 @@ +# Snapshot Tool for Amazon RDS + +The Snapshot Tool for RDS automates the task of creating manual snapshots, copying them into a different account and a different region, and deleting them after a specified number of days. It also allows you to specify the backup schedule (at what times and how often) and a retention period in days. This version will work with all Amazon RDS instances except Amazon Aurora. For a version that works with Amazon Aurora, please visit the [Snapshot Tool for Amazon Aurora](https://github.com/awslabs/aurora-snapshot-tool). + +**IMPORTANT** Run the tf in the same **region** where your RDS instances run (both in the source and destination accounts). If that is not possible because AWS Step Functions is not available, you will need to use the **SourceRegionOverride** parameter explained below. + + +## Getting Started + + +### Building From Source and Deploying + +1. Create an S3 bucket to hold the Lambda function zip files. The bucket must be in the same region where the Lambda functions will run and the Lambda functions must run in the same region as the RDS instances. Add the lambda Zips to this bucket. +2. Clone the repository +3. Run ```tf apply --auto-approve``` but be sure to use the correct bucket name in the `CodeBucket` parameter when launching applying the tf. + + +### Source Account +#### Components +The following components will be created in the source account: +* 3 Lambda functions (TakeSnapshotsRDS, ShareSnapshotsRDS, DeleteOldSnapshotsRDS) +* 3 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineTakeSnapshotRDS, stateMachineShareSnapshotRDS, stateMachineDeleteOldSnapshotsRDS) +* 3 Cloudwatch Event Rules to trigger the state functions +* 3 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures +* A Cloudformation stack containing all these resources + +#### Installing in the source account +Run snapshot_tool_RDS_source.json on the Cloudformation console. +You wil need to specify the different parameters. The default values will back up all RDS instances in the region at 1AM UTC, once a day. +If your instances are encrypted, you will need to provide access to the KMS Key to the destination account. You can read more on how to do that here: https://aws.amazon.com/premiumsupport/knowledge-center/share-cmk-account/ + +Here is a break down of each parameter for the source template: + +* **BackupInterval** - how many hours between backup +* **BackupSchedule** - at what times and how often to run backups. Set in accordance with **BackupInterval**. For example, set **BackupInterval** to 8 hours and **BackupSchedule** 0 0,8,16 * * ? * if you want backups to run at 0, 8 and 16 UTC. If your backups run more often than **BackupInterval**, snapshots will only be created when the latest snapshot is older than **BackupInterval**. If you set BackupInterval to more than once a day, make sure to adjust BackupSchedule accordingly or backups will only be taken at the times specified in the CRON expression. +* **InstanceNamePattern** - set to the names of the instances you want this tool to back up. You can use a Python regex that will be searched in the instance identifier. For example, if your instances are named *prod-01*, *prod-02*, etc, you can set **InstanceNamePattern** to *prod*. The string you specify will be searched anywhere in the name unless you use an anchor such as ^ or $. In most cases, a simple name like "prod" or "dev" will suffice. More information on Python regular expressions here: https://docs.python.org/2/howto/regex.html +* **DestinationAccount** - the account where you want snapshots to be copied to +* **LogLevel** - The log level you want as output to the Lambda functions. ERROR is usually enough. You can increase to INFO or DEBUG. +* **RetentionDays** - the amount of days you want your snapshots to be kept. Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CreatedBy, Value: Snapshot Tool for RDS) +* **ShareSnapshots** - Set to TRUE if you are sharing snapshots with a different account. If you set to FALSE, StateMachine, Lambda functions and associated Cloudwatch Alarms related to sharing across accounts will not be created. It is useful if you only want to take backups and manage the retention, but do not need to copy them across accounts or regions. +* **SourceRegionOverride** - if you are running RDS on a region where Step Functions is not available, this parameter will allow you to override the source region. For example, at the time of this writing, you may be running RDS in Northern California (us-west-1) and would like to copy your snapshots to Montreal (ca-central-1). Neither region supports Step Functions at the time of this writing so deploying this tool there will not work. The solution is to run this template in a region that supports Step Functions (such as North Virginia or Ohio) and set **SourceRegionOverride** to *us-west-1*. +**IMPORTANT**: deploy to the closest regions for best results. + +* **CodeBucket** - this parameter specifies the bucket where the code for the Lambda functions is located. The Lambda function code is located in the ```lambda``` directory in zip format. These files need to be on the **root* of the bucket or the CloudFormation templates will fail. Please follow the instructions to build source (earlier on this README file) +* **DeleteOldSnapshots** - Set to TRUE to enable functionality that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete **all snapshots**, older than **RetentionDays**, created by this tool; not just the ones created after **DeleteOldSnapshots** is set to TRUE. +* **TaggedInstance** - Set to TRUE to enable functionality that will only take snapshots for RDS Instances with tag CopyDBSnapshot set to True. The settings in InstanceNamePattern and TaggedInstance both need to evaluate successfully for a snapshot to be created (logical AND). + +### Destination Account +#### Components +The following components will be created in the destination account: +* 2 Lambda functions (CopySnapshotsDestRDS, DeleteOldSnapshotsDestRDS) +* 2 State Machines (Amazon Step Functions) to trigger execution of each Lambda function (stateMachineCopySnapshotsDestRDS, stateMachineDeleteOldSnapshotsDestRDS) +* 2 Cloudwatch Event Rules to trigger the state functions +* 2 Cloudwatch Alarms and associated SNS Topics to alert on State Machines failures +* A Cloudformation stack containing all these resources + +On your destination account, you will need to run snapshot_tool_RDS_dest.json on the Cloudformation. As before, you will need to run it in a region where Step Functions is available. +The following parameters are available: + +* **DestinationRegion** - the region where you want your snapshots to be copied. If you set it to the same as the source region, the snapshots will be copied from the source account but will be kept in the source region. This is useful if you would like to keep a copy of your snapshots in a different account but would prefer not to copy them to a different region. +* **SnapshotPattern** - similar to InstanceNamePattern. See above +* **DeleteOldSnapshots** - Set to TRUE to enable functionanility that will delete snapshots after **RetentionDays**. Set to FALSE if you want to disable this functionality completely. (Associated Lambda and State Machine resources will not be created in the account). **WARNING** If you decide to enable this functionality later on, bear in mind it will delete ALL SNAPSHOTS older than RetentionDays created by this tool, not just the ones created after **DeleteOldSnapshots** is set to TRUE. +* **CrossAccountCopy** - if you only need to copy snapshots across regions and not to a different account, set this to FALSE. When set to false, the no-x-account version of the Lambda functions will be deployed and will expect snapshots to be in the same account as they run. +* **KmsKeySource** KMS Key to be used for copying encrypted snapshots on the source region. If you are copying to a different region, you will also need to provide a second key in the destination region. +* **KmsKeyDestination** KMS Key to be used for copying encrypted snapshots to the destination region. If you are not copying to a different region, this parameter is not necessary. +* **RetentionDays** - as in the source account, the amount of days you want your snapshots to be kept. **Do not set this parameter to a value lower than the source account.** Snapshots created more than **RetentionDays** ago will be automatically deleted (only if they contain a tag with Key: CopiedBy, Value: Snapshot Tool for RDS) + +## How it Works + +There are two sets of Lambda Step Functions that take regular snapshots and copy them across. Snapshots can take time, and they do not signal when they're complete. Snapshots are scheduled to *begin* at a certain time using CloudWatch Events. Then different Lambda Step Functions run periodically to look for new snapshots. When they find new snapshots, they do the sharing and the copying functions. + +### In the Source Account + +A CloudWatch Event is scheduled to trigger Lambda Step Function State Machine named `stateMachineTakeSnapshotsRDS`. That state machine invokes a function named `lambdaTakeSnapshotsRDS`. That function triggers a snapshot and applies some standard tags. It matches RDS instances using a regular expression on their names. + +There are two other state machines and lambda functions. The `statemachineShareSnapshotsRDS` looks for new snapshots created by the `lambdaTakeSnapshotsRDS` function. When it finds them, it shares them with the destination account. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventShareSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a new snapshot that is intended to be shared, it shares the snapshot. + +The other state machine is the `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. + +### In the Destination Account + +There are two state machines and corresponding lambda functions. The `statemachineCopySnapshotsDestRDS` looks for new snapshots that have been shared but have not yet been copied. When it finds them, it creates a copy in the destination account, encrypted with the KMS key that has been stipulated. This state machine is, by default, run every 10 minutes. (To change it, you need to change the `ScheduleExpression` property of the `cwEventCopySnapshotsRDS` resource in `snapshots_tool_rds_dest.json`). + +The other state machine is just like the corresponding state machine and function in the source account. The state machine is `statemachineDeleteOldSnapshotsRDS` and it calls `lambdaDeleteOldSnapshotsRDS` to delete snapshots according to the `RetentionDays` parameter when the stack is launched. This state machine is, by default, run once each hour. (To change it, you need to change the `ScheduleExpression` property of the `cwEventDeleteOldSnapshotsRDS` resource in `snapshots_tool_rds_source.json`). If it finds a snapshot that is older than the retention time, it deletes the snapshot. \ No newline at end of file diff --git a/rds_snapshot_tool_destination/main.tf b/rds_snapshot_tool_destination/main.tf new file mode 100644 index 0000000..93e0b9b --- /dev/null +++ b/rds_snapshot_tool_destination/main.tf @@ -0,0 +1,4 @@ + +module "snapshots_tool_rds_dest" { + source = "./modules/rds_destination" +} diff --git a/rds_snapshot_tool_destination/modules/.DS_Store b/rds_snapshot_tool_destination/modules/.DS_Store new file mode 100644 index 0000000..3aba569 Binary files /dev/null and b/rds_snapshot_tool_destination/modules/.DS_Store differ diff --git a/rds_snapshot_tool_destination/modules/rds_destination/cloudwatch.tf b/rds_snapshot_tool_destination/modules/rds_destination/cloudwatch.tf new file mode 100644 index 0000000..9b7574e --- /dev/null +++ b/rds_snapshot_tool_destination/modules/rds_destination/cloudwatch.tf @@ -0,0 +1,94 @@ +resource "aws_cloudwatch_metric_alarm" "alarmcw_copy_failed_dest" { + + alarm_name = "failed-rds-copy" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "1" + metric_name = "ExecutionsFailed" + namespace = "AWS/States" + period = "300" + statistic = "Sum" + threshold = "1.0" + + dimensions = { + StateMachineArn = aws_sfn_state_machine.statemachine_copy_old_snapshots_dest_rds.arn + } + + alarm_description = "This metric monitors state machine failure for copying snapshots" + alarm_actions = [ + aws_sns_topic.copy_failed_dest.id + ] +} + +resource "aws_cloudwatch_metric_alarm" "alarmcw_delete_old_failed_dest" { + alarm_name = "failed-rds-delete-old-snapshot" + comparison_operator = "GreaterThanOrEqualToThreshold" + evaluation_periods = "2" + metric_name = "ExecutionsFailed" + namespace = "AWS/States" + period = "3600" + statistic = "Sum" + threshold = "2.0" + + dimensions = { + StateMachineArn = aws_sfn_state_machine.statemachine_delete_old_snapshots_dest_rds.arn + } + + alarm_description = "This metric monitors state machine failure for deleting old snapshots" + alarm_actions = [ + aws_sns_topic.delete_old_failed_dest.id + ] +} + +resource "aws_cloudwatch_event_rule" "copy_snapshots_rds" { + name = "capture-snapshot-copy-cw-events" + description = "Capture all CW state change events and triggers RDS copy state machine in destination account" + schedule_expression = "cron(30 * * * ? *)" + is_enabled = true + + + event_pattern = <