├── .github └── PULL_REQUEST_TEMPLATE.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Instructions └── Deploy an Amazon Aurora PostgreSQL DB cluster with recommended best practices using AWS CloudFormation.pdf ├── LICENSE ├── NOTICE ├── README.md ├── cftemplates ├── Aurora-Postgres-DB-Cluster.yml ├── VPC-3AZs.yml └── VPC-SSH-Bastion.yml ├── lambda └── dbbootstrap.zip └── media └── AWS-Aurora-Architecture.png /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/aws-aurora-cloudformation-samples/issues), or [recently closed](https://github.com/aws-samples/aws-aurora-cloudformation-samples/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-samples/aws-aurora-cloudformation-samples/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-samples/aws-aurora-cloudformation-samples/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /Instructions/Deploy an Amazon Aurora PostgreSQL DB cluster with recommended best practices using AWS CloudFormation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-aurora-cloudformation-samples/ed40cc26e9809dc98a7d15eedc36bf41a3d24ff6/Instructions/Deploy an Amazon Aurora PostgreSQL DB cluster with recommended best practices using AWS CloudFormation.pdf -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | AWS Aurora CloudFormation Samples 2 | Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Aurora CloudFormation Samples 2 | 3 | This GitHub repository contains: 4 | 5 | 1. A set of AWS CloudFormation samples to deploy an Amazon Aurora DB cluster based on AWS security and high availability best practices. 6 | 2. Python based source code to bootstrap the database upon creation using AWS Lambda. 7 | 8 | When you are starting your journey with Amazon Aurora and want to set up AWS resources based on the recommended best practices of [AWS Well-Architected Framework](https://docs.aws.amazon.com/wellarchitected/latest/userguide/intro.html#waf), you can use the CloudFormation templates provided here. 9 | 10 | # Deploy an Amazon Aurora PostgreSQL DB Cluster 11 | 12 | ## Architecture overview 13 | 14 | Here is a diagram of our architecture and a brief summary of what you are going to set up. 15 | 16 | ![](media/AWS-Aurora-Architecture.png) 17 | 18 | The sample CloudFormation templates provision the network infrastructure and all the components shown in the architecture diagram. I broke the CloudFormation templates into the following three stacks. 19 | 20 | 1. CloudFormation template to set up VPC, subnets, route tables, internet gateway, NAT gateway, S3 gateway endpoint, [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) interface endpoint, and other networking components. 21 | 2. CloudFormation template to set up an Amazon Linux bastion host in an Auto Scaling group to connect to the Aurora PostgreSQL DB cluster. 22 | 3. CloudFormation template to set up Aurora PostgreSQL DB cluster with master user password stored in AWS Secrets Manager and bootstrap the database using [AWS Lambda](http://aws.amazon.com/lambda). 23 | 24 | The stacks are integrated using exported output values. Using three different CloudFormation stacks instead of one nested stack gives you some flexibility. For example, you can choose to deploy the VPC and bastion host CloudFormation stacks once and Aurora PostgreSQL DB cluster CloudFormation stack multiple times in an AWS Region. 25 | 26 | ## Best practices, Prerequisites and Set up Instructions 27 | 28 | For Best practices incorporated in the sample AWS CloudFormation samples, prerequisites and set up instructions refer the following document. You can download this document and then launch AWS CloudFormation directly from it, by selecting the ![Launch Stack](https://s3.amazonaws.com/cloudformation-examples/cloudformation-launch-stack.png) buttons embedded in the set up section. 29 | 30 | [Deploy an Amazon Aurora PostgreSQL DB cluster with recommended best practices using AWS CloudFormation](Instructions/Deploy%20an%20Amazon%20Aurora%20PostgreSQL%20DB%20cluster%20with%20recommended%20best%20practices%20using%20AWS%20CloudFormation.pdf) 31 | 32 | 33 | # License 34 | 35 | This library is licensed under the Apache 2.0 License. 36 | -------------------------------------------------------------------------------- /cftemplates/Aurora-Postgres-DB-Cluster.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'CloudFormation Template to create Aurora Postgresql Cluster DB Instance' 3 | 4 | ############################################################################### 5 | # Parameters 6 | ############################################################################### 7 | 8 | Parameters: 9 | 10 | ParentVPCStack: 11 | Description: 'Provide Stack name of parent VPC stack based on VPC-3AZs yaml template. Refer Cloudformation dashboard in AWS Console to get this.' 12 | Type: String 13 | MinLength: '1' 14 | MaxLength: '128' 15 | AllowedPattern: '^[a-zA-Z]+[0-9a-zA-Z\-]*$' 16 | 17 | ParentSSHBastionStack: 18 | Description: 'Provide Stack name of parent Amazon Linux bastion host stack based on VPC-SSH-Bastion yaml template. Refer Cloudformation dashboard in AWS Console to get this.' 19 | Type: String 20 | MinLength: '1' 21 | MaxLength: '128' 22 | AllowedPattern: '^[a-zA-Z]+[0-9a-zA-Z\-]*$' 23 | 24 | DBName: 25 | Description: Database Name 26 | Type: String 27 | MinLength: '1' 28 | MaxLength: '64' 29 | AllowedPattern: "^[a-zA-Z]+[0-9a-zA-Z_]*$" 30 | ConstraintDescription: Must start with a letter. Only numbers, letters, and _ accepted. max length 64 characters 31 | 32 | DBPort: 33 | Description: TCP/IP Port for the Database Instance 34 | Type: Number 35 | Default: 5432 36 | ConstraintDescription: 'Must be in the range [1150-65535]' 37 | MinValue: 1150 38 | MaxValue: 65535 39 | 40 | DBUsername: 41 | Description: Database master username 42 | Type: String 43 | MinLength: '1' 44 | MaxLength: '16' 45 | AllowedPattern: "^[a-zA-Z]+[0-9a-zA-Z_]*$" 46 | ConstraintDescription: Must start with a letter. Only numbers, letters, and _ accepted. max length 16 characters 47 | 48 | DBEngineVersion: 49 | Description: Select Database Engine Version 50 | Type: String 51 | Default: 13.7 52 | AllowedValues: 53 | - 11.16 54 | - 12.11 55 | - 13.7 56 | - 14.3 57 | 58 | DBInstanceClass: 59 | Default: db.r6g.large 60 | Description: Database Instance Class 61 | Type: String 62 | AllowedValues: 63 | - db.t4g.medium 64 | - db.t4g.large 65 | - db.r5.large 66 | - db.r5.xlarge 67 | - db.r5.2xlarge 68 | - db.r5.4xlarge 69 | - db.r5.8xlarge 70 | - db.r5.12xlarge 71 | - db.r5.16xlarge 72 | - db.r5.24xlarge 73 | - db.r6g.large 74 | - db.r6g.xlarge 75 | - db.r6g.2xlarge 76 | - db.r6g.4xlarge 77 | - db.r6g.8xlarge 78 | - db.r6g.12xlarge 79 | - db.r6g.16xlarge 80 | - db.x2g.large 81 | - db.x2g.xlarge 82 | - db.x2g.2xlarge 83 | - db.x2g.4xlarge 84 | - db.x2g.8xlarge 85 | - db.x2g.12xlarge 86 | - db.x2g.16xlarge 87 | 88 | DBSnapshotName: 89 | Description: Optional. DB Snapshot ID to restore database. Leave this blank if you are not restoring from a snapshot. 90 | Type: String 91 | Default: "" 92 | 93 | LambdaBootStrapS3Bucket: 94 | Description: Optional. Specify S3 bucket name for e.g. apgbootstrapscripts where Lambda DB Bootstrap Python script is stored. 95 | Default: '' 96 | Type: String 97 | 98 | LambdaBootStrapS3Key: 99 | Description: Optional. Specify S3 key for e.g. lambda/dbbootstrap.zip where Lambda DB Bootstrap Python script is stored. 100 | Default: '' 101 | Type: String 102 | 103 | ########################################################################### 104 | # Mandatory tags that will be added to all resources that support tags 105 | ########################################################################### 106 | 107 | 108 | EnvironmentStage: 109 | Type: String 110 | Description: The environment tag is used to designate the Environment Stage of the associated AWS resource. 111 | AllowedValues: 112 | - dev 113 | - test 114 | - pre-prod 115 | - prod 116 | Default: dev 117 | 118 | Application: 119 | Type: String 120 | Description: The Application tag is used to designate the application of the associated AWS resource. In this capacity application does not refer to an installed software component, but rather the overall business application that the resource supports. 121 | AllowedPattern: "^[a-zA-Z]+[a-zA-Z ]+[a-zA-Z]+$" 122 | ConstraintDescription: provide a valid application name containing only letters and spaces 123 | 124 | ApplicationVersion: 125 | Type: String 126 | Description: The ApplicationVersion tag is used to designate the specific version of the application. Format should be in the Pattern - "#.#.#" 127 | Default: '1.0.0' 128 | AllowedPattern: '^[a-zA-Z0-9\.\-]+$' 129 | ConstraintDescription: provide a valid application version 130 | 131 | ProjectCostCenter: 132 | Type: String 133 | Description: The ProjectCostCenter tag is used to designate the cost center associated with the project of the given AWS resource. 134 | AllowedPattern: "^[a-zA-Z0-9]+$" 135 | ConstraintDescription: provide a valid cost center 136 | 137 | ServiceOwnersEmailContact: 138 | Type: String 139 | Description: The ServiceOwnersEmailContact tag is used to designate business owner(s) email address associated with the given AWS resource for sending outage or maintenance notifications 140 | AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' 141 | ConstraintDescription: provide a valid email address. 142 | 143 | NotificationList: 144 | Type: String 145 | Description: The Email notification list is used to configure a SNS topic for sending cloudwatch alarm and RDS Event notifications 146 | AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' 147 | ConstraintDescription: provide a valid email address. 148 | 149 | Confidentiality: 150 | Type: String 151 | Description: The Confidentiality tag is used to designate the confidentiality classification of the data that is associated with the resource. 152 | AllowedValues: 153 | - public 154 | - private 155 | - confidential 156 | - pii/phi 157 | 158 | Compliance: 159 | Type: String 160 | Description: The Compliance tag is used to specify the Compliance level for the AWS resource. 161 | AllowedValues: 162 | - hipaa 163 | - sox 164 | - fips 165 | - other 166 | - none 167 | 168 | ############################################################################### 169 | # Parameter groups 170 | ############################################################################### 171 | 172 | Metadata: 173 | AWS::CloudFormation::Interface: 174 | ParameterGroups: 175 | - 176 | Label: 177 | default: Environment 178 | Parameters: 179 | - EnvironmentStage 180 | - 181 | Label: 182 | default: DB Parameters 183 | Parameters: 184 | - DBName 185 | - DBPort 186 | - DBUsername 187 | - DBInstanceClass 188 | - DBEngineVersion 189 | - DBSnapshotName 190 | - NotificationList 191 | - LambdaBootStrapS3Bucket 192 | - LambdaBootStrapS3Key 193 | - 194 | Label: 195 | default: Networking 196 | Parameters: 197 | - ParentVPCStack 198 | - ParentSSHBastionStack 199 | - 200 | Label: 201 | default: Mandatory Tags 202 | Parameters: 203 | - Application 204 | - ApplicationVersion 205 | - ProjectCostCenter 206 | - ServiceOwnersEmailContact 207 | - Confidentiality 208 | - Compliance 209 | 210 | ############################################################################### 211 | # Mappings 212 | ############################################################################### 213 | 214 | Mappings: 215 | DBFamilyMap: 216 | "11.16": 217 | "family": "aurora-postgresql11" 218 | "12.11": 219 | "family": "aurora-postgresql12" 220 | "13.7": 221 | "family": "aurora-postgresql13" 222 | "14.3": 223 | "family": "aurora-postgresql14" 224 | 225 | 226 | ############################################################################### 227 | # Conditions 228 | ############################################################################### 229 | Conditions: 230 | IsUseDBSnapshot: !Not [!Equals [!Ref DBSnapshotName, ""]] 231 | IsNotUseDBSnapshot: !Not [Condition: IsUseDBSnapshot] 232 | IsProd: !Equals [!Ref EnvironmentStage, 'prod'] 233 | IsReplica: !Or [!Equals [!Ref EnvironmentStage, 'pre-prod'], Condition: IsProd] 234 | DoDBBootStrap: !And 235 | - !Not [!Equals [!Ref LambdaBootStrapS3Bucket, '']] 236 | - !Not [!Equals [!Ref LambdaBootStrapS3Key, '']] 237 | - !Not [Condition: IsUseDBSnapshot] 238 | DoEnableIAM: !Not [!Equals [!Ref DBEngineVersion, '9.6.8']] 239 | 240 | 241 | 242 | ############################################################################### 243 | # Resources 244 | ############################################################################### 245 | 246 | Resources: 247 | 248 | MonitoringIAMRole: 249 | Type: AWS::IAM::Role 250 | Condition: IsProd 251 | Properties: 252 | AssumeRolePolicyDocument: 253 | Version: "2012-10-17" 254 | Statement: 255 | - 256 | Effect: "Allow" 257 | Principal: 258 | Service: 259 | - "monitoring.rds.amazonaws.com" 260 | Action: 261 | - "sts:AssumeRole" 262 | Path: "/" 263 | ManagedPolicyArns: 264 | - arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole 265 | 266 | DBBootStrapLambdaRole: 267 | Type: AWS::IAM::Role 268 | Condition: DoDBBootStrap 269 | Properties: 270 | AssumeRolePolicyDocument: 271 | Version: "2012-10-17" 272 | Statement: 273 | - Effect: Allow 274 | Principal: 275 | Service: 276 | - lambda.amazonaws.com 277 | Action: 278 | - sts:AssumeRole 279 | Path: "/" 280 | ManagedPolicyArns: 281 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole' 282 | Policies: 283 | - 284 | PolicyName: "secretaccess" 285 | PolicyDocument: 286 | Version: "2012-10-17" 287 | Statement: 288 | - 289 | Effect: "Allow" 290 | Action: "secretsmanager:GetSecretValue" 291 | Resource: !Ref AuroraMasterSecret 292 | 293 | DBSNSTopic: 294 | Type: AWS::SNS::Topic 295 | Properties: 296 | Subscription: 297 | - Endpoint: !Ref NotificationList 298 | Protocol: email 299 | 300 | DBSubnetGroup: 301 | Type: 'AWS::RDS::DBSubnetGroup' 302 | Properties: 303 | DBSubnetGroupDescription: !Ref 'AWS::StackName' 304 | SubnetIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}] 305 | 306 | ClusterSecurityGroup: 307 | Type: 'AWS::EC2::SecurityGroup' 308 | Properties: 309 | GroupDescription: !Ref 'AWS::StackName' 310 | SecurityGroupIngress: 311 | - IpProtocol: tcp 312 | FromPort: !Ref DBPort 313 | ToPort: !Ref DBPort 314 | SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentSSHBastionStack}-BastionSecurityGroupID'} 315 | Description: 'Access to Bastion Host Security Group' 316 | - IpProtocol: tcp 317 | FromPort: !Ref DBPort 318 | ToPort: !Ref DBPort 319 | SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-SecretRotationLambdaSecurityGroup'} 320 | Description: 'Access to Lambda Security Group' 321 | VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'} 322 | Tags: 323 | - Key: Name 324 | Value: !Sub '${AWS::StackName}-AuroraClusterSecurityGroup' 325 | 326 | ClusterSecurityGroupIngress: 327 | Type: 'AWS::EC2::SecurityGroupIngress' 328 | Properties: 329 | GroupId: !GetAtt 'ClusterSecurityGroup.GroupId' 330 | IpProtocol: -1 331 | SourceSecurityGroupId: !Ref ClusterSecurityGroup 332 | Description: 'Self Reference' 333 | 334 | RDSDBClusterParameterGroup: 335 | Type: AWS::RDS::DBClusterParameterGroup 336 | Properties: 337 | Description: !Join [ "- ", [ "Aurora PG Cluster Parameter Group for Cloudformation Stack ", !Ref DBName ] ] 338 | Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] 339 | Parameters: 340 | rds.force_ssl: 1 341 | 342 | DBParamGroup: 343 | Type: AWS::RDS::DBParameterGroup 344 | Properties: 345 | Description: !Join [ "- ", [ "Aurora PG Database Instance Parameter Group for Cloudformation Stack ", !Ref DBName ] ] 346 | Family: !FindInMap [DBFamilyMap, !Ref DBEngineVersion, "family"] 347 | Parameters: 348 | shared_preload_libraries: auto_explain,pg_stat_statements,pg_hint_plan,pgaudit 349 | log_statement: "ddl" 350 | log_connections: 1 351 | log_disconnections: 1 352 | log_lock_waits: 1 353 | log_min_duration_statement: 5000 354 | auto_explain.log_min_duration: 5000 355 | auto_explain.log_verbose: 1 356 | log_rotation_age: 1440 357 | log_rotation_size: 102400 358 | rds.log_retention_period: 10080 359 | random_page_cost: 1 360 | track_activity_query_size: 16384 361 | idle_in_transaction_session_timeout: 7200000 362 | statement_timeout: 7200000 363 | search_path: '"$user",public' 364 | 365 | AuroraKMSCMK: 366 | Type: 'AWS::KMS::Key' 367 | DeletionPolicy: Retain 368 | Properties: 369 | KeyPolicy: 370 | Version: '2012-10-17' 371 | Statement: 372 | - Effect: Allow 373 | Principal: 374 | AWS: !Sub 'arn:aws:iam::${AWS::AccountId}:root' 375 | Action: 'kms:*' 376 | Resource: '*' 377 | - Effect: Allow 378 | Principal: 379 | AWS: '*' 380 | Action: 381 | - 'kms:Encrypt' 382 | - 'kms:Decrypt' 383 | - 'kms:ReEncrypt*' 384 | - 'kms:GenerateDataKey*' 385 | - 'kms:CreateGrant' 386 | - 'kms:ListGrants' 387 | - 'kms:DescribeKey' 388 | Resource: '*' 389 | Condition: 390 | StringEquals: 391 | 'kms:CallerAccount': !Ref 'AWS::AccountId' 392 | 'kms:ViaService': !Sub 'rds.${AWS::Region}.amazonaws.com' 393 | 394 | AuroraKMSCMKAlias: 395 | Type: 'AWS::KMS::Alias' 396 | DeletionPolicy: Retain 397 | DependsOn: AuroraDBCluster 398 | Properties: 399 | AliasName: !Sub 'alias/${AuroraDBCluster}' 400 | TargetKeyId: !Ref AuroraKMSCMK 401 | 402 | AuroraMasterSecret: 403 | Condition: IsNotUseDBSnapshot 404 | Type: AWS::SecretsManager::Secret 405 | Properties: 406 | Name: !Join ['/', [!Ref EnvironmentStage, 'aurora-pg', !Ref 'AWS::StackName']] 407 | Description: !Join ['', ['Aurora PostgreSQL Master User Secret ', 'for CloudFormation Stack ', !Ref 'AWS::StackName']] 408 | Tags: 409 | - 410 | Key: EnvironmentStage 411 | Value: !Ref EnvironmentStage 412 | - 413 | Key: DatabaseEngine 414 | Value: 'Aurora PostgreSQL' 415 | - 416 | Key: StackID 417 | Value: !Ref 'AWS::StackId' 418 | GenerateSecretString: 419 | SecretStringTemplate: !Join ['', ['{"username": "', !Ref DBUsername, '"}']] 420 | GenerateStringKey: "password" 421 | ExcludeCharacters: '"@/\' 422 | PasswordLength: 16 423 | 424 | SecretAuroraClusterAttachment: 425 | Condition: IsNotUseDBSnapshot 426 | Type: AWS::SecretsManager::SecretTargetAttachment 427 | Properties: 428 | SecretId: !Ref AuroraMasterSecret 429 | TargetId: !Ref AuroraDBCluster 430 | TargetType: AWS::RDS::DBCluster 431 | 432 | AuroraSecretResourcePolicy: 433 | Condition: IsNotUseDBSnapshot 434 | Type: AWS::SecretsManager::ResourcePolicy 435 | Properties: 436 | SecretId: !Ref AuroraMasterSecret 437 | ResourcePolicy: 438 | Version: "2012-10-17" 439 | Statement: 440 | - 441 | Effect: "Deny" 442 | Principal: 443 | AWS: !Sub "arn:aws:iam::${AWS::AccountId}:root" 444 | Action: "secretsmanager:DeleteSecret" 445 | Resource: "*" 446 | 447 | CreateSecretRotationLambdaRole: 448 | Condition: IsNotUseDBSnapshot 449 | Type: AWS::IAM::Role 450 | Properties: 451 | AssumeRolePolicyDocument: 452 | Version: "2012-10-17" 453 | Statement: 454 | - Effect: Allow 455 | Principal: 456 | Service: 457 | - lambda.amazonaws.com 458 | Action: 459 | - sts:AssumeRole 460 | Path: "/" 461 | ManagedPolicyArns: 462 | - 'arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole' 463 | - 'arn:aws:iam::aws:policy/SecretsManagerReadWrite' 464 | - 'arn:aws:iam::aws:policy/IAMFullAccess' 465 | Policies: 466 | - 467 | PolicyName: "AdditionalPermissions" 468 | PolicyDocument: 469 | Version: "2012-10-17" 470 | Statement: 471 | - 472 | Effect: "Allow" 473 | Action: 474 | - "cloudformation:DescribeStackResources" 475 | - "cloudformation:DeleteStack" 476 | Resource: !Sub 'arn:aws:cloudformation:${AWS::Region}:${AWS::AccountId}:stack/serverlessrepo-${AWS::StackName}-SecretRotationLambdaStack/*' 477 | - 478 | Effect: "Allow" 479 | Action: 480 | - "lambda:DeleteFunction" 481 | - "lambda:GetFunctionConfiguration" 482 | - "lambda:RemovePermission" 483 | - "lambda:TagResource" 484 | Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:SecretsManager-SecretRotationFn-${AWS::StackName}' 485 | - 486 | Effect: "Allow" 487 | Action: 488 | - "iam:DetachRolePolicy" 489 | Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/serverlessrepo*' 490 | 491 | CreateSecretRotationLambdaFn: 492 | Condition: IsNotUseDBSnapshot 493 | Type: AWS::Lambda::Function 494 | DependsOn: 495 | - CreateSecretRotationLambdaRole 496 | Properties: 497 | Code: 498 | ZipFile: | 499 | import cfnresponse 500 | import boto3 501 | from botocore.vendored import requests 502 | 503 | def lambda_handler(event, context): 504 | slrepoclient = boto3.client('serverlessrepo') 505 | cfclient = boto3.client('cloudformation') 506 | lambdaclient = boto3.client('lambda') 507 | SecretsManagerEndpoint = event['ResourceProperties']['SecretsManagerEndpoint'] 508 | SecretRotationLambdaFnName = event['ResourceProperties']['SecretRotationLambdaFnName'] 509 | SecretRotationLambdaStackName = event['ResourceProperties']['SecretRotationLambdaStackName'] 510 | SubnetIds = event['ResourceProperties']['SubnetIds'] 511 | SecretRotationLambdaSG = event['ResourceProperties']['SecretRotationLambdaSG'] 512 | 513 | responseData = {} 514 | 515 | try: 516 | if event['RequestType'] == 'Create': 517 | serverlessreporesponse = slrepoclient.create_cloud_formation_change_set(ApplicationId='arn:aws:serverlessrepo:us-east-1:297356227824:applications/SecretsManagerRDSPostgreSQLRotationSingleUser', 518 | Capabilities=['CAPABILITY_IAM', 'CAPABILITY_RESOURCE_POLICY'], 519 | ParameterOverrides=[ 520 | { 521 | 'Name': 'endpoint', 522 | 'Value': SecretsManagerEndpoint 523 | }, 524 | { 525 | 'Name': 'functionName', 526 | 'Value': SecretRotationLambdaFnName 527 | }, 528 | ], 529 | StackName=SecretRotationLambdaStackName) 530 | waiter = cfclient.get_waiter('change_set_create_complete') 531 | waiter.wait(ChangeSetName=serverlessreporesponse['ChangeSetId'],WaiterConfig={'Delay': 10,'MaxAttempts': 60}) 532 | 533 | cloudformationresponse = cfclient.execute_change_set(ChangeSetName=serverlessreporesponse['ChangeSetId']) 534 | waiter = cfclient.get_waiter('stack_create_complete') 535 | waiter.wait(StackName=serverlessreporesponse['StackId'],WaiterConfig={'Delay': 10,'MaxAttempts': 60}) 536 | 537 | lambdaresponse = lambdaclient.add_permission(FunctionName=SecretRotationLambdaFnName,StatementId='SecretsManagerAccess',Action='lambda:InvokeFunction',Principal='secretsmanager.amazonaws.com') 538 | lambdaresponse = lambdaclient.update_function_configuration(FunctionName=SecretRotationLambdaFnName,VpcConfig={'SubnetIds': SubnetIds,'SecurityGroupIds': SecretRotationLambdaSG}) 539 | 540 | responseData['Data'] = "SUCCESS: Secret Rotation Lambda created successfully." 541 | responseData['SecretRotationLambdaARN'] = lambdaclient.get_function(FunctionName=SecretRotationLambdaFnName)['Configuration']['FunctionArn'] 542 | 543 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "None") 544 | elif event['RequestType'] == 'Delete': 545 | SecretRotationLambdaStackName = 'serverlessrepo-' + SecretRotationLambdaStackName 546 | response = cfclient.delete_stack(StackName=SecretRotationLambdaStackName) 547 | waiter = cfclient.get_waiter('stack_delete_complete') 548 | waiter.wait(StackName='string',WaiterConfig={'Delay': 10,'MaxAttempts': 60}) 549 | 550 | responseData['Data'] = "SUCCESS: Stack delete complete." 551 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "None") 552 | else: 553 | responseData['Data'] = "{} is unsupported stack operation for this lambda function.".format(event['RequestType']) 554 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "None") 555 | except Exception as e: 556 | print(e) 557 | responseData['Data'] = "ERROR: Exception encountered!" 558 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "None") 559 | Description: >- 560 | Create Secret Rotation Lambda function using AWS Serverless Application Repository with template provided by AWS Secrets Manager 561 | Handler: index.lambda_handler 562 | MemorySize: 128 563 | Role: !GetAtt CreateSecretRotationLambdaRole.Arn 564 | Runtime: python3.9 565 | Timeout: 120 566 | VpcConfig: 567 | SecurityGroupIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SecretRotationLambdaSecurityGroup'}] 568 | SubnetIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}] 569 | 570 | CreateSecretRotationLambdaFnTrigger: 571 | Condition: IsNotUseDBSnapshot 572 | Type: Custom::LambdaAPGSecretsManager 573 | Version: "1.0" 574 | Properties: 575 | ServiceToken: !GetAtt 'CreateSecretRotationLambdaFn.Arn' 576 | SecretsManagerEndpoint: !Sub 'https://secretsmanager.${AWS::Region}.amazonaws.com' 577 | SecretRotationLambdaFnName: !Sub 'SecretsManager-SecretRotationFn-${AWS::StackName}' 578 | SecretRotationLambdaStackName: !Sub '${AWS::StackName}-SecretRotationLambdaStack' 579 | SubnetIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}] 580 | SecretRotationLambdaSG: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SecretRotationLambdaSecurityGroup'}] 581 | 582 | 583 | AuroraSecretRotationSchedule: 584 | Condition: IsNotUseDBSnapshot 585 | Type: AWS::SecretsManager::RotationSchedule 586 | DependsOn: 587 | - SecretAuroraClusterAttachment 588 | - AuroraDBFirstInstance 589 | Properties: 590 | SecretId: !Ref AuroraMasterSecret 591 | RotationLambdaARN: !GetAtt CreateSecretRotationLambdaFnTrigger.SecretRotationLambdaARN 592 | RotationRules: 593 | AutomaticallyAfterDays: 30 594 | 595 | AuroraDBCluster: 596 | Type: AWS::RDS::DBCluster 597 | DeletionPolicy: Snapshot 598 | UpdateReplacePolicy: Snapshot 599 | Properties: 600 | Engine: aurora-postgresql 601 | EngineVersion: !Ref DBEngineVersion 602 | DatabaseName: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Ref DBName] 603 | Port: !Ref DBPort 604 | MasterUsername: 605 | !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraMasterSecret, ':SecretString:username}}' ]]] 606 | MasterUserPassword: 607 | !If [IsUseDBSnapshot, !Ref "AWS::NoValue", !Join ['', ['{{resolve:secretsmanager:', !Ref AuroraMasterSecret, ':SecretString:password}}' ]]] 608 | DBSubnetGroupName: !Ref DBSubnetGroup 609 | VpcSecurityGroupIds: 610 | - !Ref ClusterSecurityGroup 611 | BackupRetentionPeriod: !If [IsProd, 35, 7] 612 | DBClusterParameterGroupName: !Ref RDSDBClusterParameterGroup 613 | SnapshotIdentifier: !If [IsUseDBSnapshot, !Ref DBSnapshotName, !Ref "AWS::NoValue"] 614 | StorageEncrypted: !If [IsUseDBSnapshot, !Ref "AWS::NoValue", true] 615 | KmsKeyId: !If [IsNotUseDBSnapshot, !Ref AuroraKMSCMK, !Ref 'AWS::NoValue'] 616 | EnableIAMDatabaseAuthentication: !If [DoEnableIAM, true, !Ref "AWS::NoValue"] 617 | Tags: 618 | - 619 | Key: EnvironmentStage 620 | Value: !Ref EnvironmentStage 621 | - 622 | Key: Application 623 | Value: !Ref Application 624 | - 625 | Key: ApplicationVersion 626 | Value: !Ref ApplicationVersion 627 | - 628 | Key: ProjectCostCenter 629 | Value: !Ref ProjectCostCenter 630 | - 631 | Key: ServiceOwnersEmailContact 632 | Value: !Ref ServiceOwnersEmailContact 633 | - 634 | Key: Confidentiality 635 | Value: !Ref Confidentiality 636 | - 637 | Key: Compliance 638 | Value: !Ref Compliance 639 | 640 | AuroraDBFirstInstance: 641 | Type: AWS::RDS::DBInstance 642 | Properties: 643 | CopyTagsToSnapshot: true 644 | DBInstanceClass: 645 | Ref: DBInstanceClass 646 | DBClusterIdentifier: !Ref AuroraDBCluster 647 | Engine: aurora-postgresql 648 | EngineVersion: !Ref DBEngineVersion 649 | DBParameterGroupName: 650 | Ref: DBParamGroup 651 | MonitoringInterval: !If [IsProd, 1, 0] 652 | MonitoringRoleArn: !If [IsProd, !GetAtt MonitoringIAMRole.Arn, !Ref "AWS::NoValue"] 653 | AutoMinorVersionUpgrade: !If [IsProd, 'false', 'true'] 654 | DBSubnetGroupName: !Ref DBSubnetGroup 655 | PubliclyAccessible: false 656 | EnablePerformanceInsights: true 657 | PerformanceInsightsKMSKeyId: !Ref AuroraKMSCMK 658 | PerformanceInsightsRetentionPeriod: !If [IsProd, 731, 7] 659 | Tags: 660 | - 661 | Key: EnvironmentStage 662 | Value: !Ref EnvironmentStage 663 | - 664 | Key: Application 665 | Value: !Ref Application 666 | - 667 | Key: ApplicationVersion 668 | Value: !Ref ApplicationVersion 669 | - 670 | Key: ProjectCostCenter 671 | Value: !Ref ProjectCostCenter 672 | - 673 | Key: ServiceOwnersEmailContact 674 | Value: !Ref ServiceOwnersEmailContact 675 | - 676 | Key: Confidentiality 677 | Value: !Ref Confidentiality 678 | - 679 | Key: Compliance 680 | Value: !Ref Compliance 681 | 682 | AuroraDBSecondInstance: 683 | Condition: IsReplica 684 | Type: AWS::RDS::DBInstance 685 | DependsOn: 686 | - AuroraDBFirstInstance 687 | Properties: 688 | CopyTagsToSnapshot: true 689 | DBInstanceClass: 690 | Ref: DBInstanceClass 691 | DBClusterIdentifier: !Ref AuroraDBCluster 692 | Engine: aurora-postgresql 693 | EngineVersion: !Ref DBEngineVersion 694 | DBParameterGroupName: 695 | Ref: DBParamGroup 696 | MonitoringInterval: !If [IsProd, 1, 0] 697 | MonitoringRoleArn: !If [IsProd, !GetAtt MonitoringIAMRole.Arn, !Ref "AWS::NoValue"] 698 | AutoMinorVersionUpgrade: !If [IsProd, 'false', 'true'] 699 | DBSubnetGroupName: !Ref DBSubnetGroup 700 | PubliclyAccessible: false 701 | EnablePerformanceInsights: true 702 | PerformanceInsightsKMSKeyId: !Ref AuroraKMSCMK 703 | PerformanceInsightsRetentionPeriod: !If [IsProd, 731, 7] 704 | Tags: 705 | - 706 | Key: EnvironmentStage 707 | Value: !Ref EnvironmentStage 708 | - 709 | Key: Application 710 | Value: !Ref Application 711 | - 712 | Key: ApplicationVersion 713 | Value: !Ref ApplicationVersion 714 | - 715 | Key: ProjectCostCenter 716 | Value: !Ref ProjectCostCenter 717 | - 718 | Key: ServiceOwnersEmailContact 719 | Value: !Ref ServiceOwnersEmailContact 720 | - 721 | Key: Confidentiality 722 | Value: !Ref Confidentiality 723 | - 724 | Key: Compliance 725 | Value: !Ref Compliance 726 | 727 | CPUUtilizationAlarm1: 728 | Type: "AWS::CloudWatch::Alarm" 729 | Properties: 730 | ActionsEnabled: true 731 | AlarmActions: 732 | - Ref: DBSNSTopic 733 | AlarmDescription: 'CPU_Utilization' 734 | Dimensions: 735 | - Name: DBInstanceIdentifier 736 | Value: 737 | Ref: AuroraDBFirstInstance 738 | MetricName: CPUUtilization 739 | Statistic: Maximum 740 | Namespace: 'AWS/RDS' 741 | Threshold: '80' 742 | Unit: Percent 743 | ComparisonOperator: 'GreaterThanOrEqualToThreshold' 744 | Period: '60' 745 | EvaluationPeriods: '5' 746 | TreatMissingData: 'notBreaching' 747 | 748 | CPUUtilizationAlarm2: 749 | Condition: IsReplica 750 | Type: "AWS::CloudWatch::Alarm" 751 | Properties: 752 | ActionsEnabled: true 753 | AlarmActions: 754 | - Ref: DBSNSTopic 755 | AlarmDescription: 'CPU_Utilization' 756 | Dimensions: 757 | - Name: DBInstanceIdentifier 758 | Value: 759 | Ref: AuroraDBSecondInstance 760 | MetricName: CPUUtilization 761 | Statistic: Maximum 762 | Namespace: 'AWS/RDS' 763 | Threshold: '80' 764 | Unit: Percent 765 | ComparisonOperator: 'GreaterThanOrEqualToThreshold' 766 | Period: '60' 767 | EvaluationPeriods: '5' 768 | TreatMissingData: 'notBreaching' 769 | 770 | MaxUsedTxIDsAlarm1: 771 | Type: "AWS::CloudWatch::Alarm" 772 | Properties: 773 | ActionsEnabled: true 774 | AlarmActions: 775 | - Ref: DBSNSTopic 776 | AlarmDescription: 'Maximum Used Transaction IDs' 777 | Dimensions: 778 | - Name: DBInstanceIdentifier 779 | Value: 780 | Ref: AuroraDBFirstInstance 781 | MetricName: 'MaximumUsedTransactionIDs' 782 | Statistic: Average 783 | Namespace: 'AWS/RDS' 784 | Threshold: '600000000' 785 | Unit: Count 786 | ComparisonOperator: 'GreaterThanOrEqualToThreshold' 787 | Period: '60' 788 | EvaluationPeriods: '5' 789 | TreatMissingData: 'notBreaching' 790 | 791 | MaxUsedTxIDsAlarm2: 792 | Condition: IsReplica 793 | Type: "AWS::CloudWatch::Alarm" 794 | Properties: 795 | ActionsEnabled: true 796 | AlarmActions: 797 | - Ref: DBSNSTopic 798 | AlarmDescription: 'Maximum Used Transaction IDs' 799 | Dimensions: 800 | - Name: DBInstanceIdentifier 801 | Value: 802 | Ref: AuroraDBSecondInstance 803 | MetricName: 'MaximumUsedTransactionIDs' 804 | Statistic: Average 805 | Namespace: 'AWS/RDS' 806 | Threshold: '600000000' 807 | Unit: Count 808 | ComparisonOperator: 'GreaterThanOrEqualToThreshold' 809 | Period: '60' 810 | EvaluationPeriods: '5' 811 | TreatMissingData: 'notBreaching' 812 | 813 | FreeLocalStorageAlarm1: 814 | Type: "AWS::CloudWatch::Alarm" 815 | Properties: 816 | ActionsEnabled: true 817 | AlarmActions: 818 | - Ref: DBSNSTopic 819 | AlarmDescription: 'Free Local Storage' 820 | Dimensions: 821 | - Name: DBInstanceIdentifier 822 | Value: 823 | Ref: AuroraDBFirstInstance 824 | MetricName: 'FreeLocalStorage' 825 | Statistic: Average 826 | Namespace: 'AWS/RDS' 827 | Threshold: '5368709120' 828 | Unit: Bytes 829 | ComparisonOperator: 'LessThanOrEqualToThreshold' 830 | Period: '60' 831 | EvaluationPeriods: '5' 832 | TreatMissingData: 'notBreaching' 833 | 834 | FreeLocalStorageAlarm2: 835 | Condition: IsReplica 836 | Type: "AWS::CloudWatch::Alarm" 837 | Properties: 838 | ActionsEnabled: true 839 | AlarmActions: 840 | - Ref: DBSNSTopic 841 | AlarmDescription: 'Free Local Storage' 842 | Dimensions: 843 | - Name: DBInstanceIdentifier 844 | Value: 845 | Ref: AuroraDBSecondInstance 846 | MetricName: 'FreeLocalStorage' 847 | Statistic: Average 848 | Namespace: 'AWS/RDS' 849 | Threshold: '5368709120' 850 | Unit: Bytes 851 | ComparisonOperator: 'LessThanOrEqualToThreshold' 852 | Period: '60' 853 | EvaluationPeriods: '5' 854 | TreatMissingData: 'notBreaching' 855 | 856 | DatabaseClusterEventSubscription: 857 | Type: 'AWS::RDS::EventSubscription' 858 | Properties: 859 | EventCategories: 860 | - failover 861 | - failure 862 | - notification 863 | SnsTopicArn: !Ref DBSNSTopic 864 | SourceIds: [!Ref AuroraDBCluster] 865 | SourceType: 'db-cluster' 866 | 867 | DatabaseInstanceEventSubscription: 868 | Type: 'AWS::RDS::EventSubscription' 869 | Properties: 870 | EventCategories: 871 | - availability 872 | - configuration change 873 | - deletion 874 | - failover 875 | - failure 876 | - maintenance 877 | - notification 878 | - recovery 879 | SnsTopicArn: !Ref DBSNSTopic 880 | SourceIds: 881 | - !Ref AuroraDBFirstInstance 882 | - !If [IsReplica, !Ref AuroraDBSecondInstance, !Ref "AWS::NoValue"] 883 | SourceType: 'db-instance' 884 | 885 | DBParameterGroupEventSubscription: 886 | Type: 'AWS::RDS::EventSubscription' 887 | Properties: 888 | EventCategories: 889 | - configuration change 890 | SnsTopicArn: !Ref DBSNSTopic 891 | SourceIds: [!Ref DBParamGroup] 892 | SourceType: 'db-parameter-group' 893 | 894 | DBBootStrapLambdaFn: 895 | Condition: DoDBBootStrap 896 | Type: AWS::Lambda::Function 897 | DependsOn: 898 | - DBBootStrapLambdaRole 899 | Properties: 900 | Code: 901 | S3Bucket: !Ref LambdaBootStrapS3Bucket 902 | S3Key: !Ref LambdaBootStrapS3Key 903 | Description: >- 904 | BootStrap newly Created Aurora PostgreSQL Database 905 | Handler: dbbootstrap.handler 906 | MemorySize: 128 907 | Role: !GetAtt DBBootStrapLambdaRole.Arn 908 | Runtime: python3.9 909 | Timeout: 60 910 | VpcConfig: 911 | SecurityGroupIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SecretRotationLambdaSecurityGroup'}] 912 | SubnetIds: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}] 913 | Environment: 914 | Variables: 915 | DBHost: !GetAtt 'AuroraDBCluster.Endpoint.Address' 916 | DBPort: !GetAtt 'AuroraDBCluster.Endpoint.Port' 917 | DBUser: !Ref DBUsername 918 | DBName: !Ref DBName 919 | Secret_ARN: !Ref AuroraMasterSecret 920 | Region_Name: !Ref "AWS::Region" 921 | 922 | DBBootStrapLambdaFnTrigger: 923 | Condition: DoDBBootStrap 924 | Type: Custom::LambdaAPGBootStrap 925 | DependsOn: 926 | - AuroraDBFirstInstance 927 | - AuroraSecretRotationSchedule 928 | Version: "1.0" 929 | Properties: 930 | ServiceToken: !GetAtt 'DBBootStrapLambdaFn.Arn' 931 | 932 | ############################################################################### 933 | # Outputs 934 | ############################################################################### 935 | Outputs: 936 | ClusterEndpoint: 937 | Description: 'Aurora Cluster/Writer Endpoint' 938 | Value: !GetAtt 'AuroraDBCluster.Endpoint.Address' 939 | ReaderEndpoint: 940 | Description: 'Aurora Reader Endpoint' 941 | Value: !GetAtt 'AuroraDBCluster.ReadEndpoint.Address' 942 | Port: 943 | Description: 'Aurora Endpoint Port' 944 | Value: !GetAtt 'AuroraDBCluster.Endpoint.Port' 945 | DBUsername: 946 | Description: 'Database master username' 947 | Value: !Ref DBUsername 948 | DBName: 949 | Description: 'Database Name' 950 | Value: !Ref DBName 951 | PSQLCommandLine: 952 | Description: PSQL Command Line 953 | Value: !Join 954 | - '' 955 | - - 'psql --host=' 956 | - !GetAtt 'AuroraDBCluster.Endpoint.Address' 957 | - ' --port=' 958 | - !GetAtt 'AuroraDBCluster.Endpoint.Port' 959 | - ' --username=' 960 | - !Ref DBUsername 961 | - ' --dbname=' 962 | - !Ref DBName 963 | -------------------------------------------------------------------------------- /cftemplates/VPC-3AZs.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | 3 | Description: 'Cloudformation Template to create a VPC with public and private subnets in 3 AZs' 4 | 5 | Metadata: 6 | 'AWS::CloudFormation::Interface': 7 | ParameterGroups: 8 | - Label: 9 | default: 'VPC Parameters' 10 | Parameters: 11 | - ClassB 12 | ParameterLabels: 13 | ClassB: 14 | default: ClassB 2nd Octet 15 | 16 | Parameters: 17 | ClassB: 18 | Description: 'Specify the 2nd Octet of IPv4 CIDR block for the VPC (10.XXX.0.0/16) in the range [0-255]' 19 | Type: Number 20 | Default: 0 21 | ConstraintDescription: 'Must be in the range [0-255]' 22 | MinValue: 0 23 | MaxValue: 255 24 | 25 | Resources: 26 | VPC: 27 | Type: 'AWS::EC2::VPC' 28 | Properties: 29 | CidrBlock: !Sub '10.${ClassB}.0.0/16' 30 | EnableDnsSupport: true 31 | EnableDnsHostnames: true 32 | InstanceTenancy: default 33 | Tags: 34 | - Key: Name 35 | Value: !Sub '${AWS::StackName}-VPC' 36 | 37 | InternetGateway: 38 | Type: 'AWS::EC2::InternetGateway' 39 | Properties: 40 | Tags: 41 | - Key: Name 42 | Value: !Sub '10.${ClassB}.0.0/16' 43 | 44 | VPCGatewayAttachment: 45 | Type: 'AWS::EC2::VPCGatewayAttachment' 46 | Properties: 47 | VpcId: !Ref VPC 48 | InternetGatewayId: !Ref InternetGateway 49 | 50 | NATEIPA: 51 | DependsOn: VPCGatewayAttachment 52 | Type: AWS::EC2::EIP 53 | Properties: 54 | Domain: vpc 55 | 56 | NATEIPB: 57 | DependsOn: VPCGatewayAttachment 58 | Type: AWS::EC2::EIP 59 | Properties: 60 | Domain: vpc 61 | 62 | NATEIPC: 63 | DependsOn: VPCGatewayAttachment 64 | Type: AWS::EC2::EIP 65 | Properties: 66 | Domain: vpc 67 | 68 | SubnetAPublic: 69 | Type: 'AWS::EC2::Subnet' 70 | Properties: 71 | AvailabilityZone: !Select [0, !GetAZs ''] 72 | CidrBlock: !Sub '10.${ClassB}.0.0/20' 73 | MapPublicIpOnLaunch: true 74 | VpcId: !Ref VPC 75 | Tags: 76 | - Key: Name 77 | Value: !Join 78 | - '_' 79 | - - !Sub '10.${ClassB}.0.0/16' 80 | - !Select [0, !GetAZs ''] 81 | - 'Public' 82 | - Key: Reach 83 | Value: public 84 | 85 | SubnetAPrivate: 86 | Type: 'AWS::EC2::Subnet' 87 | Properties: 88 | AvailabilityZone: !Select [0, !GetAZs ''] 89 | CidrBlock: !Sub '10.${ClassB}.16.0/20' 90 | VpcId: !Ref VPC 91 | Tags: 92 | - Key: Name 93 | Value: !Join 94 | - '_' 95 | - - !Sub '10.${ClassB}.0.0/16' 96 | - !Select [0, !GetAZs ''] 97 | - 'Private' 98 | - Key: Reach 99 | Value: private 100 | 101 | SubnetBPublic: 102 | Type: 'AWS::EC2::Subnet' 103 | Properties: 104 | AvailabilityZone: !Select [1, !GetAZs ''] 105 | CidrBlock: !Sub '10.${ClassB}.32.0/20' 106 | MapPublicIpOnLaunch: true 107 | VpcId: !Ref VPC 108 | Tags: 109 | - Key: Name 110 | Value: !Join 111 | - '_' 112 | - - !Sub '10.${ClassB}.0.0/16' 113 | - !Select [1, !GetAZs ''] 114 | - 'Public' 115 | - Key: Reach 116 | Value: public 117 | 118 | SubnetBPrivate: 119 | Type: 'AWS::EC2::Subnet' 120 | Properties: 121 | AvailabilityZone: !Select [1, !GetAZs ''] 122 | CidrBlock: !Sub '10.${ClassB}.48.0/20' 123 | VpcId: !Ref VPC 124 | Tags: 125 | - Key: Name 126 | Value: !Join 127 | - '_' 128 | - - !Sub '10.${ClassB}.0.0/16' 129 | - !Select [1, !GetAZs ''] 130 | - 'Private' 131 | - Key: Reach 132 | Value: private 133 | 134 | SubnetCPublic: 135 | Type: 'AWS::EC2::Subnet' 136 | Properties: 137 | AvailabilityZone: !Select [2, !GetAZs ''] 138 | CidrBlock: !Sub '10.${ClassB}.64.0/20' 139 | MapPublicIpOnLaunch: true 140 | VpcId: !Ref VPC 141 | Tags: 142 | - Key: Name 143 | Value: !Join 144 | - '_' 145 | - - !Sub '10.${ClassB}.0.0/16' 146 | - !Select [2, !GetAZs ''] 147 | - 'Public' 148 | - Key: Reach 149 | Value: public 150 | 151 | SubnetCPrivate: 152 | Type: 'AWS::EC2::Subnet' 153 | Properties: 154 | AvailabilityZone: !Select [2, !GetAZs ''] 155 | CidrBlock: !Sub '10.${ClassB}.80.0/20' 156 | VpcId: !Ref VPC 157 | Tags: 158 | - Key: Name 159 | Value: !Join 160 | - '_' 161 | - - !Sub '10.${ClassB}.0.0/16' 162 | - !Select [2, !GetAZs ''] 163 | - 'Private' 164 | - Key: Reach 165 | Value: private 166 | 167 | RouteTablePublic: 168 | Type: 'AWS::EC2::RouteTable' 169 | Properties: 170 | VpcId: !Ref VPC 171 | Tags: 172 | - Key: Name 173 | Value: !Join 174 | - '_' 175 | - - !Sub '10.${ClassB}.0.0/16' 176 | - 'Public' 177 | 178 | RouteTableAPrivate: 179 | Type: 'AWS::EC2::RouteTable' 180 | Properties: 181 | VpcId: !Ref VPC 182 | Tags: 183 | - Key: Name 184 | Value: !Join 185 | - '_' 186 | - - !Sub '10.${ClassB}.0.0/16' 187 | - !Select [0, !GetAZs ''] 188 | - 'Private' 189 | 190 | RouteTableBPrivate: 191 | Type: 'AWS::EC2::RouteTable' 192 | Properties: 193 | VpcId: !Ref VPC 194 | Tags: 195 | - Key: Name 196 | Value: !Join 197 | - '_' 198 | - - !Sub '10.${ClassB}.0.0/16' 199 | - !Select [1, !GetAZs ''] 200 | - 'Private' 201 | 202 | RouteTableCPrivate: 203 | Type: 'AWS::EC2::RouteTable' 204 | Properties: 205 | VpcId: !Ref VPC 206 | Tags: 207 | - Key: Name 208 | Value: !Join 209 | - '_' 210 | - - !Sub '10.${ClassB}.0.0/16' 211 | - !Select [2, !GetAZs ''] 212 | - 'Private' 213 | 214 | RouteTableAssociationAPublic: 215 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 216 | Properties: 217 | SubnetId: !Ref SubnetAPublic 218 | RouteTableId: !Ref RouteTablePublic 219 | 220 | RouteTableAssociationAPrivate: 221 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 222 | Properties: 223 | SubnetId: !Ref SubnetAPrivate 224 | RouteTableId: !Ref RouteTableAPrivate 225 | 226 | RouteTableAssociationBPublic: 227 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 228 | Properties: 229 | SubnetId: !Ref SubnetBPublic 230 | RouteTableId: !Ref RouteTablePublic 231 | 232 | RouteTableAssociationBPrivate: 233 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 234 | Properties: 235 | SubnetId: !Ref SubnetBPrivate 236 | RouteTableId: !Ref RouteTableBPrivate 237 | 238 | RouteTableAssociationCPublic: 239 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 240 | Properties: 241 | SubnetId: !Ref SubnetCPublic 242 | RouteTableId: !Ref RouteTablePublic 243 | 244 | RouteTableAssociationCPrivate: 245 | Type: 'AWS::EC2::SubnetRouteTableAssociation' 246 | Properties: 247 | SubnetId: !Ref SubnetCPrivate 248 | RouteTableId: !Ref RouteTableCPrivate 249 | 250 | RouteTablePublicInternetRoute: 251 | Type: 'AWS::EC2::Route' 252 | DependsOn: VPCGatewayAttachment 253 | Properties: 254 | RouteTableId: !Ref RouteTablePublic 255 | DestinationCidrBlock: '0.0.0.0/0' 256 | GatewayId: !Ref InternetGateway 257 | 258 | NetworkAclPublic: 259 | Type: 'AWS::EC2::NetworkAcl' 260 | Properties: 261 | VpcId: !Ref VPC 262 | Tags: 263 | - Key: Name 264 | Value: !Join 265 | - '_' 266 | - - !Sub '10.${ClassB}.0.0/16' 267 | - 'NACL' 268 | - 'Public' 269 | 270 | NetworkAclPrivate: 271 | Type: 'AWS::EC2::NetworkAcl' 272 | Properties: 273 | VpcId: !Ref VPC 274 | Tags: 275 | - Key: Name 276 | Value: !Join 277 | - '_' 278 | - - !Sub '10.${ClassB}.0.0/16' 279 | - 'NACL' 280 | - 'Private' 281 | 282 | SubnetNetworkAclAssociationAPublic: 283 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 284 | Properties: 285 | SubnetId: !Ref SubnetAPublic 286 | NetworkAclId: !Ref NetworkAclPublic 287 | 288 | SubnetNetworkAclAssociationAPrivate: 289 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 290 | Properties: 291 | SubnetId: !Ref SubnetAPrivate 292 | NetworkAclId: !Ref NetworkAclPrivate 293 | 294 | SubnetNetworkAclAssociationBPublic: 295 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 296 | Properties: 297 | SubnetId: !Ref SubnetBPublic 298 | NetworkAclId: !Ref NetworkAclPublic 299 | 300 | SubnetNetworkAclAssociationBPrivate: 301 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 302 | Properties: 303 | SubnetId: !Ref SubnetBPrivate 304 | NetworkAclId: !Ref NetworkAclPrivate 305 | 306 | SubnetNetworkAclAssociationCPublic: 307 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 308 | Properties: 309 | SubnetId: !Ref SubnetCPublic 310 | NetworkAclId: !Ref NetworkAclPublic 311 | 312 | SubnetNetworkAclAssociationCPrivate: 313 | Type: 'AWS::EC2::SubnetNetworkAclAssociation' 314 | Properties: 315 | SubnetId: !Ref SubnetCPrivate 316 | NetworkAclId: !Ref NetworkAclPrivate 317 | 318 | NetworkAclEntryInPublicAllowAll: 319 | Type: 'AWS::EC2::NetworkAclEntry' 320 | Properties: 321 | NetworkAclId: !Ref NetworkAclPublic 322 | RuleNumber: 99 323 | Protocol: -1 324 | RuleAction: allow 325 | Egress: false 326 | CidrBlock: '0.0.0.0/0' 327 | 328 | NetworkAclEntryOutPublicAllowAll: 329 | Type: 'AWS::EC2::NetworkAclEntry' 330 | Properties: 331 | NetworkAclId: !Ref NetworkAclPublic 332 | RuleNumber: 99 333 | Protocol: -1 334 | RuleAction: allow 335 | Egress: true 336 | CidrBlock: '0.0.0.0/0' 337 | 338 | NetworkAclEntryInPrivateAllowVPC: 339 | Type: 'AWS::EC2::NetworkAclEntry' 340 | Properties: 341 | NetworkAclId: !Ref NetworkAclPrivate 342 | RuleNumber: 99 343 | Protocol: -1 344 | RuleAction: allow 345 | Egress: false 346 | CidrBlock: '0.0.0.0/0' 347 | 348 | NetworkAclEntryOutPrivateAllowVPC: 349 | Type: 'AWS::EC2::NetworkAclEntry' 350 | Properties: 351 | NetworkAclId: !Ref NetworkAclPrivate 352 | RuleNumber: 99 353 | Protocol: -1 354 | RuleAction: allow 355 | Egress: true 356 | CidrBlock: '0.0.0.0/0' 357 | 358 | NATGatewayA: 359 | DependsOn: VPC 360 | Type: AWS::EC2::NatGateway 361 | Properties: 362 | AllocationId: !GetAtt [NATEIPA,AllocationId] 363 | SubnetId: !Ref SubnetAPublic 364 | Tags: 365 | - Key: Name 366 | Value: !Join 367 | - '_' 368 | - - !Sub '10.${ClassB}.0.0/16' 369 | - !Select [0, !GetAZs ''] 370 | - 'NGW' 371 | 372 | NATGatewayB: 373 | DependsOn: VPC 374 | Type: AWS::EC2::NatGateway 375 | Properties: 376 | AllocationId: !GetAtt [NATEIPB,AllocationId] 377 | SubnetId: !Ref SubnetBPublic 378 | Tags: 379 | - Key: Name 380 | Value: !Join 381 | - '_' 382 | - - !Sub '10.${ClassB}.0.0/16' 383 | - !Select [1, !GetAZs ''] 384 | - 'NGW' 385 | 386 | NATGatewayC: 387 | DependsOn: VPC 388 | Type: AWS::EC2::NatGateway 389 | Properties: 390 | AllocationId: !GetAtt [NATEIPC,AllocationId] 391 | SubnetId: !Ref SubnetCPublic 392 | Tags: 393 | - Key: Name 394 | Value: !Join 395 | - '_' 396 | - - !Sub '10.${ClassB}.0.0/16' 397 | - !Select [2, !GetAZs ''] 398 | - 'NGW' 399 | 400 | RouteTablePrivateANATRoute: 401 | Type: AWS::EC2::Route 402 | Properties: 403 | RouteTableId: !Ref RouteTableAPrivate 404 | DestinationCidrBlock: 0.0.0.0/0 405 | NatGatewayId: !Ref NATGatewayA 406 | 407 | RouteTablePrivateBNATRoute: 408 | Type: AWS::EC2::Route 409 | Properties: 410 | RouteTableId: !Ref RouteTableBPrivate 411 | DestinationCidrBlock: 0.0.0.0/0 412 | NatGatewayId: !Ref NATGatewayB 413 | 414 | RouteTablePrivateCNATRoute: 415 | Type: AWS::EC2::Route 416 | Properties: 417 | RouteTableId: !Ref RouteTableCPrivate 418 | DestinationCidrBlock: 0.0.0.0/0 419 | NatGatewayId: !Ref NATGatewayC 420 | 421 | S3VPCEndpoint: 422 | Type: 'AWS::EC2::VPCEndpoint' 423 | Properties: 424 | PolicyDocument: 425 | Version: 2012-10-17 426 | Statement: 427 | - Action: '*' 428 | Effect: Allow 429 | Resource: '*' 430 | Principal: '*' 431 | RouteTableIds: 432 | - !Ref RouteTableAPrivate 433 | - !Ref RouteTableBPrivate 434 | - !Ref RouteTableCPrivate 435 | ServiceName: !Join 436 | - '' 437 | - - com.amazonaws. 438 | - !Ref 'AWS::Region' 439 | - .s3 440 | VpcId: !Ref VPC 441 | 442 | SecretRotationLambdaSecurityGroup: 443 | Type: AWS::EC2::SecurityGroup 444 | Properties: 445 | GroupDescription: !Join [ " - ", [ "Security group for Secret Rotation Lambda ENIs", !Ref 'AWS::StackName' ] ] 446 | VpcId: !Ref VPC 447 | Tags: 448 | - Key: Name 449 | Value: !Sub '${AWS::StackName}-SecretRotationLambdaSecurityGroup' 450 | 451 | SecretRotationLambdaSecurityGroupIngress: 452 | Type: 'AWS::EC2::SecurityGroupIngress' 453 | Properties: 454 | GroupId: !GetAtt 'SecretRotationLambdaSecurityGroup.GroupId' 455 | IpProtocol: -1 456 | SourceSecurityGroupId: !Ref SecretRotationLambdaSecurityGroup 457 | Description: 'Self Reference' 458 | 459 | SecretsManagerVPCEndpoint: 460 | Type: 'AWS::EC2::VPCEndpoint' 461 | Properties: 462 | VpcEndpointType: 'Interface' 463 | PrivateDnsEnabled: true 464 | VpcId: !Ref VPC 465 | SubnetIds: 466 | - !Ref SubnetAPrivate 467 | - !Ref SubnetBPrivate 468 | - !Ref SubnetCPrivate 469 | SecurityGroupIds: 470 | - !Ref SecretRotationLambdaSecurityGroup 471 | ServiceName: !Join 472 | - '' 473 | - - com.amazonaws. 474 | - !Ref 'AWS::Region' 475 | - .secretsmanager 476 | 477 | Outputs: 478 | TemplateID: 479 | Description: 'Template ID' 480 | Value: 'VPC-3AZs' 481 | 482 | StackName: 483 | Description: 'Stack name' 484 | Value: !Sub '${AWS::StackName}' 485 | 486 | VPC: 487 | Description: 'VPC' 488 | Value: !Ref VPC 489 | Export: 490 | Name: !Sub '${AWS::StackName}-VPC' 491 | 492 | ClassB: 493 | Description: 'Class B' 494 | Value: !Ref ClassB 495 | Export: 496 | Name: !Sub '${AWS::StackName}-ClassB' 497 | 498 | CidrBlock: 499 | Description: 'The set of IP addresses for the VPC' 500 | Value: !GetAtt 'VPC.CidrBlock' 501 | Export: 502 | Name: !Sub '${AWS::StackName}-CidrBlock' 503 | 504 | AZs: 505 | Description: 'AZs' 506 | Value: 3 507 | Export: 508 | Name: !Sub '${AWS::StackName}-AZs' 509 | 510 | AZA: 511 | Description: 'AZ of A' 512 | Value: !Select [0, !GetAZs ''] 513 | Export: 514 | Name: !Sub '${AWS::StackName}-AZA' 515 | 516 | AZB: 517 | Description: 'AZ of B' 518 | Value: !Select [1, !GetAZs ''] 519 | Export: 520 | Name: !Sub '${AWS::StackName}-AZB' 521 | 522 | AZC: 523 | Description: 'AZ of C' 524 | Value: !Select [2, !GetAZs ''] 525 | Export: 526 | Name: !Sub '${AWS::StackName}-AZC' 527 | 528 | SubnetsPublic: 529 | Description: 'Subnets public' 530 | Value: !Join [',', [!Ref SubnetAPublic, !Ref SubnetBPublic, !Ref SubnetCPublic]] 531 | Export: 532 | Name: !Sub '${AWS::StackName}-SubnetsPublic' 533 | 534 | SubnetsPrivate: 535 | Description: 'Subnets private' 536 | Value: !Join [',', [!Ref SubnetAPrivate, !Ref SubnetBPrivate, !Ref SubnetCPrivate]] 537 | Export: 538 | Name: !Sub '${AWS::StackName}-SubnetsPrivate' 539 | 540 | RouteTablesPublic: 541 | Description: 'Route tables public' 542 | Value: !Ref RouteTablePublic 543 | Export: 544 | Name: !Sub '${AWS::StackName}-RouteTablePublic' 545 | 546 | RouteTablesPrivate: 547 | Description: 'Route tables private' 548 | Value: !Join [',', [!Ref RouteTableAPrivate, !Ref RouteTableBPrivate, !Ref RouteTableCPrivate]] 549 | Export: 550 | Name: !Sub '${AWS::StackName}-RouteTablesPrivate' 551 | 552 | SubnetAPublic: 553 | Description: 'Subnet A public' 554 | Value: !Ref SubnetAPublic 555 | Export: 556 | Name: !Sub '${AWS::StackName}-SubnetAPublic' 557 | 558 | SubnetAPrivate: 559 | Description: 'Subnet A private' 560 | Value: !Ref SubnetAPrivate 561 | Export: 562 | Name: !Sub '${AWS::StackName}-SubnetAPrivate' 563 | 564 | RouteTableAPrivate: 565 | Description: 'Route table A private' 566 | Value: !Ref RouteTableAPrivate 567 | Export: 568 | Name: !Sub '${AWS::StackName}-RouteTableAPrivate' 569 | 570 | SubnetBPublic: 571 | Description: 'Subnet B public' 572 | Value: !Ref SubnetBPublic 573 | Export: 574 | Name: !Sub '${AWS::StackName}-SubnetBPublic' 575 | 576 | SubnetBPrivate: 577 | Description: 'Subnet B private' 578 | Value: !Ref SubnetBPrivate 579 | Export: 580 | Name: !Sub '${AWS::StackName}-SubnetBPrivate' 581 | 582 | RouteTableBPrivate: 583 | Description: 'Route table B private' 584 | Value: !Ref RouteTableBPrivate 585 | Export: 586 | Name: !Sub '${AWS::StackName}-RouteTableBPrivate' 587 | 588 | SubnetCPublic: 589 | Description: 'Subnet C public' 590 | Value: !Ref SubnetCPublic 591 | Export: 592 | Name: !Sub '${AWS::StackName}-SubnetCPublic' 593 | 594 | SubnetCPrivate: 595 | Description: 'Subnet C private' 596 | Value: !Ref SubnetCPrivate 597 | Export: 598 | Name: !Sub '${AWS::StackName}-SubnetCPrivate' 599 | 600 | RouteTableCPrivate: 601 | Description: 'Route table C private' 602 | Value: !Ref RouteTableCPrivate 603 | Export: 604 | Name: !Sub '${AWS::StackName}-RouteTableCPrivate' 605 | 606 | S3VPCEndpoint: 607 | Description: S3 VPC Endpoint 608 | Value: !Ref S3VPCEndpoint 609 | Export: 610 | Name: !Sub '${AWS::StackName}-S3VPCEndpoint' 611 | 612 | SecretsManagerVPCEndpoint: 613 | Description: Secrets Manager VPC Endpoint 614 | Value: !Ref SecretsManagerVPCEndpoint 615 | Export: 616 | Name: !Sub '${AWS::StackName}-SecretsManagerVPCEndpoint' 617 | 618 | SecretRotationLambdaSecurityGroup: 619 | Description: Secret Rotation Lambda SecurityGroup for AWS Secrets Manager 620 | Value: !Ref SecretRotationLambdaSecurityGroup 621 | Export: 622 | Name: !Sub '${AWS::StackName}-SecretRotationLambdaSecurityGroup' 623 | -------------------------------------------------------------------------------- /cftemplates/VPC-SSH-Bastion.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | 3 | Description: >- 4 | Cloudformation template to create a highly available SSH bastion host 5 | 6 | Metadata: 7 | AWS::CloudFormation::Interface: 8 | ParameterGroups: 9 | - Label: 10 | default: Network Configuration 11 | Parameters: 12 | - ParentVPCStack 13 | - RemoteAccessCIDR 14 | - Label: 15 | default: Amazon EC2 Configuration 16 | Parameters: 17 | - KeyPairName 18 | - BastionInstanceType 19 | - LogsRetentionInDays 20 | - NotificationList 21 | - Label: 22 | default: Linux Bastion Configuration 23 | Parameters: 24 | - BastionTenancy 25 | - EnableBanner 26 | - BastionBanner 27 | - EnableTCPForwarding 28 | - EnableX11Forwarding 29 | 30 | ParameterLabels: 31 | BastionTenancy: 32 | default: Bastion Tenancy 33 | BastionBanner: 34 | default: Bastion Banner 35 | BastionInstanceType: 36 | default: Bastion Instance Type 37 | EnableBanner: 38 | default: Enable Banner 39 | EnableTCPForwarding: 40 | default: Enable TCP Forwarding 41 | EnableX11Forwarding: 42 | default: Enable X11 Forwarding 43 | KeyPairName: 44 | default: Key Pair Name 45 | RemoteAccessCIDR: 46 | default: Allowed Bastion External Access CIDR 47 | AltInitScript: 48 | default: Custom Bootstrap Script 49 | OSImageOverride: 50 | default: AMI override 51 | NotificationList: 52 | default: SNS Notification Email 53 | 54 | 55 | Parameters: 56 | 57 | ParentVPCStack: 58 | Description: 'Stack name of parent VPC stack based on VPC-3AZs yaml template. Refer Cloudformation dashboard in AWS Console to get this.' 59 | Type: String 60 | 61 | NotificationList: 62 | Type: String 63 | Description: The Email notification list is used to configure a SNS topic for sending cloudwatch alarm notifications 64 | AllowedPattern: '^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$' 65 | ConstraintDescription: provide a valid email address. 66 | 67 | LogsRetentionInDays: 68 | Description: Specify the number of days you want to retain log events 69 | Type: Number 70 | Default: 14 71 | AllowedValues: [1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400, 545, 731, 1827, 3653] 72 | 73 | BastionBanner: 74 | Default: default 75 | Description: Banner text to display upon login. Leave at default or provide AWS S3 location for the file containing Banner text. 76 | Type: String 77 | ConstraintDescription: URL must begin with s3:// 78 | 79 | BastionTenancy: 80 | Description: 'VPC Tenancy in which bastion host will be launched. Options: ''dedicated'' or ''default''' 81 | Type: String 82 | Default: default 83 | AllowedValues: 84 | - dedicated 85 | - default 86 | 87 | BastionInstanceType: 88 | AllowedValues: 89 | - t3.micro 90 | - t3.small 91 | - t3.medium 92 | - t3.large 93 | - t3.xlarge 94 | - t3.2xlarge 95 | - m5.large 96 | - m5.xlarge 97 | - m5.2xlarge 98 | - m5.4xlarge 99 | Default: t3.micro 100 | Description: Amazon EC2 instance type for the bastion instance 101 | Type: String 102 | 103 | EnableBanner: 104 | AllowedValues: 105 | - 'true' 106 | - 'false' 107 | Default: 'true' 108 | Description: >- 109 | To include a banner to be displayed when connecting via SSH to the 110 | bastion, set this parameter to true 111 | Type: String 112 | 113 | EnableTCPForwarding: 114 | Type: String 115 | Description: Enable/Disable TCP Forwarding 116 | Default: 'false' 117 | AllowedValues: 118 | - 'true' 119 | - 'false' 120 | 121 | EnableX11Forwarding: 122 | Type: String 123 | Description: Enable/Disable X11 Forwarding 124 | Default: 'false' 125 | AllowedValues: 126 | - 'true' 127 | - 'false' 128 | 129 | KeyPairName: 130 | Description: >- 131 | Enter a Public/private key pair. If you do not have one in this AWS Region, 132 | create it before continuing 133 | Type: 'AWS::EC2::KeyPair::KeyName' 134 | 135 | RemoteAccessCIDR: 136 | AllowedPattern: >- 137 | ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$ 138 | ConstraintDescription: CIDR block parameter must be in the form x.x.x.x/x 139 | Description: Allowed CIDR block in the x.x.x.x/x format for external SSH access to the bastion host 140 | Type: String 141 | 142 | 143 | AltInitScript: 144 | AllowedPattern: ^http.*|^$ 145 | ConstraintDescription: URL must begin with http 146 | Description: Optional. Specify custom bootstrap script AWS S3 location to run during bastion host setup 147 | Default: '' 148 | Type: String 149 | 150 | OSImageOverride: 151 | Description: Optional. Specify a region specific image to use for the instance 152 | Type: String 153 | Default: '' 154 | 155 | 156 | 157 | Mappings: 158 | 159 | AWSAMIRegionMap: 160 | af-south-1: 161 | AMZNLINUX2: ami-0936d2754993c364e 162 | ap-northeast-1: 163 | AMZNLINUX2: ami-0ca38c7440de1749a 164 | ap-northeast-2: 165 | AMZNLINUX2: ami-0f2c95e9fe3f8f80e 166 | ap-northeast-3: 167 | AMZNLINUX2: ami-06e9ad0943b200859 168 | ap-south-1: 169 | AMZNLINUX2: ami-010aff33ed5991201 170 | ap-southeast-1: 171 | AMZNLINUX2: ami-02f26adf094f51167 172 | ap-southeast-2: 173 | AMZNLINUX2: ami-0186908e2fdeea8f3 174 | ca-central-1: 175 | AMZNLINUX2: ami-0101734ab73bd9e15 176 | eu-central-1: 177 | AMZNLINUX2: ami-043097594a7df80ec 178 | me-south-1: 179 | AMZNLINUX2: ami-0880769bc15eeec4f 180 | ap-east-1: 181 | AMZNLINUX2: ami-0aca22cb23f122f27 182 | eu-north-1: 183 | AMZNLINUX2: ami-050fdc53cf6ba8f7f 184 | eu-south-1: 185 | AMZNLINUX2: ami-0f447354763f0eaac 186 | eu-west-1: 187 | AMZNLINUX2: ami-063d4ab14480ac177 188 | eu-west-2: 189 | AMZNLINUX2: ami-06dc09bb8854cbde3 190 | eu-west-3: 191 | AMZNLINUX2: ami-0b3e57ee3b63dd76b 192 | sa-east-1: 193 | AMZNLINUX2: ami-05373777d08895384 194 | us-east-1: 195 | AMZNLINUX2: ami-0d5eff06f840b45e9 196 | us-gov-west-1: 197 | AMZNLINUX2: ami-0bbf3595bb2fb39ec 198 | us-gov-east-1: 199 | AMZNLINUX2: ami-0cc17d57bec8c6017 200 | us-east-2: 201 | AMZNLINUX2: ami-077e31c4939f6a2f3 202 | us-west-1: 203 | AMZNLINUX2: ami-04468e03c37242e1e 204 | us-west-2: 205 | AMZNLINUX2: ami-0cf6f5c8a62fa5da6 206 | cn-north-1: 207 | AMZNLINUX2: ami-0c52e2685c7218558 208 | cn-northwest-1: 209 | AMZNLINUX2: ami-05b9b6d6acf8ae9b6 210 | 211 | 212 | LinuxAMINameMap: 213 | Amazon-Linux2-HVM: 214 | Code: AMZNLINUX2 215 | OS: Amazon 216 | 217 | Conditions: 218 | 219 | UseAlternativeInitialization: !Not 220 | - !Equals 221 | - !Ref AltInitScript 222 | - '' 223 | 224 | UseOSImageOverride: !Not 225 | - !Equals 226 | - !Ref OSImageOverride 227 | - '' 228 | 229 | DefaultBanner: !Equals [!Ref BastionBanner, 'default'] 230 | 231 | Resources: 232 | 233 | EC2SNSTopic: 234 | Type: AWS::SNS::Topic 235 | Properties: 236 | Subscription: 237 | - Endpoint: !Ref NotificationList 238 | Protocol: email 239 | 240 | BastionMainLogGroup: 241 | Type: 'AWS::Logs::LogGroup' 242 | Properties: 243 | RetentionInDays: !Ref LogsRetentionInDays 244 | 245 | SSHMetricFilter: 246 | Type: 'AWS::Logs::MetricFilter' 247 | Properties: 248 | LogGroupName: !Ref BastionMainLogGroup 249 | FilterPattern: USER_LOGIN 250 | MetricTransformations: 251 | - MetricName: SSHCommandCount 252 | MetricValue: 1 253 | MetricNamespace: !Join 254 | - / 255 | - - AWSQuickStart 256 | - !Ref 'AWS::StackName' 257 | 258 | BastionSecurityGroup: 259 | Type: 'AWS::EC2::SecurityGroup' 260 | Properties: 261 | GroupDescription: !Ref 'AWS::StackName' 262 | VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'} 263 | SecurityGroupIngress: 264 | - IpProtocol: tcp 265 | FromPort: '22' 266 | ToPort: '22' 267 | CidrIp: !Ref RemoteAccessCIDR 268 | - IpProtocol: icmp 269 | FromPort: '-1' 270 | ToPort: '-1' 271 | CidrIp: !Ref RemoteAccessCIDR 272 | Tags: 273 | - Key: Name 274 | Value: !Sub '${AWS::StackName}-BastionSecurityGroup' 275 | 276 | BastionHostRole: 277 | Type: 'AWS::IAM::Role' 278 | Properties: 279 | Policies: 280 | - PolicyDocument: 281 | Version: 2012-10-17 282 | Statement: 283 | - Action: 284 | - 's3:GetObject' 285 | Resource: !Sub 'arn:${AWS::Partition}:s3:::aws-quickstart-${AWS::Region}/quickstart-linux-bastion/*' 286 | Effect: Allow 287 | PolicyName: aws-quick-start-s3-policy 288 | - PolicyDocument: 289 | Version: 2012-10-17 290 | Statement: 291 | - Action: 292 | - 'logs:CreateLogStream' 293 | - 'logs:GetLogEvents' 294 | - 'logs:PutLogEvents' 295 | - 'logs:DescribeLogGroups' 296 | - 'logs:DescribeLogStreams' 297 | - 'logs:PutRetentionPolicy' 298 | - 'logs:PutMetricFilter' 299 | - 'logs:CreateLogGroup' 300 | Resource: !Sub "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:${BastionMainLogGroup}:*" 301 | Effect: Allow 302 | PolicyName: bastion-cloudwatch-logs-policy 303 | - PolicyDocument: 304 | Version: 2012-10-17 305 | Statement: 306 | - Action: 307 | - 'ec2:AssociateAddress' 308 | - 'ec2:DescribeAddresses' 309 | Resource: 310 | - '*' 311 | Effect: Allow 312 | PolicyName: bastion-eip-policy 313 | - PolicyDocument: 314 | Version: 2012-10-17 315 | Statement: 316 | - Action: 317 | - 'ec2:DescribeInstances' 318 | - 'ec2:DescribeTags' 319 | Resource: 320 | - '*' 321 | Effect: Allow 322 | PolicyName: ec2-selfaccess-policy 323 | - PolicyDocument: 324 | Version: 2012-10-17 325 | Statement: 326 | - Action: 327 | - 'iam:ListAccountAliases' 328 | - 'autoscaling:DescribeAutoScalingInstances' 329 | - 'autoscaling:DescribeAutoScalingGroups' 330 | - 'autoscaling:DescribeLifecycle*' 331 | Resource: 332 | - '*' 333 | Effect: Allow 334 | PolicyName: ASGSelfAccessPolicy 335 | - PolicyDocument: 336 | Version: 2012-10-17 337 | Statement: 338 | - Action: 339 | - 'autoscaling:CompleteLifecycleAction' 340 | - 'autoscaling:RecordLifecycleActionHeartbeat' 341 | Resource: 342 | - !Sub 'arn:${AWS::Partition}:autoscaling:${AWS::Region}:${AWS::AccountId}:autoScalingGroup:*:autoScalingGroupName/${AWS::StackName}*' 343 | Effect: Allow 344 | PolicyName: ASGLifeCycleAccessPolicy 345 | ManagedPolicyArns: 346 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/AmazonSSMManagedInstanceCore' 347 | - !Sub 'arn:${AWS::Partition}:iam::aws:policy/CloudWatchAgentServerPolicy' 348 | Path: / 349 | AssumeRolePolicyDocument: 350 | Statement: 351 | - Action: 352 | - 'sts:AssumeRole' 353 | Principal: 354 | Service: 355 | - !Sub 'ec2.${AWS::URLSuffix}' 356 | Effect: Allow 357 | Version: 2012-10-17 358 | 359 | BastionHostProfile: 360 | Type: 'AWS::IAM::InstanceProfile' 361 | Properties: 362 | Roles: 363 | - !Ref BastionHostRole 364 | Path: / 365 | 366 | EIP: 367 | Type: 'AWS::EC2::EIP' 368 | Properties: 369 | Domain: vpc 370 | 371 | BastionAutoScalingGroup: 372 | Type: 'AWS::AutoScaling::AutoScalingGroup' 373 | Properties: 374 | DesiredCapacity: '1' 375 | LaunchConfigurationName: !Ref BastionLaunchConfiguration 376 | LifecycleHookSpecificationList: 377 | - LifecycleTransition: 'autoscaling:EC2_INSTANCE_LAUNCHING' 378 | LifecycleHookName: instance-patching-reboot 379 | HeartbeatTimeout: 3600 380 | MaxSize: '1' 381 | MinSize: '1' 382 | Tags: 383 | - Key: Name 384 | Value: !Sub 385 | - '${AWS::StackName}-BastionHost-${CidrBlock}' 386 | - CidrBlock: {'Fn::ImportValue': !Sub '${ParentVPCStack}-CidrBlock'} 387 | PropagateAtLaunch: true 388 | NotificationConfigurations: 389 | - TopicARN: !Ref EC2SNSTopic 390 | NotificationTypes: 391 | - 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR' 392 | - 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' 393 | VPCZoneIdentifier: !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPublic'}] 394 | CreationPolicy: 395 | ResourceSignal: 396 | Count: 1 397 | Timeout: PT10M 398 | UpdatePolicy: 399 | AutoScalingRollingUpdate: 400 | PauseTime: PT10M 401 | SuspendProcesses: 402 | - HealthCheck 403 | - ReplaceUnhealthy 404 | - AZRebalance 405 | - AlarmNotification 406 | - ScheduledActions 407 | WaitOnResourceSignals: true 408 | 409 | BastionLaunchConfiguration: 410 | Type: 'AWS::AutoScaling::LaunchConfiguration' 411 | Metadata: 412 | 'AWS::CloudFormation::Authentication': 413 | S3AccessCreds: 414 | type: S3 415 | roleName: !Ref BastionHostRole 416 | buckets: 417 | - !Sub 'aws-quickstart-${AWS::Region}' 418 | 'AWS::CloudFormation::Init': 419 | config: 420 | files: 421 | /tmp/auditd.rules: 422 | mode: '000550' 423 | owner: root 424 | group: root 425 | content: | 426 | -a exit,always -F arch=b64 -S execve 427 | -a exit,always -F arch=b32 -S execve 428 | /tmp/auditing_configure.sh: 429 | source: !Sub 'https://aws-quickstart-${AWS::Region}.s3.${AWS::Region}.${AWS::URLSuffix}/quickstart-linux-bastion/scripts/auditing_configure.sh' 430 | mode: '000550' 431 | owner: root 432 | group: root 433 | authentication: S3AccessCreds 434 | /tmp/bastion_bootstrap.sh: 435 | source: !If 436 | - UseAlternativeInitialization 437 | - !Ref AltInitScript 438 | - !Sub 'https://aws-quickstart-${AWS::Region}.s3.${AWS::Region}.${AWS::URLSuffix}/quickstart-linux-bastion/scripts/bastion_bootstrap.sh' 439 | mode: '000550' 440 | owner: root 441 | group: root 442 | authentication: S3AccessCreds 443 | /home/ec2-user/.psqlrc: 444 | content: !Sub | 445 | \set PROMPT1 '%[%033[1;31m%]%M%[%033[0m%]:%> %[%033[1;33m%]%n%[%033[0m%]@%/%R%#%x ' 446 | \pset pager off 447 | \set COMP_KEYWORD_CASE upper 448 | \set VERBOSITY verbose 449 | \set HISTCONTROL ignorespace 450 | \set HISTFILE ~/.psql_history- :DBNAME 451 | \set HISTSIZE 5000 452 | \set version 'SELECT version();' 453 | \set extensions 'select * from pg_available_extensions;' 454 | mode: "000644" 455 | owner: "root" 456 | group: "root" 457 | commands: 458 | a-add_auditd_rules: 459 | cwd: '/tmp/' 460 | env: 461 | BASTION_OS: !FindInMap [LinuxAMINameMap, 'Amazon-Linux2-HVM', OS] 462 | command: "./auditing_configure.sh" 463 | b-bootstrap: 464 | cwd: '/tmp/' 465 | command: !Sub 466 | - "REGION=${AWS::Region} URL_SUFFIX=${AWS::URLSuffix} BANNER_REGION=${AWS::Region} ./bastion_bootstrap.sh --banner ${BannerUrl} --enable ${EnableBanner} --tcp-forwarding ${EnableTCPForwarding} --x11-forwarding ${EnableX11Forwarding}" 467 | - BannerUrl: !If 468 | - DefaultBanner 469 | - !Sub 's3://aws-quickstart-${AWS::Region}/quickstart-linux-bastion/scripts/banner_message.txt' 470 | - !Ref BastionBanner 471 | Properties: 472 | AssociatePublicIpAddress: 'true' 473 | PlacementTenancy: !Ref BastionTenancy 474 | KeyName: !Ref KeyPairName 475 | IamInstanceProfile: !Ref BastionHostProfile 476 | ImageId: !If 477 | - UseOSImageOverride 478 | - !Ref OSImageOverride 479 | - !FindInMap 480 | - AWSAMIRegionMap 481 | - !Ref 'AWS::Region' 482 | - !FindInMap 483 | - LinuxAMINameMap 484 | - 'Amazon-Linux2-HVM' 485 | - Code 486 | SecurityGroups: 487 | - !Ref BastionSecurityGroup 488 | InstanceType: !Ref BastionInstanceType 489 | UserData: 490 | Fn::Base64: 491 | !Sub | 492 | #!/bin/bash 493 | set -x 494 | export PATH=$PATH:/usr/local/bin 495 | 496 | MYINSTANCEID="$(wget -q -O - http://169.254.169.254/latest/meta-data/instance-id)" 497 | MYREGION="$(curl -s 169.254.169.254/latest/meta-data/placement/availability-zone | sed 's/.$//')" 498 | NAMEOFASG=$(aws ec2 describe-tags --region $MYREGION --filters {"Name=resource-id,Values=$MYINSTANCEID","Name=key,Values=aws:autoscaling:groupName"} --output=text | cut -f5) 499 | PATCHDONEFLAG=/root/patchingrebootwasdone.flg 500 | 501 | # Utility for checking if a restart is needed by any of the patching that was done 502 | yum update yum-utils 503 | 504 | #cfn signaling functions 505 | function cfn_fail 506 | { 507 | cfn-signal -e 1 --stack ${AWS::StackName} --region ${AWS::Region} --resource BastionAutoScalingGroup 508 | exit 1 509 | } 510 | 511 | function cfn_success 512 | { 513 | cfn-signal -e 0 --stack ${AWS::StackName} --region ${AWS::Region} --resource BastionAutoScalingGroup 514 | exit 0 515 | } 516 | 517 | if [ ! -f $PATCHDONEFLAG ]; then 518 | yum install git -y || apt-get install -y git || zypper -n install git 519 | amazon-linux-extras enable postgresql14 && yum clean metadata && sudo yum install -y postgresql-contrib postgresql libpq-devel 520 | until git clone https://github.com/aws-quickstart/quickstart-linux-utilities.git ; do echo "Retrying"; done 521 | 522 | cd /quickstart-linux-utilities; 523 | source quickstart-cfn-tools.source; 524 | 525 | qs_update-os || qs_err; 526 | qs_bootstrap_pip || qs_err " pip bootstrap failed "; 527 | qs_aws-cfn-bootstrap || qs_err " cfn bootstrap failed "; 528 | 529 | needs-restarting -r 530 | 531 | if [ $? -gt 0 ]; then 532 | # Resetting userdata semaphore so that userdata will process again on restart 533 | rm /var/lib/cloud/instances/*/sem/config_scripts_user 534 | touch $PATCHDONEFLAG 535 | reboot 536 | sleep 30 537 | fi 538 | else 539 | cd /quickstart-linux-utilities; 540 | source quickstart-cfn-tools.source; 541 | fi 542 | 543 | EIP_LIST="${EIP}" 544 | CLOUDWATCHGROUP=${BastionMainLogGroup} 545 | 546 | cfn-init -v --stack '${AWS::StackName}' --resource BastionLaunchConfiguration --region ${AWS::Region} || cfn_fail 547 | 548 | aws --region ${AWS::Region} autoscaling complete-lifecycle-action --lifecycle-action-result CONTINUE --instance-id $MYINSTANCEID --lifecycle-hook-name instance-patching-reboot --auto-scaling-group-name $NAMEOFASG 549 | 550 | [ $(qs_status) == 0 ] && cfn_success || cfn_fail 551 | 552 | CPUTooHighAlarm: 553 | Type: 'AWS::CloudWatch::Alarm' 554 | Properties: 555 | AlarmDescription: 'Average CPU utilization over last 10 minutes higher than 80%' 556 | Namespace: 'AWS/EC2' 557 | MetricName: CPUUtilization 558 | Statistic: Average 559 | Period: 600 560 | EvaluationPeriods: 1 561 | ComparisonOperator: GreaterThanThreshold 562 | Threshold: 80 563 | AlarmActions: 564 | - Ref: EC2SNSTopic 565 | Dimensions: 566 | - Name: AutoScalingGroupName 567 | Value: !Ref BastionAutoScalingGroup 568 | 569 | Outputs: 570 | TemplateID: 571 | Description: 'Template ID' 572 | Value: 'VPC-SSH-Bastion' 573 | 574 | BastionAutoScalingGroup: 575 | Description: Auto Scaling Group Reference ID 576 | Value: !Ref BastionAutoScalingGroup 577 | Export: 578 | Name: !Sub '${AWS::StackName}-BastionAutoScalingGroup' 579 | 580 | EIP: 581 | Description: The public IP address of the SSH bastion host/instance 582 | Value: !Ref EIP 583 | Export: 584 | Name: !Sub '${AWS::StackName}-EIP' 585 | 586 | SSHCommand: 587 | Description: SSH command line 588 | Value: !Join 589 | - '' 590 | - - 'ssh -i "' 591 | - !Ref KeyPairName 592 | - '.pem" ' 593 | - 'ec2-user@' 594 | - !Ref EIP 595 | 596 | CloudWatchLogs: 597 | Description: CloudWatch Logs GroupName. Your SSH logs will be stored here. 598 | Value: !Ref BastionMainLogGroup 599 | Export: 600 | Name: !Sub '${AWS::StackName}-CloudWatchLogs' 601 | 602 | BastionSecurityGroupID: 603 | Description: Use this Security Group to reference incoming traffic from the SSH bastion host/instance 604 | Value: !Ref BastionSecurityGroup 605 | Export: 606 | Name: !Sub '${AWS::StackName}-BastionSecurityGroupID' -------------------------------------------------------------------------------- /lambda/dbbootstrap.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-aurora-cloudformation-samples/ed40cc26e9809dc98a7d15eedc36bf41a3d24ff6/lambda/dbbootstrap.zip -------------------------------------------------------------------------------- /media/AWS-Aurora-Architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/aws-aurora-cloudformation-samples/ed40cc26e9809dc98a7d15eedc36bf41a3d24ff6/media/AWS-Aurora-Architecture.png --------------------------------------------------------------------------------