├── .editorconfig ├── .gitattributes ├── .github ├── auto-merge.yml ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ └── deployment.yml ├── .gitignore ├── .mvn └── wrapper │ ├── MavenWrapperDownloader.java │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── RELEASE.md ├── SECURITY.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main └── java │ └── dev │ └── stratospheric │ └── cdk │ ├── ApplicationEnvironment.java │ ├── DockerRepository.java │ ├── JumpHost.java │ ├── Network.java │ ├── PostgresDatabase.java │ ├── Service.java │ └── SpringBootApplicationStack.java └── test └── java └── dev └── stratospheric └── cdk └── ApplicationEnvironmentTest.java /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | tab_width = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.github/auto-merge.yml: -------------------------------------------------------------------------------- 1 | requiredLabels: 2 | - PR-merge 3 | 4 | updateBranch: true 5 | deleteBranchAfterMerge: true 6 | 7 | reportStatus: true 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | labels: 9 | - "dependabot" 10 | - "PR-merge" 11 | 12 | - package-ecosystem: "github-actions" 13 | directory: "/" 14 | schedule: 15 | interval: "daily" 16 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '29 7 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'java' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Setup Java JDK 41 | uses: actions/setup-java@v4 42 | with: 43 | distribution: 'temurin' 44 | java-version: 11 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v3 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | - run: | 57 | ./mvnw package 58 | 59 | - name: Perform CodeQL Analysis 60 | uses: github/codeql-action/analyze@v3 61 | -------------------------------------------------------------------------------- /.github/workflows/deployment.yml: -------------------------------------------------------------------------------- 1 | name: "Build & Release" 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | # Runs the CI build on all PRs and branches. 8 | # Publishes to Maven Central on pushes to the "release" branch. 9 | build: 10 | name: "Build & Release to Maven Central" 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: "Checkout sources" 15 | uses: actions/checkout@v4 16 | 17 | - name: "Setup Java" 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: 11 22 | server-id: ossrh 23 | server-username: OSSRH_USERNAME 24 | server-password: OSSRH_PASSWORD 25 | gpg-passphrase: GPG_PASSPHRASE 26 | gpg-private-key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} 27 | cache: 'maven' 28 | 29 | - name: "Run Maven build" 30 | run: ./mvnw -B package 31 | 32 | - name: "Publish to the Maven Central Repository" 33 | if: github.ref == 'refs/heads/release' 34 | run: | 35 | git config user.email "actions@github.com" 36 | git config user.name "GitHub Actions" 37 | 38 | ./mvnw -B release:prepare release:perform -Dusername=$GITHUB_ACTOR -Dpassword=$GITHUB_TOKEN -P release 39 | env: 40 | OSSRH_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 41 | OSSRH_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 42 | GPG_PASSPHRASE: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .project 3 | .settings/ 4 | .idea 5 | target 6 | *.iml 7 | 8 | .serverless 9 | 10 | venv 11 | 12 | revision.json 13 | 14 | update_error 15 | 16 | HELP.md 17 | .gradle 18 | build/ 19 | !gradle/wrapper/gradle-wrapper.jar 20 | !**/src/main/** 21 | !**/src/test/** 22 | /gradle/ 23 | **/cdk.out/ 24 | -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stratospheric-dev/cdk-constructs/a1807ae4d71ce77ca630360fa7034194e092f24f/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stratospheric CDK Constructs 2 | 3 | ![Build & Release](https://github.com/stratospheric-dev/cdk-constructs/workflows/Build%20&%20Release/badge.svg?branch=release) 4 | ![CodeQL](https://github.com/stratospheric-dev/cdk-constructs/workflows/CodeQL/badge.svg) 5 | 6 | [![CDK Constructs Maven Central](https://img.shields.io/maven-central/v/dev.stratospheric/cdk-constructs.svg?label=CDK%20Constructs%20Maven%20Central)](https://search.maven.org/search?q=g:%22dev.stratospheric%22%20AND%20a:%22cdk-constructs%22) 7 | 8 | A collection of Java CDK constructs that play well together to deploy an application and a database to Amazon ECS. 9 | 10 | The constructs have been built to deploy a real application into production and will be updated as this application evolves. 11 | 12 | These constructs are explained in further detail in [our book](https://stratospheric.dev). 13 | 14 | From version 0.1.0 onwards, this constructs library only supports the AWS CDK v2. For migrating your existing AWS CDK v1 setup, follow the [official AWS migration guide](https://docs.aws.amazon.com/cdk/v2/guide/migrating-v2.html). 15 | 16 | ## Installation 17 | 18 | Load the dependency from Maven Central by adding this to your `pom.xml`: 19 | 20 | ```xml 21 | 22 | 23 | dev.stratospheric 24 | cdk-constructs 25 | ${latestVersion} 26 | 27 | 28 | ``` 29 | 30 | ... our your `build.gradle`: 31 | 32 | ```groovy 33 | implementation('dev.stratospheric:cdk-constructs:${latestVersion}') 34 | ``` 35 | 36 | The `latestVersion` is: [![](https://img.shields.io/maven-central/v/dev.stratospheric/cdk-constructs.svg?label=)](https://search.maven.org/search?q=g:%22dev.stratospheric%22%20AND%20a:%22cdk-constructs%22) 37 | 38 | Use this version without the `v` prefix inside your `pom.xml` or `build.gradle`: `v0.1.0` -> `0.1.0`. 39 | 40 | To override the version of the AWS Java CDK library, use a `` inside your `pom.xml`: 41 | 42 | ```xml 43 | 44 | 45 | 46 | software.amazon.awscdk 47 | aws-cdk-lib 48 | 2.8.0 49 | 50 | 51 | 52 | ``` 53 | 54 | ## Construct Overview 55 | 56 | A short description of the constructs in this project. For a more details description have a look at the Javadocs. 57 | 58 | * **[DockerRepository](src/main/java/dev/stratospheric/cdk/DockerRepository.java)**: a stack that contains a single ECR Docker repository and grants push and pull permissions to all users of the given account. 59 | * **[Network](src/main/java/dev/stratospheric/cdk/Network.java)**: creates a VPC with public and isolated subnets and a loadbalancer. Exposes parameters in the parameter store to be consumed by other constructs so they can be placed into that network. 60 | * **[PostgresDatabase](src/main/java/dev/stratospheric/cdk/PostgresDatabase.java)**: creates a PostgreSQL database in the isolated subnets of a given network. Requires a running `Network` stack (or at least the parameters it would expose in the SSM parameter store). Exposes the database connection parameters in the parameter store. 61 | * **[JumpHost](src/main/java/dev/stratospheric/cdk/JumpHost.java)**: creates an EC2 instance in a `Network`'s public subnet that has access to the PostgreSQL instance in a `PostgreSQLDatabase` stack. This EC2 instance can act as a jump host (aka bastion host) to connect to the database from your local machine. 62 | * **[Service](src/main/java/dev/stratospheric/cdk/Service.java)**: creates an ECS service that deploys a given Docker image into the public subnets of a given `Network`. Allows configuration of things like health check and environment variables. 63 | * **[SpringBootApplicationStack](src/main/java/dev/stratospheric/cdk/Service.java)**: a stack that combines the [Network](src/main/java/dev/stratospheric/cdk/Network.java) and [Service](src/main/java/dev/stratospheric/cdk/Service.java) constructs, configured for easy deployment of a Spring Boot Docker image. 64 | 65 | 66 | ## Usage 67 | 68 | An example usage of the database construct might look like this: 69 | 70 | ```java 71 | public class DatabaseApp { 72 | 73 | public static void main(final String[] args) { 74 | App app = new App(); 75 | 76 | String environmentName = (String) app.getNode().tryGetContext("environmentName"); 77 | requireNonEmpty(environmentName, "context variable 'environmentName' must not be null"); 78 | 79 | String applicationName = (String) app.getNode().tryGetContext("applicationName"); 80 | requireNonEmpty(applicationName, "context variable 'applicationName' must not be null"); 81 | 82 | String accountId = (String) app.getNode().tryGetContext("accountId"); 83 | requireNonEmpty(accountId, "context variable 'accountId' must not be null"); 84 | 85 | String region = (String) app.getNode().tryGetContext("region"); 86 | requireNonEmpty(region, "context variable 'region' must not be null"); 87 | 88 | Environment awsEnvironment = makeEnv(accountId, region); 89 | 90 | ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment( 91 | applicationName, 92 | environmentName 93 | ); 94 | 95 | Stack networkStack = new Stack(app, "DatabaseStack", StackProps.builder() 96 | .stackName(environmentName + "-Database") 97 | .env(awsEnvironment) 98 | .build()); 99 | 100 | PostgresDatabase database = new PostgresDatabase( 101 | networkStack, 102 | "Database", 103 | awsEnvironment, 104 | applicationEnvironment, 105 | new PostgresDatabase.DatabaseProperties()); 106 | 107 | app.synth(); 108 | } 109 | 110 | static Environment makeEnv(String account, String region) { 111 | return Environment.builder() 112 | .account(account) 113 | .region(region) 114 | .build(); 115 | } 116 | 117 | } 118 | ``` 119 | 120 | **An instance of `ApplicationEnvironment` specifies which environment the construct should be deployed into**. You can have multiple instances of each construct running, each with a different application (i.e. the name of the service you want to deploy) and a different environment (i.e. "test", "staging", "prod" or similar). 121 | 122 | **Most constructs take a properties object in the constructor**. Use these to configure the constructs. They have sensible defaults. 123 | 124 | **Most constructs require certain parameters to be available in the SSM parameter store**. The `PostgresDatabase` construct, for example, needs the parameters exposed by the `Network` construct to be available in the SSM parameter store. Read the Javadocs to see which parameters are required. You can set those parameters manually, but it's easiest to have deployed a `Network` construct in the same environment beforehand. 125 | 126 | While it's totally possible to put all constructs into the same CDK app, **we recommend to put each construct into its own CDK app**. The reason for this is flexibility. You may want to deploy and destroy a jump host separately from the rest. Or you may want to move a database between two environments. In these cases, having everything in the same app makes you very inflexible. 127 | 128 | Also, a monolithic CDK app would require you to pass the parameters for ALL constructs, even if you only want to deploy or destroy a single one. 129 | 130 | ## Release Process 131 | 132 | Head over to the [release information document](RELEASE.md). 133 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Information 2 | 3 | ## Performing a Release 4 | 5 | To release a new version of `cdk-constructs`, please create a PR for the `release` branch. 6 | 7 | Once it's merged, we'll automatically deploy a new version to Maven Central. 8 | 9 | ## GPG Key Information 10 | 11 | - The current GPG key was created on Philip's MacBook Pro M1 laptop 12 | - Key ID: `081897A195B1450E` 13 | - Both the exported GPG key and the secret passphrase are stored as GitHub Secrets for this repository 14 | - Extract the GPG private key with `gpg --armor --export-secret-keys YOUR_ID` 15 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently, only the latest version will receive security updates. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | You can get in touch by writing an email to info@stratospheric.dev 10 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | dev.stratospheric 5 | cdk-constructs 6 | 0.1.16-SNAPSHOT 7 | jar 8 | 9 | CDK Constructs 10 | A collection of CDK constructs to deploy an application on AWS. 11 | https://github.com/stratospheric-dev/cdk-constructs 12 | 13 | 14 | 11 15 | 11 16 | UTF-8 17 | UTF-8 18 | 2.200.1 19 | 10.4.2 20 | 2.19.0 21 | 22 | 23 | 24 | Stratospheric 25 | https://stratospheric.dev 26 | 27 | 28 | 2020 29 | 30 | 31 | 32 | Tom Hombergs 33 | thombergs 34 | tom.hombergs@gmail.com 35 | 36 | 37 | Philip Riecks 38 | rieckpil 39 | mail@philipriecks.de 40 | 41 | 42 | Björn Wilmsmann 43 | BjoernKW 44 | bjoern@bjoernkw.com 45 | 46 | 47 | 48 | 49 | 50 | MIT 51 | https://opensource.org/licenses/MIT 52 | repo 53 | 54 | 55 | 56 | 57 | scm:git:git://github.com/stratospheric-dev/cdk-constructs.git 58 | scm:git:https://github.com/stratospheric-dev/cdk-constructs.git 59 | https://github.com/stratospheric-dev/cdk-constructs 60 | cdk-constructs-0.0.23 61 | 62 | 63 | 64 | 65 | ossrh 66 | https://oss.sonatype.org/content/repositories/snapshots 67 | 68 | 69 | ossrh 70 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 71 | 72 | 73 | 74 | 75 | 76 | release 77 | 78 | 79 | 80 | org.apache.maven.plugins 81 | maven-gpg-plugin 82 | 3.2.7 83 | 84 | 85 | sign-artifacts 86 | verify 87 | 88 | sign 89 | 90 | 91 | 92 | 93 | 94 | org.sonatype.plugins 95 | nexus-staging-maven-plugin 96 | 1.7.0 97 | true 98 | 99 | ossrh 100 | https://oss.sonatype.org/ 101 | true 102 | 10 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | software.amazon.awscdk 113 | aws-cdk-lib 114 | ${aws-cdk-lib.version} 115 | 116 | 117 | software.constructs 118 | constructs 119 | ${constructs.version} 120 | 121 | 122 | org.junit.jupiter 123 | junit-jupiter 124 | 5.13.1 125 | test 126 | 127 | 128 | org.assertj 129 | assertj-core 130 | 3.27.3 131 | test 132 | 133 | 134 | 135 | 136 | 137 | 138 | com.fasterxml.jackson.core 139 | jackson-core 140 | ${jackson.version} 141 | 142 | 143 | com.fasterxml.jackson.core 144 | jackson-databind 145 | ${jackson.version} 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | org.apache.maven.archetype 154 | archetype-packaging 155 | 3.4.0 156 | 157 | 158 | 159 | 160 | org.apache.maven.plugins 161 | maven-release-plugin 162 | 3.1.1 163 | 164 | 165 | org.apache.maven.plugins 166 | maven-source-plugin 167 | 3.3.1 168 | 169 | 170 | attach-sources 171 | 172 | jar-no-fork 173 | 174 | 175 | 176 | 177 | 178 | org.apache.maven.plugins 179 | maven-javadoc-plugin 180 | 3.11.2 181 | 182 | none 183 | 184 | 185 | 186 | attach-javadocs 187 | 188 | jar 189 | 190 | 191 | 192 | 193 | 194 | org.apache.maven.plugins 195 | maven-resources-plugin 196 | 3.3.1 197 | 198 | false 199 | 200 | 201 | 202 | 203 | 204 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/ApplicationEnvironment.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import software.amazon.awscdk.Tags; 4 | import software.constructs.IConstruct; 5 | 6 | /** 7 | * An application can be deployed into multiple environments (staging, production, ...). 8 | * An {@link ApplicationEnvironment} object serves as a descriptor in which environment 9 | * an application is deployed. 10 | *

11 | * The constructs in this package will use this descriptor when naming AWS resources so that they 12 | * can be deployed into multiple environments at the same time without conflicts. 13 | */ 14 | public class ApplicationEnvironment { 15 | 16 | private final String applicationName; 17 | private final String environmentName; 18 | 19 | /** 20 | * Constructor. 21 | * 22 | * @param applicationName the name of the application that you want to deploy. 23 | * @param environmentName the name of the environment the application shall be deployed into. 24 | */ 25 | public ApplicationEnvironment(String applicationName, String environmentName) { 26 | this.applicationName = applicationName; 27 | this.environmentName = environmentName; 28 | } 29 | 30 | public String getApplicationName() { 31 | return applicationName; 32 | } 33 | 34 | public String getEnvironmentName() { 35 | return environmentName; 36 | } 37 | 38 | /** 39 | * Strips non-alphanumeric characters from a String since some AWS resources don't cope with 40 | * them when using them in resource names. 41 | */ 42 | private String sanitize(String environmentName) { 43 | return environmentName.replaceAll("[^a-zA-Z0-9-]", ""); 44 | } 45 | 46 | @Override 47 | public String toString() { 48 | return sanitize(environmentName + "-" + applicationName); 49 | } 50 | 51 | /** 52 | * Prefixes a string with the application name and environment name. 53 | */ 54 | public String prefix(String string) { 55 | return this + "-" + string; 56 | } 57 | 58 | /** 59 | * Prefixes a string with the application name and environment name. Returns only the last characterLimit 60 | * characters from the name. 61 | */ 62 | public String prefix(String string, int characterLimit) { 63 | String name = this + "-" + string; 64 | if (name.length() <= characterLimit) { 65 | return name; 66 | } 67 | return name.substring(name.length() - characterLimit); 68 | } 69 | 70 | public void tag(IConstruct construct) { 71 | Tags.of(construct).add("environment", environmentName); 72 | Tags.of(construct).add("application", applicationName); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/DockerRepository.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.Collections; 4 | import java.util.Objects; 5 | 6 | import software.amazon.awscdk.Environment; 7 | import software.amazon.awscdk.RemovalPolicy; 8 | import software.amazon.awscdk.services.ecr.IRepository; 9 | import software.amazon.awscdk.services.ecr.LifecycleRule; 10 | import software.amazon.awscdk.services.ecr.Repository; 11 | import software.amazon.awscdk.services.iam.AccountPrincipal; 12 | import software.constructs.Construct; 13 | 14 | /** 15 | * Provisions an ECR repository for Docker images. Every user in the given account will have access 16 | * to push and pull images. 17 | */ 18 | public class DockerRepository extends Construct { 19 | 20 | private final IRepository ecrRepository; 21 | 22 | public DockerRepository( 23 | final Construct scope, 24 | final String id, 25 | final Environment awsEnvironment, 26 | final DockerRepositoryInputParameters dockerRepositoryInputParameters) { 27 | super(scope, id); 28 | 29 | this.ecrRepository = Repository.Builder.create(this, "ecrRepository") 30 | .repositoryName(dockerRepositoryInputParameters.dockerRepositoryName) 31 | .removalPolicy(dockerRepositoryInputParameters.retainRegistryOnDelete ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY) 32 | .lifecycleRules(Collections.singletonList(LifecycleRule.builder() 33 | .rulePriority(1) 34 | .description("limit to " + dockerRepositoryInputParameters.maxImageCount + " images") 35 | .maxImageCount(dockerRepositoryInputParameters.maxImageCount) 36 | .build())) 37 | .build(); 38 | 39 | // grant pull and push to all users of the account 40 | ecrRepository.grantPullPush(new AccountPrincipal(dockerRepositoryInputParameters.accountId)); 41 | } 42 | 43 | public IRepository getEcrRepository() { 44 | return ecrRepository; 45 | } 46 | 47 | public static class DockerRepositoryInputParameters { 48 | private final String dockerRepositoryName; 49 | private final String accountId; 50 | private final int maxImageCount; 51 | private final boolean retainRegistryOnDelete; 52 | 53 | /** 54 | * @param dockerRepositoryName the name of the docker repository to create. 55 | * @param accountId ID of the AWS account which shall have permission to push and pull the Docker repository. 56 | */ 57 | public DockerRepositoryInputParameters(String dockerRepositoryName, String accountId) { 58 | this.dockerRepositoryName = dockerRepositoryName; 59 | this.accountId = accountId; 60 | this.maxImageCount = 10; 61 | this.retainRegistryOnDelete = true; 62 | } 63 | 64 | /** 65 | * @param dockerRepositoryName the name of the docker repository to create. 66 | * @param accountId ID of the AWS account which shall have permission to push and pull the Docker repository. 67 | * @param maxImageCount the maximum number of images to be held in the repository before old images get deleted. 68 | */ 69 | public DockerRepositoryInputParameters(String dockerRepositoryName, String accountId, int maxImageCount) { 70 | Objects.requireNonNull(accountId, "accountId must not be null"); 71 | Objects.requireNonNull(dockerRepositoryName, "dockerRepositoryName must not be null"); 72 | this.accountId = accountId; 73 | this.maxImageCount = maxImageCount; 74 | this.dockerRepositoryName = dockerRepositoryName; 75 | this.retainRegistryOnDelete = true; 76 | } 77 | 78 | /** 79 | * @param dockerRepositoryName the name of the docker repository to create. 80 | * @param accountId ID of the AWS account which shall have permission to push and pull the Docker repository. 81 | * @param maxImageCount the maximum number of images to be held in the repository before old images get deleted. 82 | * @param retainRegistryOnDelete indicating whether or not the container registry should be destroyed or retained on deletion. 83 | */ 84 | public DockerRepositoryInputParameters(String dockerRepositoryName, String accountId, int maxImageCount, boolean retainRegistryOnDelete) { 85 | Objects.requireNonNull(accountId, "accountId must not be null"); 86 | Objects.requireNonNull(dockerRepositoryName, "dockerRepositoryName must not be null"); 87 | this.accountId = accountId; 88 | this.maxImageCount = maxImageCount; 89 | this.dockerRepositoryName = dockerRepositoryName; 90 | this.retainRegistryOnDelete = retainRegistryOnDelete; 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/JumpHost.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.Objects; 4 | 5 | import software.amazon.awscdk.CfnOutput; 6 | import software.amazon.awscdk.Environment; 7 | import software.amazon.awscdk.services.ec2.CfnInstance; 8 | import software.amazon.awscdk.services.ec2.CfnSecurityGroup; 9 | import software.amazon.awscdk.services.ec2.CfnSecurityGroupIngress; 10 | import software.constructs.Construct; 11 | 12 | import static java.util.Collections.singletonList; 13 | 14 | /** 15 | * A stack that deploys an EC2 instance to use as a jump host (or "bastion host") to create an SSH tunnel to the RDS instance. 16 | * The jump host is placed into the public subnets of a given VPC and it will have access the database's security group. 17 | *

18 | * The following parameters need to exist in the SSM parameter store for this stack to successfully deploy: 19 | *

25 | *

26 | * The stack creates its public IP address as output, so you can look up its IP address in the CloudFormation AWS console 27 | * when you need it to access it via SSH. 28 | */ 29 | public class JumpHost extends Construct { 30 | 31 | private final ApplicationEnvironment applicationEnvironment; 32 | 33 | public JumpHost( 34 | final Construct scope, 35 | final String id, 36 | final Environment environment, 37 | final ApplicationEnvironment applicationEnvironment, 38 | final JumpHostInputParameters jumpHostInputParameters, 39 | final PostgresDatabase.DatabaseOutputParameters databaseOutputParameters) { 40 | 41 | super(scope, id); 42 | 43 | this.applicationEnvironment = applicationEnvironment; 44 | 45 | Network.NetworkOutputParameters networkOutputParameters = Network.getOutputParametersFromParameterStore(this, applicationEnvironment.getEnvironmentName()); 46 | 47 | CfnSecurityGroup jumpHostSecurityGroup = CfnSecurityGroup.Builder.create(this, "securityGroup") 48 | .groupName(applicationEnvironment.prefix("jumpHostSecurityGroup")) 49 | .groupDescription("SecurityGroup containing the jump host") 50 | .vpcId(networkOutputParameters.getVpcId()) 51 | .build(); 52 | 53 | String databaseSecurityGroupId = databaseOutputParameters.getDatabaseSecurityGroupId(); 54 | 55 | allowAccessToJumpHost(jumpHostSecurityGroup); 56 | allowAccessToDatabase(jumpHostSecurityGroup, databaseSecurityGroupId); 57 | 58 | CfnInstance instance = createEc2Instance( 59 | jumpHostInputParameters.keyName, 60 | jumpHostSecurityGroup, 61 | networkOutputParameters); 62 | 63 | CfnOutput publicIpOutput = CfnOutput.Builder.create(this, "publicIp") 64 | .value(instance.getAttrPublicIp()) 65 | .build(); 66 | 67 | applicationEnvironment.tag(this); 68 | 69 | } 70 | 71 | private CfnInstance createEc2Instance( 72 | String keyName, 73 | CfnSecurityGroup jumpHostSecurityGroup, 74 | Network.NetworkOutputParameters networkOutputParameters) { 75 | 76 | return CfnInstance.Builder.create(this, "jumpHostInstance") 77 | .instanceType("t2.nano") 78 | .securityGroupIds(singletonList(jumpHostSecurityGroup.getAttrGroupId())) 79 | .imageId("ami-0f96495a064477ffb") 80 | .subnetId(networkOutputParameters.getPublicSubnets().get(0)) 81 | .keyName(keyName) 82 | .build(); 83 | 84 | } 85 | 86 | private void allowAccessToDatabase(CfnSecurityGroup fromSecurityGroup, String toSecurityGroupId) { 87 | CfnSecurityGroupIngress dbSecurityGroupIngress = CfnSecurityGroupIngress.Builder.create(this, "IngressFromJumpHost") 88 | .sourceSecurityGroupId(fromSecurityGroup.getAttrGroupId()) 89 | .groupId(toSecurityGroupId) 90 | .fromPort(5432) 91 | .toPort(5432) 92 | .ipProtocol("TCP") 93 | .build(); 94 | } 95 | 96 | private void allowAccessToJumpHost(CfnSecurityGroup jumpHostSecurityGroup) { 97 | CfnSecurityGroupIngress jumpHostSecurityGroupIngress = CfnSecurityGroupIngress.Builder.create(this, "IngressFromOutside") 98 | .groupId(jumpHostSecurityGroup.getAttrGroupId()) 99 | .fromPort(22) 100 | .toPort(22) 101 | .ipProtocol("TCP") 102 | .cidrIp("0.0.0.0/0") 103 | .build(); 104 | } 105 | 106 | public static class JumpHostInputParameters { 107 | private final String keyName; 108 | 109 | /** 110 | * @param keyName the name of the key pair that will be installed in the jump host EC2 instance so you can 111 | * access it via SSH. This key pair must be created via the EC2 console beforehand. 112 | */ 113 | public JumpHostInputParameters(String keyName) { 114 | Objects.requireNonNull(keyName, "parameter 'keyName' cannot be null"); 115 | this.keyName = keyName; 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/Network.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.Collections; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | import java.util.stream.Collectors; 8 | 9 | import org.jetbrains.annotations.NotNull; 10 | import org.jetbrains.annotations.Nullable; 11 | import software.amazon.awscdk.Duration; 12 | import software.amazon.awscdk.Environment; 13 | import software.amazon.awscdk.Tags; 14 | import software.amazon.awscdk.services.ec2.CfnSecurityGroupIngress; 15 | import software.amazon.awscdk.services.ec2.ISecurityGroup; 16 | import software.amazon.awscdk.services.ec2.ISubnet; 17 | import software.amazon.awscdk.services.ec2.IVpc; 18 | import software.amazon.awscdk.services.ec2.SecurityGroup; 19 | import software.amazon.awscdk.services.ec2.SubnetConfiguration; 20 | import software.amazon.awscdk.services.ec2.SubnetType; 21 | import software.amazon.awscdk.services.ec2.Vpc; 22 | import software.amazon.awscdk.services.ecs.Cluster; 23 | import software.amazon.awscdk.services.ecs.ICluster; 24 | import software.amazon.awscdk.services.elasticloadbalancingv2.*; 25 | import software.amazon.awscdk.services.ssm.StringParameter; 26 | import software.constructs.Construct; 27 | 28 | import static java.util.Arrays.asList; 29 | 30 | /** 31 | * Creates a base network for an application served by ECS. The network stack contains a VPC, 32 | * two public and two isolated subnets, an ECS cluster, and an internet-facing load balancer with an HTTP and 33 | * an optional HTTPS listener. The listeners can be used in other stacks to attach to an ECS service, 34 | * for instance. 35 | *

36 | * The construct exposes some output parameters to be used by other constructs. You can access them by: 37 | *

42 | */ 43 | public class Network extends Construct { 44 | 45 | private static final String PARAMETER_VPC_ID = "vpcId"; 46 | private static final String PARAMETER_HTTP_LISTENER = "httpListenerArn"; 47 | private static final String PARAMETER_HTTPS_LISTENER = "httpsListenerArn"; 48 | private static final String PARAMETER_LOADBALANCER_SECURITY_GROUP_ID = "loadBalancerSecurityGroupId"; 49 | private static final String PARAMETER_ECS_CLUSTER_NAME = "ecsClusterName"; 50 | private static final String PARAMETER_ISOLATED_SUBNET_ONE = "isolatedSubnetIdOne"; 51 | private static final String PARAMETER_ISOLATED_SUBNET_TWO = "isolatedSubnetIdTwo"; 52 | private static final String PARAMETER_PUBLIC_SUBNET_ONE = "publicSubnetIdOne"; 53 | private static final String PARAMETER_PUBLIC_SUBNET_TWO = "publicSubnetIdTwo"; 54 | private static final String PARAMETER_AVAILABILITY_ZONE_ONE = "availabilityZoneOne"; 55 | private static final String PARAMETER_AVAILABILITY_ZONE_TWO = "availabilityZoneTwo"; 56 | private static final String PARAMETER_LOAD_BALANCER_ARN = "loadBalancerArn"; 57 | private static final String PARAMETER_LOAD_BALANCER_DNS_NAME = "loadBalancerDnsName"; 58 | private static final String PARAMETER_LOAD_BALANCER_HOSTED_ZONE_ID = "loadBalancerCanonicalHostedZoneId"; 59 | private final IVpc vpc; 60 | private final String environmentName; 61 | private final ICluster ecsCluster; 62 | private IApplicationListener httpListener; 63 | private IApplicationListener httpsListener; 64 | private ISecurityGroup loadbalancerSecurityGroup; 65 | private IApplicationLoadBalancer loadBalancer; 66 | 67 | public Network( 68 | final Construct scope, 69 | final String id, 70 | final Environment environment, 71 | final String environmentName, 72 | final NetworkInputParameters networkInputParameters) { 73 | 74 | super(scope, id); 75 | 76 | this.environmentName = environmentName; 77 | 78 | this.vpc = createVpc(environmentName); 79 | 80 | // We're preparing an ECS cluster in the network stack and using it in the ECS stack. 81 | // If the cluster were in the ECS stack, it would interfere with deleting the ECS stack, 82 | // because an ECS service would still depend on it. 83 | this.ecsCluster = Cluster.Builder.create(this, "cluster") 84 | .vpc(this.vpc) 85 | .clusterName(prefixWithEnvironmentName("ecsCluster")) 86 | .build(); 87 | 88 | createLoadBalancer(vpc, networkInputParameters.getSslCertificateArn()); 89 | 90 | Tags.of(this).add("environment", environmentName); 91 | } 92 | 93 | /** 94 | * Collects the output parameters of an already deployed {@link Network} construct from the parameter store. This requires 95 | * that a {@link Network} construct has been deployed previously. If you want to access the parameters from the same 96 | * stack that the {@link Network} construct is in, use the plain {@link #getOutputParameters()} method. 97 | * 98 | * @param scope the construct in which we need the output parameters 99 | * @param environmentName the name of the environment for which to load the output parameters. The deployed {@link Network} 100 | * construct must have been deployed into this environment. 101 | */ 102 | public static NetworkOutputParameters getOutputParametersFromParameterStore(Construct scope, String environmentName) { 103 | return new NetworkOutputParameters( 104 | getVpcIdFromParameterStore(scope, environmentName), 105 | getHttpListenerArnFromParameterStore(scope, environmentName), 106 | getHttpsListenerArnFromParameterStore(scope, environmentName), 107 | getLoadbalancerSecurityGroupIdFromParameterStore(scope, environmentName), 108 | getEcsClusterNameFromParameterStore(scope, environmentName), 109 | getIsolatedSubnetsFromParameterStore(scope, environmentName), 110 | getPublicSubnetsFromParameterStore(scope, environmentName), 111 | getAvailabilityZonesFromParameterStore(scope, environmentName), 112 | getLoadBalancerArnFromParameterStore(scope, environmentName), 113 | getLoadBalancerDnsNameFromParameterStore(scope,environmentName), 114 | getLoadBalancerCanonicalHostedZoneIdFromParameterStore(scope,environmentName) 115 | ); 116 | } 117 | 118 | @NotNull 119 | private static String createParameterName(String environmentName, String parameterName) { 120 | return environmentName + "-Network-" + parameterName; 121 | } 122 | 123 | private static String getVpcIdFromParameterStore(Construct scope, String environmentName) { 124 | return StringParameter.fromStringParameterName(scope, PARAMETER_VPC_ID, createParameterName(environmentName, PARAMETER_VPC_ID)) 125 | .getStringValue(); 126 | } 127 | 128 | private static String getHttpListenerArnFromParameterStore(Construct scope, String environmentName) { 129 | return StringParameter.fromStringParameterName(scope, PARAMETER_HTTP_LISTENER, createParameterName(environmentName, PARAMETER_HTTP_LISTENER)) 130 | .getStringValue(); 131 | } 132 | 133 | private static Optional getHttpsListenerArnFromParameterStore(Construct scope, String environmentName) { 134 | String value = StringParameter.fromStringParameterName(scope, PARAMETER_HTTPS_LISTENER, createParameterName(environmentName, PARAMETER_HTTPS_LISTENER)) 135 | .getStringValue(); 136 | if ("null".equals(value)) { 137 | return Optional.empty(); 138 | } else { 139 | return Optional.ofNullable(value); 140 | } 141 | } 142 | 143 | private static String getLoadbalancerSecurityGroupIdFromParameterStore(Construct scope, String environmentName) { 144 | return StringParameter.fromStringParameterName(scope, PARAMETER_LOADBALANCER_SECURITY_GROUP_ID, createParameterName(environmentName, PARAMETER_LOADBALANCER_SECURITY_GROUP_ID)) 145 | .getStringValue(); 146 | } 147 | 148 | private static String getEcsClusterNameFromParameterStore(Construct scope, String environmentName) { 149 | return StringParameter.fromStringParameterName(scope, PARAMETER_ECS_CLUSTER_NAME, createParameterName(environmentName, PARAMETER_ECS_CLUSTER_NAME)) 150 | .getStringValue(); 151 | } 152 | 153 | private static List getIsolatedSubnetsFromParameterStore(Construct scope, String environmentName) { 154 | 155 | String subnetOneId = StringParameter.fromStringParameterName(scope, PARAMETER_ISOLATED_SUBNET_ONE, createParameterName(environmentName, PARAMETER_ISOLATED_SUBNET_ONE)) 156 | .getStringValue(); 157 | 158 | String subnetTwoId = StringParameter.fromStringParameterName(scope, PARAMETER_ISOLATED_SUBNET_TWO, createParameterName(environmentName, PARAMETER_ISOLATED_SUBNET_TWO)) 159 | .getStringValue(); 160 | 161 | return asList(subnetOneId, subnetTwoId); 162 | } 163 | 164 | private static List getPublicSubnetsFromParameterStore(Construct scope, String environmentName) { 165 | 166 | String subnetOneId = StringParameter.fromStringParameterName(scope, PARAMETER_PUBLIC_SUBNET_ONE, createParameterName(environmentName, PARAMETER_PUBLIC_SUBNET_ONE)) 167 | .getStringValue(); 168 | 169 | String subnetTwoId = StringParameter.fromStringParameterName(scope, PARAMETER_PUBLIC_SUBNET_TWO, createParameterName(environmentName, PARAMETER_PUBLIC_SUBNET_TWO)) 170 | .getStringValue(); 171 | 172 | return asList(subnetOneId, subnetTwoId); 173 | } 174 | 175 | private static List getAvailabilityZonesFromParameterStore(Construct scope, String environmentName) { 176 | 177 | String availabilityZoneOne = StringParameter.fromStringParameterName(scope, PARAMETER_AVAILABILITY_ZONE_ONE, createParameterName(environmentName, PARAMETER_AVAILABILITY_ZONE_ONE)) 178 | .getStringValue(); 179 | 180 | String availabilityZoneTwo = StringParameter.fromStringParameterName(scope, PARAMETER_AVAILABILITY_ZONE_TWO, createParameterName(environmentName, PARAMETER_AVAILABILITY_ZONE_TWO)) 181 | .getStringValue(); 182 | 183 | return asList(availabilityZoneOne, availabilityZoneTwo); 184 | } 185 | 186 | private static String getLoadBalancerArnFromParameterStore(Construct scope, String environmentName) { 187 | return StringParameter.fromStringParameterName(scope, PARAMETER_LOAD_BALANCER_ARN, createParameterName(environmentName, PARAMETER_LOAD_BALANCER_ARN)) 188 | .getStringValue(); 189 | } 190 | 191 | private static String getLoadBalancerDnsNameFromParameterStore(Construct scope, String environmentName) { 192 | return StringParameter.fromStringParameterName(scope, PARAMETER_LOAD_BALANCER_DNS_NAME, createParameterName(environmentName, PARAMETER_LOAD_BALANCER_DNS_NAME)) 193 | .getStringValue(); 194 | } 195 | 196 | private static String getLoadBalancerCanonicalHostedZoneIdFromParameterStore(Construct scope, String environmentName) { 197 | return StringParameter.fromStringParameterName(scope, PARAMETER_LOAD_BALANCER_HOSTED_ZONE_ID, createParameterName(environmentName, PARAMETER_LOAD_BALANCER_HOSTED_ZONE_ID)) 198 | .getStringValue(); 199 | } 200 | 201 | public IVpc getVpc() { 202 | return vpc; 203 | } 204 | 205 | public IApplicationListener getHttpListener() { 206 | return httpListener; 207 | } 208 | 209 | /** 210 | * The load balancer's HTTPS listener. May be null if the load balancer is configured for HTTP only! 211 | */ 212 | @Nullable 213 | public IApplicationListener getHttpsListener() { 214 | return httpsListener; 215 | } 216 | 217 | public ISecurityGroup getLoadbalancerSecurityGroup() { 218 | return loadbalancerSecurityGroup; 219 | } 220 | 221 | public IApplicationLoadBalancer getLoadBalancer() { 222 | return loadBalancer; 223 | } 224 | 225 | public ICluster getEcsCluster() { 226 | return ecsCluster; 227 | } 228 | 229 | /** 230 | * Creates a VPC with 2 private and 2 public subnets in different AZs and without a NAT gateway 231 | * (i.e. the private subnets have no access to the internet). 232 | */ 233 | private IVpc createVpc(final String environmentName) { 234 | 235 | SubnetConfiguration publicSubnets = SubnetConfiguration.builder() 236 | .subnetType(SubnetType.PUBLIC) 237 | .name(prefixWithEnvironmentName("publicSubnet")) 238 | .build(); 239 | 240 | SubnetConfiguration isolatedSubnets = SubnetConfiguration.builder() 241 | .subnetType(SubnetType.PRIVATE_ISOLATED) 242 | .name(prefixWithEnvironmentName("isolatedSubnet")) 243 | .build(); 244 | 245 | return Vpc.Builder.create(this, "vpc") 246 | .natGateways(0) 247 | .maxAzs(2) 248 | .subnetConfiguration(asList( 249 | publicSubnets, 250 | isolatedSubnets 251 | )) 252 | .build(); 253 | } 254 | 255 | private String prefixWithEnvironmentName(String string) { 256 | return this.environmentName + "-" + string; 257 | } 258 | 259 | /** 260 | * Creates a load balancer that accepts HTTP and HTTPS requests from the internet and puts it into 261 | * the VPC's public subnets. 262 | */ 263 | private void createLoadBalancer( 264 | final IVpc vpc, 265 | final Optional sslCertificateArn) { 266 | 267 | loadbalancerSecurityGroup = SecurityGroup.Builder.create(this, "loadbalancerSecurityGroup") 268 | .securityGroupName(prefixWithEnvironmentName("loadbalancerSecurityGroup")) 269 | .description("Public access to the load balancer.") 270 | .vpc(vpc) 271 | .build(); 272 | 273 | CfnSecurityGroupIngress ingressFromPublic = CfnSecurityGroupIngress.Builder.create(this, "ingressToLoadbalancer") 274 | .groupId(loadbalancerSecurityGroup.getSecurityGroupId()) 275 | .cidrIp("0.0.0.0/0") 276 | .ipProtocol("-1") 277 | .build(); 278 | 279 | loadBalancer = ApplicationLoadBalancer.Builder.create(this, "loadbalancer") 280 | .loadBalancerName(prefixWithEnvironmentName("loadbalancer")) 281 | .vpc(vpc) 282 | .internetFacing(true) 283 | .securityGroup(loadbalancerSecurityGroup) 284 | .build(); 285 | 286 | IApplicationTargetGroup dummyTargetGroup = ApplicationTargetGroup.Builder.create(this, "defaultTargetGroup") 287 | .vpc(vpc) 288 | .port(8080) 289 | .protocol(ApplicationProtocol.HTTP) 290 | .targetGroupName(prefixWithEnvironmentName("no-op-targetGroup")) 291 | .targetType(TargetType.IP) 292 | .deregistrationDelay(Duration.seconds(5)) 293 | .healthCheck( 294 | HealthCheck 295 | .builder() 296 | .healthyThresholdCount(2) 297 | .interval(Duration.seconds(10)) 298 | .timeout(Duration.seconds(5)) 299 | .build() 300 | ) 301 | .build(); 302 | 303 | httpListener = loadBalancer.addListener("httpListener", BaseApplicationListenerProps.builder() 304 | .port(80) 305 | .protocol(ApplicationProtocol.HTTP) 306 | .open(true) 307 | .build()); 308 | 309 | httpListener.addTargetGroups("http-defaultTargetGroup", AddApplicationTargetGroupsProps.builder() 310 | .targetGroups(Collections.singletonList(dummyTargetGroup)) 311 | .build()); 312 | 313 | if (sslCertificateArn.isPresent()) { 314 | IListenerCertificate certificate = ListenerCertificate.fromArn(sslCertificateArn.get()); 315 | httpsListener = loadBalancer.addListener("httpsListener", BaseApplicationListenerProps.builder() 316 | .port(443) 317 | .protocol(ApplicationProtocol.HTTPS) 318 | .certificates(Collections.singletonList(certificate)) 319 | .open(true) 320 | .build()); 321 | 322 | httpsListener.addTargetGroups("https-defaultTargetGroup", AddApplicationTargetGroupsProps.builder() 323 | .targetGroups(Collections.singletonList(dummyTargetGroup)) 324 | .build()); 325 | 326 | ListenerAction redirectAction = ListenerAction.redirect( 327 | RedirectOptions.builder() 328 | .protocol("HTTPS") 329 | .port("443") 330 | .build() 331 | ); 332 | ApplicationListenerRule applicationListenerRule = new ApplicationListenerRule( 333 | this, 334 | "HttpListenerRule", 335 | ApplicationListenerRuleProps.builder() 336 | .listener(httpListener) 337 | .priority(1) 338 | .conditions(List.of(ListenerCondition.pathPatterns(List.of("*")))) 339 | .action(redirectAction) 340 | .build() 341 | ); 342 | } 343 | 344 | createOutputParameters(); 345 | } 346 | 347 | /** 348 | * Stores output parameters of this stack in the parameter store so they can be retrieved by other stacks 349 | * or constructs as necessary. 350 | */ 351 | private void createOutputParameters() { 352 | 353 | StringParameter vpcId = StringParameter.Builder.create(this, "vpcId") 354 | .parameterName(createParameterName(environmentName, PARAMETER_VPC_ID)) 355 | .stringValue(this.vpc.getVpcId()) 356 | .build(); 357 | 358 | StringParameter httpListener = StringParameter.Builder.create(this, "httpListener") 359 | .parameterName(createParameterName(environmentName, PARAMETER_HTTP_LISTENER)) 360 | .stringValue(this.httpListener.getListenerArn()) 361 | .build(); 362 | 363 | if (this.httpsListener != null) { 364 | StringParameter httpsListener = StringParameter.Builder.create(this, "httpsListener") 365 | .parameterName(createParameterName(environmentName, PARAMETER_HTTPS_LISTENER)) 366 | .stringValue(this.httpsListener.getListenerArn()) 367 | .build(); 368 | } else { 369 | StringParameter httpsListener = StringParameter.Builder.create(this, "httpsListener") 370 | .parameterName(createParameterName(environmentName, PARAMETER_HTTPS_LISTENER)) 371 | .stringValue("null") 372 | .build(); 373 | } 374 | 375 | StringParameter loadbalancerSecurityGroup = StringParameter.Builder.create(this, "loadBalancerSecurityGroupId") 376 | .parameterName(createParameterName(environmentName, PARAMETER_LOADBALANCER_SECURITY_GROUP_ID)) 377 | .stringValue(this.loadbalancerSecurityGroup.getSecurityGroupId()) 378 | .build(); 379 | 380 | StringParameter cluster = StringParameter.Builder.create(this, "ecsClusterName") 381 | .parameterName(createParameterName(environmentName, PARAMETER_ECS_CLUSTER_NAME)) 382 | .stringValue(this.ecsCluster.getClusterName()) 383 | .build(); 384 | 385 | // I would have liked to use StringListParameter to store a list of AZs, but it's currently broken (https://github.com/aws/aws-cdk/issues/3586). 386 | StringParameter availabilityZoneOne = StringParameter.Builder.create(this, "availabilityZoneOne") 387 | .parameterName(createParameterName(environmentName, PARAMETER_AVAILABILITY_ZONE_ONE)) 388 | .stringValue(vpc.getAvailabilityZones().get(0)) 389 | .build(); 390 | 391 | StringParameter availabilityZoneTwo = StringParameter.Builder.create(this, "availabilityZoneTwo") 392 | .parameterName(createParameterName(environmentName, PARAMETER_AVAILABILITY_ZONE_TWO)) 393 | .stringValue(vpc.getAvailabilityZones().get(1)) 394 | .build(); 395 | 396 | // I would have liked to use StringListParameter to store a list of AZs, but it's currently broken (https://github.com/aws/aws-cdk/issues/3586). 397 | StringParameter isolatedSubnetOne = StringParameter.Builder.create(this, "isolatedSubnetOne") 398 | .parameterName(createParameterName(environmentName, PARAMETER_ISOLATED_SUBNET_ONE)) 399 | .stringValue(this.vpc.getIsolatedSubnets().get(0).getSubnetId()) 400 | .build(); 401 | 402 | StringParameter isolatedSubnetTwo = StringParameter.Builder.create(this, "isolatedSubnetTwo") 403 | .parameterName(createParameterName(environmentName, PARAMETER_ISOLATED_SUBNET_TWO)) 404 | .stringValue(this.vpc.getIsolatedSubnets().get(1).getSubnetId()) 405 | .build(); 406 | 407 | // I would have liked to use StringListParameter to store a list of AZs, but it's currently broken (https://github.com/aws/aws-cdk/issues/3586). 408 | StringParameter publicSubnetOne = StringParameter.Builder.create(this, "publicSubnetOne") 409 | .parameterName(createParameterName(environmentName, PARAMETER_PUBLIC_SUBNET_ONE)) 410 | .stringValue(this.vpc.getPublicSubnets().get(0).getSubnetId()) 411 | .build(); 412 | 413 | StringParameter publicSubnetTwo = StringParameter.Builder.create(this, "publicSubnetTwo") 414 | .parameterName(createParameterName(environmentName, PARAMETER_PUBLIC_SUBNET_TWO)) 415 | .stringValue(this.vpc.getPublicSubnets().get(1).getSubnetId()) 416 | .build(); 417 | 418 | 419 | StringParameter loadBalancerArn = StringParameter.Builder.create(this, "loadBalancerArn") 420 | .parameterName(createParameterName(environmentName, PARAMETER_LOAD_BALANCER_ARN)) 421 | .stringValue(this.loadBalancer.getLoadBalancerArn()) 422 | .build(); 423 | 424 | StringParameter loadBalancerDnsName = StringParameter.Builder.create(this, "loadBalancerDnsName") 425 | .parameterName(createParameterName(environmentName, PARAMETER_LOAD_BALANCER_DNS_NAME)) 426 | .stringValue(this.loadBalancer.getLoadBalancerDnsName()) 427 | .build(); 428 | 429 | StringParameter loadBalancerCanonicalHostedZoneId = StringParameter.Builder.create(this, "loadBalancerCanonicalHostedZoneId") 430 | .parameterName(createParameterName(environmentName, PARAMETER_LOAD_BALANCER_HOSTED_ZONE_ID)) 431 | .stringValue(this.loadBalancer.getLoadBalancerCanonicalHostedZoneId()) 432 | .build(); 433 | } 434 | 435 | /** 436 | * Collects the output parameters of this construct that might be of interest to other constructs. 437 | */ 438 | public NetworkOutputParameters getOutputParameters() { 439 | return new NetworkOutputParameters( 440 | this.vpc.getVpcId(), 441 | this.httpListener.getListenerArn(), 442 | this.httpsListener != null ? Optional.of(this.httpsListener.getListenerArn()) : Optional.empty(), 443 | this.loadbalancerSecurityGroup.getSecurityGroupId(), 444 | this.ecsCluster.getClusterName(), 445 | this.vpc.getIsolatedSubnets().stream().map(ISubnet::getSubnetId).collect(Collectors.toList()), 446 | this.vpc.getPublicSubnets().stream().map(ISubnet::getSubnetId).collect(Collectors.toList()), 447 | this.vpc.getAvailabilityZones(), 448 | this.loadBalancer.getLoadBalancerArn(), 449 | this.loadBalancer.getLoadBalancerDnsName(), 450 | this.loadBalancer.getLoadBalancerCanonicalHostedZoneId() 451 | ); 452 | } 453 | 454 | public static class NetworkInputParameters { 455 | private Optional sslCertificateArn; 456 | 457 | /** 458 | * @param sslCertificateArn the ARN of the SSL certificate that the load balancer will use 459 | * to terminate HTTPS communication. If no SSL certificate is passed, 460 | * the load balancer will only listen to plain HTTP. 461 | * @deprecated use {@link #withSslCertificateArn(String)} instead 462 | */ 463 | @Deprecated 464 | public NetworkInputParameters(String sslCertificateArn) { 465 | this.sslCertificateArn = Optional.ofNullable(sslCertificateArn); 466 | } 467 | 468 | public NetworkInputParameters() { 469 | this.sslCertificateArn = Optional.empty(); 470 | } 471 | 472 | public NetworkInputParameters withSslCertificateArn(String sslCertificateArn){ 473 | Objects.requireNonNull(sslCertificateArn); 474 | this.sslCertificateArn = Optional.of(sslCertificateArn); 475 | return this; 476 | } 477 | 478 | public Optional getSslCertificateArn() { 479 | return sslCertificateArn; 480 | } 481 | } 482 | 483 | public static class NetworkOutputParameters { 484 | 485 | private final String vpcId; 486 | private final String httpListenerArn; 487 | private final Optional httpsListenerArn; 488 | private final String loadbalancerSecurityGroupId; 489 | private final String ecsClusterName; 490 | private final List isolatedSubnets; 491 | private final List publicSubnets; 492 | private final List availabilityZones; 493 | private final String loadBalancerArn; 494 | private final String loadBalancerDnsName; 495 | private final String loadBalancerCanonicalHostedZoneId; 496 | 497 | public NetworkOutputParameters( 498 | String vpcId, 499 | String httpListenerArn, 500 | Optional httpsListenerArn, 501 | String loadbalancerSecurityGroupId, 502 | String ecsClusterName, 503 | List isolatedSubnets, 504 | List publicSubnets, 505 | List availabilityZones, 506 | String loadBalancerArn, 507 | String loadBalancerDnsName, 508 | String loadBalancerCanonicalHostedZoneId 509 | ) { 510 | this.vpcId = vpcId; 511 | this.httpListenerArn = httpListenerArn; 512 | this.httpsListenerArn = httpsListenerArn; 513 | this.loadbalancerSecurityGroupId = loadbalancerSecurityGroupId; 514 | this.ecsClusterName = ecsClusterName; 515 | this.isolatedSubnets = isolatedSubnets; 516 | this.publicSubnets = publicSubnets; 517 | this.availabilityZones = availabilityZones; 518 | this.loadBalancerArn = loadBalancerArn; 519 | this.loadBalancerDnsName = loadBalancerDnsName; 520 | this.loadBalancerCanonicalHostedZoneId = loadBalancerCanonicalHostedZoneId; 521 | } 522 | 523 | /** 524 | * The VPC ID. 525 | */ 526 | public String getVpcId() { 527 | return this.vpcId; 528 | } 529 | 530 | /** 531 | * The ARN of the HTTP listener. 532 | */ 533 | public String getHttpListenerArn() { 534 | return this.httpListenerArn; 535 | } 536 | 537 | /** 538 | * The ARN of the HTTPS listener. 539 | */ 540 | public Optional getHttpsListenerArn() { 541 | return this.httpsListenerArn; 542 | } 543 | 544 | /** 545 | * The ID of the load balancer's security group. 546 | */ 547 | public String getLoadbalancerSecurityGroupId() { 548 | return this.loadbalancerSecurityGroupId; 549 | } 550 | 551 | /** 552 | * The name of the ECS cluster. 553 | */ 554 | public String getEcsClusterName() { 555 | return this.ecsClusterName; 556 | } 557 | 558 | /** 559 | * The IDs of the isolated subnets. 560 | */ 561 | public List getIsolatedSubnets() { 562 | return this.isolatedSubnets; 563 | } 564 | 565 | /** 566 | * The IDs of the public subnets. 567 | */ 568 | public List getPublicSubnets() { 569 | return this.publicSubnets; 570 | } 571 | 572 | /** 573 | * The names of the availability zones of the VPC. 574 | */ 575 | public List getAvailabilityZones() { 576 | return this.availabilityZones; 577 | } 578 | 579 | /** 580 | * The ARN of the load balancer. 581 | */ 582 | public String getLoadBalancerArn() { 583 | return this.loadBalancerArn; 584 | } 585 | 586 | /** 587 | * The DNS name of the load balancer. 588 | */ 589 | public String getLoadBalancerDnsName() { 590 | return loadBalancerDnsName; 591 | } 592 | 593 | /** 594 | * The hosted zone ID of the load balancer. 595 | */ 596 | public String getLoadBalancerCanonicalHostedZoneId() { 597 | return loadBalancerCanonicalHostedZoneId; 598 | } 599 | } 600 | } 601 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/PostgresDatabase.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.Collections; 4 | import java.util.Objects; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | import software.amazon.awscdk.Environment; 8 | import software.amazon.awscdk.services.ec2.CfnSecurityGroup; 9 | import software.amazon.awscdk.services.rds.CfnDBInstance; 10 | import software.amazon.awscdk.services.rds.CfnDBSubnetGroup; 11 | import software.amazon.awscdk.services.secretsmanager.CfnSecretTargetAttachment; 12 | import software.amazon.awscdk.services.secretsmanager.ISecret; 13 | import software.amazon.awscdk.services.secretsmanager.Secret; 14 | import software.amazon.awscdk.services.secretsmanager.SecretStringGenerator; 15 | import software.amazon.awscdk.services.ssm.StringParameter; 16 | import software.constructs.Construct; 17 | 18 | /** 19 | * Creates a Postgres database in the isolated subnets of a given VPC. 20 | *

21 | * The following parameters need to exist in the SSM parameter store for this stack to successfully deploy: 22 | *

    23 | *
  • <environmentName>-Network-vpcId: ID of the VPC to deploy the database into.
  • 24 | *
  • <environmentName>-Network-isolatedSubnetOne: ID of the first isolated subnet to deploy the database into.
  • 25 | *
  • <environmentName>-Network-isolatedSubnetTwo: ID of the first isolated subnet to deploy the database into.
  • 26 | *
  • <environmentName>-Network-availabilityZoneOne: ID of the first AZ to deploy the database into.
  • 27 | *
  • <environmentName>-Network-availabilityZoneTwo: ID of the second AZ to deploy the database into.
  • 28 | *
29 | *

30 | * The stack exposes the following output parameters in the SSM parameter store to be used in other stacks: 31 | *

    32 | *
  • <environmentName>-<applicationName>-Database-endpointAddress: URL of the database
  • 33 | *
  • <environmentName>-<applicationName>-Database-endpointPort: port to access the database
  • 34 | *
  • <environmentName>-<applicationName>-Database-databaseName: name of the database
  • 35 | *
  • <environmentName>-<applicationName>-Database-securityGroupId: ID of the database's security group
  • 36 | *
  • <environmentName>-<applicationName>-Database-secretArn: ARN of the secret that stores the fields "username" and "password"
  • 37 | *
  • <environmentName>-<applicationName>-Database-instanceId: ID of the database
  • 38 | *
39 | * The static getter methods provide a convenient access to retrieve these parameters from the parameter store 40 | * for use in other stacks. 41 | */ 42 | public class PostgresDatabase extends Construct { 43 | 44 | private static final String PARAMETER_ENDPOINT_ADDRESS = "endpointAddress"; 45 | private static final String PARAMETER_ENDPOINT_PORT = "endpointPort"; 46 | private static final String PARAMETER_DATABASE_NAME = "databaseName"; 47 | private static final String PARAMETER_SECURITY_GROUP_ID = "securityGroupId"; 48 | private static final String PARAMETER_SECRET_ARN = "secretArn"; 49 | private static final String PARAMETER_INSTANCE_ID = "instanceId"; 50 | private final CfnSecurityGroup databaseSecurityGroup; 51 | private final CfnDBInstance dbInstance; 52 | private final ISecret databaseSecret; 53 | private final ApplicationEnvironment applicationEnvironment; 54 | 55 | public PostgresDatabase( 56 | final Construct scope, 57 | final String id, 58 | final Environment awsEnvironment, 59 | final ApplicationEnvironment applicationEnvironment, 60 | final DatabaseInputParameters databaseInputParameters) { 61 | 62 | super(scope, id); 63 | 64 | this.applicationEnvironment = applicationEnvironment; 65 | 66 | // Sadly, we cannot use VPC.fromLookup() to resolve a VPC object from this VpcId, because it's broken 67 | // (https://github.com/aws/aws-cdk/issues/3600). So, we have to resolve all properties we need from the VPC 68 | // via SSM parameter store. 69 | Network.NetworkOutputParameters networkOutputParameters = Network.getOutputParametersFromParameterStore(this, applicationEnvironment.getEnvironmentName()); 70 | 71 | String username = sanitizeDbParameterName(applicationEnvironment.prefix("dbUser")); 72 | 73 | databaseSecurityGroup = CfnSecurityGroup.Builder.create(this, "databaseSecurityGroup") 74 | .vpcId(networkOutputParameters.getVpcId()) 75 | .groupDescription("Security Group for the database instance") 76 | .groupName(applicationEnvironment.prefix("dbSecurityGroup")) 77 | .build(); 78 | 79 | // This will generate a JSON object with the keys "username" and "password". 80 | databaseSecret = Secret.Builder.create(this, "databaseSecret") 81 | .secretName(applicationEnvironment.prefix("DatabaseSecret")) 82 | .description("Credentials to the RDS instance") 83 | .generateSecretString(SecretStringGenerator.builder() 84 | .secretStringTemplate(String.format("{\"username\": \"%s\"}", username)) 85 | .generateStringKey("password") 86 | .passwordLength(32) 87 | .excludeCharacters("@/\\\" ") 88 | .build()) 89 | .build(); 90 | 91 | CfnDBSubnetGroup subnetGroup = CfnDBSubnetGroup.Builder.create(this, "dbSubnetGroup") 92 | .dbSubnetGroupDescription("Subnet group for the RDS instance") 93 | .dbSubnetGroupName(applicationEnvironment.prefix("dbSubnetGroup")) 94 | .subnetIds(networkOutputParameters.getIsolatedSubnets()) 95 | .build(); 96 | 97 | dbInstance = CfnDBInstance.Builder.create(this, "postgresInstance") 98 | .dbInstanceIdentifier(applicationEnvironment.prefix("database")) 99 | .allocatedStorage(String.valueOf(databaseInputParameters.storageInGb)) 100 | .availabilityZone(networkOutputParameters.getAvailabilityZones().get(0)) 101 | .dbInstanceClass(databaseInputParameters.instanceClass) 102 | .dbName(sanitizeDbParameterName(applicationEnvironment.prefix("database"))) 103 | .dbSubnetGroupName(subnetGroup.getDbSubnetGroupName()) 104 | .engine("postgres") 105 | .engineVersion(databaseInputParameters.postgresVersion) 106 | .masterUsername(username) 107 | .masterUserPassword(databaseSecret.secretValueFromJson("password").unsafeUnwrap()) 108 | .publiclyAccessible(false) 109 | .vpcSecurityGroups(Collections.singletonList(databaseSecurityGroup.getAttrGroupId())) 110 | .build(); 111 | 112 | CfnSecretTargetAttachment.Builder.create(this, "secretTargetAttachment") 113 | .secretId(databaseSecret.getSecretArn()) 114 | .targetId(dbInstance.getRef()) 115 | .targetType("AWS::RDS::DBInstance") 116 | .build(); 117 | 118 | createOutputParameters(); 119 | 120 | applicationEnvironment.tag(this); 121 | 122 | } 123 | 124 | @NotNull 125 | private static String createParameterName(ApplicationEnvironment applicationEnvironment, String parameterName) { 126 | return applicationEnvironment.getEnvironmentName() + "-" + applicationEnvironment.getApplicationName() + "-Database-" + parameterName; 127 | } 128 | 129 | /** 130 | * Collects the output parameters of an already deployed {@link PostgresDatabase} construct from the parameter store. This requires 131 | * that a {@link PostgresDatabase} construct has been deployed previously. If you want to access the parameters from the same 132 | * stack that the {@link PostgresDatabase} construct is in, use the plain {@link #getOutputParameters()} method. 133 | * 134 | * @param scope the construct in which we need the output parameters 135 | * @param environment the environment for which to load the output parameters. The deployed {@link PostgresDatabase} 136 | * construct must have been deployed into this environment. 137 | */ 138 | public static DatabaseOutputParameters getOutputParametersFromParameterStore(Construct scope, ApplicationEnvironment environment) { 139 | return new DatabaseOutputParameters( 140 | getEndpointAddress(scope, environment), 141 | getEndpointPort(scope, environment), 142 | getDbName(scope, environment), 143 | getDatabaseSecretArn(scope, environment), 144 | getDatabaseSecurityGroupId(scope, environment), 145 | getDatabaseIdentifier(scope, environment)); 146 | } 147 | 148 | private static String getDatabaseIdentifier(Construct scope, ApplicationEnvironment environment) { 149 | return StringParameter.fromStringParameterName(scope, PARAMETER_INSTANCE_ID, createParameterName(environment, PARAMETER_INSTANCE_ID)) 150 | .getStringValue(); 151 | } 152 | 153 | private static String getEndpointAddress(Construct scope, ApplicationEnvironment environment) { 154 | return StringParameter.fromStringParameterName(scope, PARAMETER_ENDPOINT_ADDRESS, createParameterName(environment, PARAMETER_ENDPOINT_ADDRESS)) 155 | .getStringValue(); 156 | } 157 | 158 | private static String getEndpointPort(Construct scope, ApplicationEnvironment environment) { 159 | return StringParameter.fromStringParameterName(scope, PARAMETER_ENDPOINT_PORT, createParameterName(environment, PARAMETER_ENDPOINT_PORT)) 160 | .getStringValue(); 161 | } 162 | 163 | private static String getDbName(Construct scope, ApplicationEnvironment environment) { 164 | return StringParameter.fromStringParameterName(scope, PARAMETER_DATABASE_NAME, createParameterName(environment, PARAMETER_DATABASE_NAME)) 165 | .getStringValue(); 166 | } 167 | 168 | private static String getDatabaseSecretArn(Construct scope, ApplicationEnvironment environment) { 169 | String secretArn = StringParameter.fromStringParameterName(scope, PARAMETER_SECRET_ARN, createParameterName(environment, PARAMETER_SECRET_ARN)) 170 | .getStringValue(); 171 | return secretArn; 172 | } 173 | 174 | private static String getDatabaseSecurityGroupId(Construct scope, ApplicationEnvironment environment) { 175 | String securityGroupId = StringParameter.fromStringParameterName(scope, PARAMETER_SECURITY_GROUP_ID, createParameterName(environment, PARAMETER_SECURITY_GROUP_ID)) 176 | .getStringValue(); 177 | return securityGroupId; 178 | } 179 | 180 | /** 181 | * Creates the outputs of this stack to be consumed by other stacks. 182 | */ 183 | private void createOutputParameters() { 184 | 185 | StringParameter endpointAddress = StringParameter.Builder.create(this, "endpointAddress") 186 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_ENDPOINT_ADDRESS)) 187 | .stringValue(this.dbInstance.getAttrEndpointAddress()) 188 | .build(); 189 | 190 | StringParameter endpointPort = StringParameter.Builder.create(this, "endpointPort") 191 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_ENDPOINT_PORT)) 192 | .stringValue(this.dbInstance.getAttrEndpointPort()) 193 | .build(); 194 | 195 | StringParameter databaseName = StringParameter.Builder.create(this, "databaseName") 196 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_DATABASE_NAME)) 197 | .stringValue(this.dbInstance.getDbName()) 198 | .build(); 199 | 200 | StringParameter securityGroupId = StringParameter.Builder.create(this, "securityGroupId") 201 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_SECURITY_GROUP_ID)) 202 | .stringValue(this.databaseSecurityGroup.getAttrGroupId()) 203 | .build(); 204 | 205 | StringParameter secret = StringParameter.Builder.create(this, "secret") 206 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_SECRET_ARN)) 207 | .stringValue(this.databaseSecret.getSecretArn()) 208 | .build(); 209 | 210 | StringParameter instanceId = StringParameter.Builder.create(this, "instanceId") 211 | .parameterName(createParameterName(this.applicationEnvironment, PARAMETER_INSTANCE_ID)) 212 | .stringValue(this.dbInstance.getDbInstanceIdentifier()) 213 | .build(); 214 | } 215 | 216 | private String sanitizeDbParameterName(String dbParameterName) { 217 | return dbParameterName 218 | // db name must have only alphanumerical characters 219 | .replaceAll("[^a-zA-Z0-9]", "") 220 | // db name must start with a letter 221 | .replaceAll("^[0-9]", "a"); 222 | } 223 | 224 | /** 225 | * Collects the output parameters that other constructs might be interested in. 226 | */ 227 | public DatabaseOutputParameters getOutputParameters() { 228 | return new DatabaseOutputParameters( 229 | this.dbInstance.getAttrEndpointAddress(), 230 | this.dbInstance.getAttrEndpointPort(), 231 | this.dbInstance.getDbName(), 232 | this.databaseSecret.getSecretArn(), 233 | this.databaseSecurityGroup.getAttrGroupId(), 234 | this.dbInstance.getDbInstanceIdentifier()); 235 | } 236 | 237 | public static class DatabaseInputParameters { 238 | private int storageInGb = 20; 239 | private String instanceClass = "db.t2.micro"; 240 | private String postgresVersion = "12.9"; 241 | 242 | /** 243 | * The storage allocated for the database in GB. 244 | *

245 | * Default: 20. 246 | */ 247 | public DatabaseInputParameters withStorageInGb(int storageInGb) { 248 | this.storageInGb = storageInGb; 249 | return this; 250 | } 251 | 252 | /** 253 | * The class of the database instance. 254 | *

255 | * Default: "db.t2.micro". 256 | */ 257 | public DatabaseInputParameters withInstanceClass(String instanceClass) { 258 | Objects.requireNonNull(instanceClass); 259 | this.instanceClass = instanceClass; 260 | return this; 261 | } 262 | 263 | /** 264 | * The version of the PostGres database. 265 | *

266 | * Default: "11.5". 267 | */ 268 | public DatabaseInputParameters withPostgresVersion(String postgresVersion) { 269 | Objects.requireNonNull(postgresVersion); 270 | this.postgresVersion = postgresVersion; 271 | return this; 272 | } 273 | 274 | } 275 | 276 | public static class DatabaseOutputParameters { 277 | private final String endpointAddress; 278 | private final String endpointPort; 279 | private final String dbName; 280 | private final String databaseSecretArn; 281 | private final String databaseSecurityGroupId; 282 | private final String instanceId; 283 | 284 | public DatabaseOutputParameters( 285 | String endpointAddress, 286 | String endpointPort, 287 | String dbName, 288 | String databaseSecretArn, 289 | String databaseSecurityGroupId, 290 | String instanceId) { 291 | this.endpointAddress = endpointAddress; 292 | this.endpointPort = endpointPort; 293 | this.dbName = dbName; 294 | this.databaseSecretArn = databaseSecretArn; 295 | this.databaseSecurityGroupId = databaseSecurityGroupId; 296 | this.instanceId = instanceId; 297 | } 298 | 299 | /** 300 | * The URL of the Postgres instance. 301 | */ 302 | public String getEndpointAddress() { 303 | return endpointAddress; 304 | } 305 | 306 | 307 | /** 308 | * The port of the Postgres instance. 309 | */ 310 | public String getEndpointPort() { 311 | return endpointPort; 312 | } 313 | 314 | /** 315 | * The database name of the Postgres instance. 316 | */ 317 | public String getDbName() { 318 | return dbName; 319 | } 320 | 321 | /** 322 | * The secret containing username and password. 323 | */ 324 | public String getDatabaseSecretArn() { 325 | return databaseSecretArn; 326 | } 327 | 328 | /** 329 | * The database's security group. 330 | */ 331 | public String getDatabaseSecurityGroupId() { 332 | return databaseSecurityGroupId; 333 | } 334 | 335 | /** 336 | * The database's identifier. 337 | */ 338 | public String getInstanceId() { 339 | return instanceId; 340 | } 341 | } 342 | } 343 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/Service.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.Collections; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Objects; 9 | import java.util.Optional; 10 | 11 | import software.amazon.awscdk.CfnCondition; 12 | import software.amazon.awscdk.Environment; 13 | import software.amazon.awscdk.Fn; 14 | import software.amazon.awscdk.RemovalPolicy; 15 | import software.amazon.awscdk.services.ec2.CfnSecurityGroup; 16 | import software.amazon.awscdk.services.ec2.CfnSecurityGroupIngress; 17 | import software.amazon.awscdk.services.ecr.IRepository; 18 | import software.amazon.awscdk.services.ecr.Repository; 19 | import software.amazon.awscdk.services.ecs.CfnService; 20 | import software.amazon.awscdk.services.ecs.CfnTaskDefinition; 21 | import software.amazon.awscdk.services.elasticloadbalancingv2.CfnListenerRule; 22 | import software.amazon.awscdk.services.elasticloadbalancingv2.CfnTargetGroup; 23 | import software.amazon.awscdk.services.iam.Effect; 24 | import software.amazon.awscdk.services.iam.PolicyDocument; 25 | import software.amazon.awscdk.services.iam.PolicyStatement; 26 | import software.amazon.awscdk.services.iam.Role; 27 | import software.amazon.awscdk.services.iam.ServicePrincipal; 28 | import software.amazon.awscdk.services.logs.LogGroup; 29 | import software.amazon.awscdk.services.logs.RetentionDays; 30 | import software.constructs.Construct; 31 | 32 | import static java.util.Collections.singletonList; 33 | 34 | /** 35 | * Creates an ECS service on top of a {@link Network}. Loads Docker images from a {@link DockerImageSource} 36 | * and places them into ECS task. Creates a log group for each ECS task. Creates a target group for the ECS tasks 37 | * that is attached to the load balancer from the {@link Network}. 38 | */ 39 | public class Service extends Construct { 40 | 41 | public Service( 42 | final Construct scope, 43 | final String id, 44 | final Environment awsEnvironment, 45 | final ApplicationEnvironment applicationEnvironment, 46 | final ServiceInputParameters serviceInputParameters, 47 | final Network.NetworkOutputParameters networkOutputParameters) { 48 | super(scope, id); 49 | 50 | List stickySessionConfiguration = Arrays.asList( 51 | CfnTargetGroup.TargetGroupAttributeProperty.builder().key("stickiness.enabled").value("true").build(), 52 | CfnTargetGroup.TargetGroupAttributeProperty.builder().key("stickiness.type").value("lb_cookie").build(), 53 | CfnTargetGroup.TargetGroupAttributeProperty.builder().key("stickiness.lb_cookie.duration_seconds").value("3600").build() 54 | ); 55 | List deregistrationDelayConfiguration = List.of( 56 | CfnTargetGroup.TargetGroupAttributeProperty.builder().key("deregistration_delay.timeout_seconds").value("5").build() 57 | ); 58 | List targetGroupAttributes = new ArrayList<>(deregistrationDelayConfiguration); 59 | if (serviceInputParameters.stickySessionsEnabled) { 60 | targetGroupAttributes.addAll(stickySessionConfiguration); 61 | } 62 | 63 | CfnTargetGroup targetGroup = CfnTargetGroup.Builder.create(this, "targetGroup") 64 | .healthCheckIntervalSeconds(serviceInputParameters.healthCheckIntervalSeconds) 65 | .healthCheckPath(serviceInputParameters.healthCheckPath) 66 | .healthCheckPort(String.valueOf(serviceInputParameters.containerPort)) 67 | .healthCheckProtocol(serviceInputParameters.containerProtocol) 68 | .healthCheckTimeoutSeconds(serviceInputParameters.healthCheckTimeoutSeconds) 69 | .healthyThresholdCount(serviceInputParameters.healthyThresholdCount) 70 | .unhealthyThresholdCount(serviceInputParameters.unhealthyThresholdCount) 71 | .targetGroupAttributes(targetGroupAttributes) 72 | .targetType("ip") 73 | .port(serviceInputParameters.containerPort) 74 | .protocol(serviceInputParameters.containerProtocol) 75 | .vpcId(networkOutputParameters.getVpcId()) 76 | .build(); 77 | 78 | CfnListenerRule.ActionProperty actionProperty = CfnListenerRule.ActionProperty.builder() 79 | .targetGroupArn(targetGroup.getRef()) 80 | .type("forward") 81 | .build(); 82 | 83 | CfnListenerRule.RuleConditionProperty condition = CfnListenerRule.RuleConditionProperty.builder() 84 | .field("path-pattern") 85 | .values(singletonList("*")) 86 | .build(); 87 | 88 | Optional httpsListenerArn = networkOutputParameters.getHttpsListenerArn(); 89 | 90 | // We only want the HTTPS listener to be deployed if the httpsListenerArn is present. 91 | if (httpsListenerArn.isPresent()) { 92 | CfnListenerRule httpsListenerRule = CfnListenerRule.Builder.create(this, "httpsListenerRule") 93 | .actions(singletonList(actionProperty)) 94 | .conditions(singletonList(condition)) 95 | .listenerArn(httpsListenerArn.get()) 96 | .priority(1) 97 | .build(); 98 | 99 | CfnCondition httpsListenerRuleCondition = CfnCondition.Builder.create(this, "httpsListenerRuleCondition") 100 | .expression(Fn.conditionNot(Fn.conditionEquals(httpsListenerArn.get(), "null"))) 101 | .build(); 102 | 103 | httpsListenerRule.getCfnOptions().setCondition(httpsListenerRuleCondition); 104 | } 105 | 106 | CfnListenerRule httpListenerRule = CfnListenerRule.Builder.create(this, "httpListenerRule") 107 | .actions(singletonList(actionProperty)) 108 | .conditions(singletonList(condition)) 109 | .listenerArn(networkOutputParameters.getHttpListenerArn()) 110 | .priority(serviceInputParameters.httpListenerPriority) 111 | .build(); 112 | 113 | LogGroup logGroup = LogGroup.Builder.create(this, "ecsLogGroup") 114 | .logGroupName(applicationEnvironment.prefix("logs")) 115 | .retention(serviceInputParameters.logRetention) 116 | .removalPolicy(RemovalPolicy.DESTROY) 117 | .build(); 118 | 119 | Role ecsTaskExecutionRole = Role.Builder.create(this, "ecsTaskExecutionRole") 120 | .assumedBy(ServicePrincipal.Builder.create("ecs-tasks.amazonaws.com").build()) 121 | .path("/") 122 | .inlinePolicies(Map.of( 123 | applicationEnvironment.prefix("ecsTaskExecutionRolePolicy"), 124 | PolicyDocument.Builder.create() 125 | .statements(singletonList(PolicyStatement.Builder.create() 126 | .effect(Effect.ALLOW) 127 | .resources(singletonList("*")) 128 | .actions(Arrays.asList( 129 | "ecr:GetAuthorizationToken", 130 | "ecr:BatchCheckLayerAvailability", 131 | "ecr:GetDownloadUrlForLayer", 132 | "ecr:BatchGetImage", 133 | "logs:CreateLogStream", 134 | "logs:PutLogEvents")) 135 | .build())) 136 | .build())) 137 | .build(); 138 | 139 | Role.Builder roleBuilder = Role.Builder.create(this, "ecsTaskRole") 140 | .assumedBy(ServicePrincipal.Builder.create("ecs-tasks.amazonaws.com").build()) 141 | .path("/"); 142 | 143 | if (!serviceInputParameters.taskRolePolicyStatements.isEmpty()) { 144 | roleBuilder.inlinePolicies(Map.of( 145 | applicationEnvironment.prefix("ecsTaskRolePolicy"), 146 | PolicyDocument.Builder.create() 147 | .statements(serviceInputParameters.taskRolePolicyStatements) 148 | .build())); 149 | } 150 | 151 | Role ecsTaskRole = roleBuilder.build(); 152 | 153 | String dockerRepositoryUrl = null; 154 | if (serviceInputParameters.dockerImageSource.isEcrSource()) { 155 | IRepository dockerRepository = Repository.fromRepositoryName(this, "ecrRepository", serviceInputParameters.dockerImageSource.getDockerRepositoryName()); 156 | dockerRepository.grantPull(ecsTaskExecutionRole); 157 | dockerRepositoryUrl = dockerRepository.repositoryUriForTag(serviceInputParameters.dockerImageSource.getDockerImageTag()); 158 | } else { 159 | dockerRepositoryUrl = serviceInputParameters.dockerImageSource.dockerImageUrl; 160 | } 161 | 162 | CfnTaskDefinition.ContainerDefinitionProperty container = CfnTaskDefinition.ContainerDefinitionProperty.builder() 163 | .name(containerName(applicationEnvironment)) 164 | .cpu(serviceInputParameters.cpu) 165 | .memory(serviceInputParameters.memory) 166 | .image(dockerRepositoryUrl) 167 | .logConfiguration(CfnTaskDefinition.LogConfigurationProperty.builder() 168 | .logDriver("awslogs") 169 | .options(Map.of( 170 | "awslogs-group", logGroup.getLogGroupName(), 171 | "awslogs-region", awsEnvironment.getRegion(), 172 | "awslogs-stream-prefix", applicationEnvironment.prefix("stream"), 173 | "awslogs-datetime-format", serviceInputParameters.awslogsDateTimeFormat)) 174 | .build()) 175 | .portMappings(singletonList(CfnTaskDefinition.PortMappingProperty.builder() 176 | .containerPort(serviceInputParameters.containerPort) 177 | .build())) 178 | .environment(toKeyValuePairs(serviceInputParameters.environmentVariables)) 179 | .stopTimeout(2) 180 | .build(); 181 | 182 | CfnTaskDefinition taskDefinition = CfnTaskDefinition.Builder.create(this, "taskDefinition") 183 | // skipped family 184 | .cpu(String.valueOf(serviceInputParameters.cpu)) 185 | .memory(String.valueOf(serviceInputParameters.memory)) 186 | .networkMode("awsvpc") 187 | .requiresCompatibilities(singletonList("FARGATE")) 188 | .executionRoleArn(ecsTaskExecutionRole.getRoleArn()) 189 | .taskRoleArn(ecsTaskRole.getRoleArn()) 190 | .containerDefinitions(singletonList(container)) 191 | .build(); 192 | 193 | CfnSecurityGroup ecsSecurityGroup = CfnSecurityGroup.Builder.create(this, "ecsSecurityGroup") 194 | .vpcId(networkOutputParameters.getVpcId()) 195 | .groupDescription("SecurityGroup for the ECS containers") 196 | .build(); 197 | 198 | // allow ECS containers to access each other 199 | CfnSecurityGroupIngress ecsIngressFromSelf = CfnSecurityGroupIngress.Builder.create(this, "ecsIngressFromSelf") 200 | .ipProtocol("-1") 201 | .sourceSecurityGroupId(ecsSecurityGroup.getAttrGroupId()) 202 | .groupId(ecsSecurityGroup.getAttrGroupId()) 203 | .build(); 204 | 205 | // allow the load balancer to access the containers 206 | CfnSecurityGroupIngress ecsIngressFromLoadbalancer = CfnSecurityGroupIngress.Builder.create(this, "ecsIngressFromLoadbalancer") 207 | .ipProtocol("-1") 208 | .sourceSecurityGroupId(networkOutputParameters.getLoadbalancerSecurityGroupId()) 209 | .groupId(ecsSecurityGroup.getAttrGroupId()) 210 | .build(); 211 | 212 | allowIngressFromEcs(serviceInputParameters.securityGroupIdsToGrantIngressFromEcs, ecsSecurityGroup); 213 | 214 | CfnService service = CfnService.Builder.create(this, "ecsService") 215 | .cluster(networkOutputParameters.getEcsClusterName()) 216 | .launchType("FARGATE") 217 | .deploymentConfiguration(CfnService.DeploymentConfigurationProperty.builder() 218 | .maximumPercent(serviceInputParameters.maximumInstancesPercent) 219 | .minimumHealthyPercent(serviceInputParameters.minimumHealthyInstancesPercent) 220 | .build()) 221 | .desiredCount(serviceInputParameters.desiredInstancesCount) 222 | .taskDefinition(taskDefinition.getRef()) 223 | .loadBalancers(singletonList(CfnService.LoadBalancerProperty.builder() 224 | .containerName(containerName(applicationEnvironment)) 225 | .containerPort(serviceInputParameters.containerPort) 226 | .targetGroupArn(targetGroup.getRef()) 227 | .build())) 228 | .networkConfiguration(CfnService.NetworkConfigurationProperty.builder() 229 | .awsvpcConfiguration(CfnService.AwsVpcConfigurationProperty.builder() 230 | .assignPublicIp("ENABLED") 231 | .securityGroups(singletonList(ecsSecurityGroup.getAttrGroupId())) 232 | .subnets(networkOutputParameters.getPublicSubnets()) 233 | .build()) 234 | .build()) 235 | .build(); 236 | 237 | // Adding an explicit dependency from the service to the listeners to avoid "has no load balancer associated" error 238 | // (see https://stackoverflow.com/questions/61250772/how-can-i-create-a-dependson-relation-between-ec2-and-rds-using-aws-cdk). 239 | service.addDependsOn(httpListenerRule); 240 | 241 | applicationEnvironment.tag(this); 242 | } 243 | 244 | private void allowIngressFromEcs(List securityGroupIds, CfnSecurityGroup ecsSecurityGroup) { 245 | int i = 1; 246 | for (String securityGroupId : securityGroupIds) { 247 | CfnSecurityGroupIngress ingress = CfnSecurityGroupIngress.Builder.create(this, "securityGroupIngress" + i) 248 | .sourceSecurityGroupId(ecsSecurityGroup.getAttrGroupId()) 249 | .groupId(securityGroupId) 250 | .ipProtocol("-1") 251 | .build(); 252 | i++; 253 | } 254 | } 255 | 256 | private String containerName(ApplicationEnvironment applicationEnvironment) { 257 | return applicationEnvironment.prefix("container"); 258 | } 259 | 260 | private CfnTaskDefinition.KeyValuePairProperty keyValuePair(String key, String value) { 261 | return CfnTaskDefinition.KeyValuePairProperty.builder() 262 | .name(key) 263 | .value(value) 264 | .build(); 265 | } 266 | 267 | public List toKeyValuePairs(Map map) { 268 | List keyValuePairs = new ArrayList<>(); 269 | for (Map.Entry entry : map.entrySet()) { 270 | keyValuePairs.add(keyValuePair(entry.getKey(), entry.getValue())); 271 | } 272 | return keyValuePairs; 273 | } 274 | 275 | public static class DockerImageSource { 276 | private final String dockerRepositoryName; 277 | private final String dockerImageTag; 278 | private final String dockerImageUrl; 279 | 280 | /** 281 | * Loads a Docker image from the given URL. 282 | */ 283 | public DockerImageSource(String dockerImageUrl) { 284 | Objects.requireNonNull(dockerImageUrl); 285 | this.dockerImageUrl = dockerImageUrl; 286 | this.dockerImageTag = null; 287 | this.dockerRepositoryName = null; 288 | } 289 | 290 | /** 291 | * Loads a Docker image from the given ECR repository and image tag. 292 | */ 293 | public DockerImageSource(String dockerRepositoryName, String dockerImageTag) { 294 | Objects.requireNonNull(dockerRepositoryName); 295 | Objects.requireNonNull(dockerImageTag); 296 | this.dockerRepositoryName = dockerRepositoryName; 297 | this.dockerImageTag = dockerImageTag; 298 | this.dockerImageUrl = null; 299 | } 300 | 301 | public boolean isEcrSource() { 302 | return this.dockerRepositoryName != null; 303 | } 304 | 305 | public String getDockerRepositoryName() { 306 | return dockerRepositoryName; 307 | } 308 | 309 | public String getDockerImageTag() { 310 | return dockerImageTag; 311 | } 312 | 313 | public String getDockerImageUrl() { 314 | return dockerImageUrl; 315 | } 316 | } 317 | 318 | public static class ServiceInputParameters { 319 | private final DockerImageSource dockerImageSource; 320 | private final Map environmentVariables; 321 | private final List securityGroupIdsToGrantIngressFromEcs; 322 | private List taskRolePolicyStatements = new ArrayList<>(); 323 | private int healthCheckIntervalSeconds = 10; 324 | private String healthCheckPath = "/"; 325 | private int containerPort = 8080; 326 | private String containerProtocol = "HTTP"; 327 | private int healthCheckTimeoutSeconds = 5; 328 | private int healthyThresholdCount = 2; 329 | private int unhealthyThresholdCount = 8; 330 | private RetentionDays logRetention = RetentionDays.ONE_WEEK; 331 | private int cpu = 256; 332 | private int memory = 512; 333 | private int desiredInstancesCount = 2; 334 | private int maximumInstancesPercent = 200; 335 | private int minimumHealthyInstancesPercent = 50; 336 | private boolean stickySessionsEnabled = false; 337 | private String awslogsDateTimeFormat = "%Y-%m-%dT%H:%M:%S.%f%z"; 338 | private int httpListenerPriority = 2; 339 | 340 | /** 341 | * Knobs and dials you can configure to run a Docker image in an ECS service. The default values are set in a way 342 | * to work out of the box with a Spring Boot application. 343 | * 344 | * @param dockerImageSource the source from where to load the Docker image that we want to deploy. 345 | * @param securityGroupIdsToGrantIngressFromEcs Ids of the security groups that the ECS containers should be granted access to. 346 | * @param environmentVariables the environment variables provided to the Java runtime within the Docker containers. 347 | */ 348 | public ServiceInputParameters( 349 | DockerImageSource dockerImageSource, 350 | List securityGroupIdsToGrantIngressFromEcs, 351 | Map environmentVariables) { 352 | this.dockerImageSource = dockerImageSource; 353 | this.environmentVariables = environmentVariables; 354 | this.securityGroupIdsToGrantIngressFromEcs = securityGroupIdsToGrantIngressFromEcs; 355 | } 356 | 357 | /** 358 | * Knobs and dials you can configure to run a Docker image in an ECS service. The default values are set in a way 359 | * to work out of the box with a Spring Boot application. 360 | * 361 | * @param dockerImageSource the source from where to load the Docker image that we want to deploy. 362 | * @param environmentVariables the environment variables provided to the Java runtime within the Docker containers. 363 | */ 364 | public ServiceInputParameters( 365 | DockerImageSource dockerImageSource, 366 | Map environmentVariables) { 367 | this.dockerImageSource = dockerImageSource; 368 | this.environmentVariables = environmentVariables; 369 | this.securityGroupIdsToGrantIngressFromEcs = Collections.emptyList(); 370 | } 371 | 372 | /** 373 | * The interval to wait between two health checks. 374 | *

375 | * Default: 15. 376 | */ 377 | public ServiceInputParameters withHealthCheckIntervalSeconds(int healthCheckIntervalSeconds) { 378 | this.healthCheckIntervalSeconds = healthCheckIntervalSeconds; 379 | return this; 380 | } 381 | 382 | /** 383 | * The path of the health check URL. 384 | *

385 | * Default: "/actuator/health". 386 | */ 387 | public ServiceInputParameters withHealthCheckPath(String healthCheckPath) { 388 | Objects.requireNonNull(healthCheckPath); 389 | this.healthCheckPath = healthCheckPath; 390 | return this; 391 | } 392 | 393 | /** 394 | * The port the application listens on within the container. 395 | *

396 | * Default: 8080. 397 | */ 398 | public ServiceInputParameters withContainerPort(int containerPort) { 399 | Objects.requireNonNull(containerPort); 400 | this.containerPort = containerPort; 401 | return this; 402 | } 403 | 404 | /** 405 | * The protocol to access the application within the container. Default: "HTTP". 406 | */ 407 | public ServiceInputParameters withContainerProtocol(String containerProtocol) { 408 | Objects.requireNonNull(containerProtocol); 409 | this.containerProtocol = containerProtocol; 410 | return this; 411 | } 412 | 413 | /** 414 | * The number of seconds to wait for a response until a health check is deemed unsuccessful. 415 | *

416 | * Default: 5. 417 | */ 418 | public ServiceInputParameters withHealthCheckTimeoutSeconds(int healthCheckTimeoutSeconds) { 419 | this.healthCheckTimeoutSeconds = healthCheckTimeoutSeconds; 420 | return this; 421 | } 422 | 423 | /** 424 | * The number of consecutive successful health checks after which an instance is declared healthy. 425 | *

426 | * Default: 2. 427 | */ 428 | public ServiceInputParameters withHealthyThresholdCount(int healthyThresholdCount) { 429 | this.healthyThresholdCount = healthyThresholdCount; 430 | return this; 431 | } 432 | 433 | /** 434 | * The number of consecutive unsuccessful health checks after which an instance is declared unhealthy. 435 | *

436 | * Default: 8. 437 | */ 438 | public ServiceInputParameters withUnhealthyThresholdCount(int unhealthyThresholdCount) { 439 | this.unhealthyThresholdCount = unhealthyThresholdCount; 440 | return this; 441 | } 442 | 443 | /** 444 | * The number of CPU units allocated to each instance of the application. See 445 | * the docs 446 | * for a table of valid values. 447 | *

448 | * Default: 256 (0.25 CPUs). 449 | */ 450 | public ServiceInputParameters withCpu(int cpu) { 451 | this.cpu = cpu; 452 | return this; 453 | } 454 | 455 | /** 456 | * The memory allocated to each instance of the application in megabytes. See 457 | * the docs 458 | * for a table of valid values. 459 | *

460 | * Default: 512. 461 | */ 462 | public ServiceInputParameters withMemory(int memory) { 463 | this.memory = memory; 464 | return this; 465 | } 466 | 467 | /** 468 | * The duration the logs of the application should be retained. 469 | *

470 | * Default: 1 week. 471 | */ 472 | public ServiceInputParameters withLogRetention(RetentionDays logRetention) { 473 | Objects.requireNonNull(logRetention); 474 | this.logRetention = logRetention; 475 | return this; 476 | } 477 | 478 | /** 479 | * The number of instances that should run in parallel behind the load balancer. 480 | *

481 | * Default: 2. 482 | */ 483 | public ServiceInputParameters withDesiredInstances(int desiredInstances) { 484 | this.desiredInstancesCount = desiredInstances; 485 | return this; 486 | } 487 | 488 | /** 489 | * The maximum percentage in relation to the desired instances that may be running at the same time 490 | * (for example during deployments). 491 | *

492 | * Default: 200. 493 | */ 494 | public ServiceInputParameters withMaximumInstancesPercent(int maximumInstancesPercent) { 495 | this.maximumInstancesPercent = maximumInstancesPercent; 496 | return this; 497 | } 498 | 499 | /** 500 | * The minimum percentage in relation to the desired instances that must be running at the same time 501 | * (for example during deployments). 502 | *

503 | * Default: 50. 504 | */ 505 | public ServiceInputParameters withMinimumHealthyInstancesPercent(int minimumHealthyInstancesPercent) { 506 | this.minimumHealthyInstancesPercent = minimumHealthyInstancesPercent; 507 | return this; 508 | } 509 | 510 | /** 511 | * The list of PolicyStatement objects that define which operations this service can perform on other 512 | * AWS resources (for example ALLOW sqs:GetQueueUrl for all SQS queues). 513 | *

514 | * Default: none (empty list). 515 | */ 516 | public ServiceInputParameters withTaskRolePolicyStatements(List taskRolePolicyStatements) { 517 | this.taskRolePolicyStatements = taskRolePolicyStatements; 518 | return this; 519 | } 520 | 521 | /** 522 | * Disable or enable sticky sessions for the the load balancer. 523 | *

524 | * Default: false. 525 | */ 526 | public ServiceInputParameters withStickySessionsEnabled(boolean stickySessionsEnabled) { 527 | this.stickySessionsEnabled = stickySessionsEnabled; 528 | return this; 529 | } 530 | 531 | /** 532 | * The format of the date time used in log entries. The awslogs driver will use this pattern to extract 533 | * the timestamp from a log event and also to distinguish between multiple multi-line log events. 534 | *

535 | * Default: %Y-%m-%dT%H:%M:%S.%f%z (to work with JSON formatted logs created with awslogs JSON Encoder). 536 | *

537 | * See also: awslogs driver 538 | */ 539 | public ServiceInputParameters withAwsLogsDateTimeFormat(String awsLogsDateTimeFormat) { 540 | this.awslogsDateTimeFormat = awsLogsDateTimeFormat; 541 | return this; 542 | } 543 | 544 | /** 545 | * The priority for the HTTP listener of the loadbalancer. The priority of two listeners must not be the same 546 | * so you need to choose different priorities for different services. 547 | */ 548 | public ServiceInputParameters withHttpListenerPriority(int priority) { 549 | this.httpListenerPriority = priority; 550 | return this; 551 | } 552 | 553 | } 554 | } 555 | -------------------------------------------------------------------------------- /src/main/java/dev/stratospheric/cdk/SpringBootApplicationStack.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import java.util.Collections; 4 | 5 | import software.amazon.awscdk.CfnOutput; 6 | import software.amazon.awscdk.CfnOutputProps; 7 | import software.amazon.awscdk.Environment; 8 | import software.amazon.awscdk.Stack; 9 | import software.amazon.awscdk.StackProps; 10 | import software.constructs.Construct; 11 | 12 | /** 13 | * This stack creates a {@link Network} and a {@link Service} that deploys a given Docker image. The {@link Service} is 14 | * pre-configured for a Spring Boot application. 15 | *

16 | * This construct is for demonstration purposes since it's not configurable and thus of little practical use. 17 | */ 18 | public class SpringBootApplicationStack extends Stack { 19 | 20 | public SpringBootApplicationStack( 21 | final Construct scope, 22 | final String id, 23 | final Environment environment, 24 | final String dockerImageUrl) { 25 | super(scope, id, StackProps.builder() 26 | .stackName("SpringBootApplication") 27 | .env(environment) 28 | .build()); 29 | 30 | 31 | Network network = new Network(this, "network", environment, "prod", new Network.NetworkInputParameters()); 32 | Service service = new Service(this, "Service", environment, new ApplicationEnvironment("SpringBootApplication", "prod"), 33 | new Service.ServiceInputParameters( 34 | new Service.DockerImageSource(dockerImageUrl), 35 | Collections.emptyList(), 36 | Collections.emptyMap()), 37 | network.getOutputParameters()); 38 | 39 | CfnOutput httpsListenerOutput = new CfnOutput(this, "loadbalancerDnsName", CfnOutputProps.builder() 40 | .exportName("loadbalancerDnsName") 41 | .value(network.getLoadBalancer().getLoadBalancerDnsName()) 42 | .build()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/dev/stratospheric/cdk/ApplicationEnvironmentTest.java: -------------------------------------------------------------------------------- 1 | package dev.stratospheric.cdk; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | 7 | class ApplicationEnvironmentTest { 8 | 9 | @Test 10 | void prefix(){ 11 | ApplicationEnvironment env = new ApplicationEnvironment("myapp", "prod"); 12 | assertThat(env.prefix("foo")).isEqualTo("prod-myapp-foo"); 13 | } 14 | 15 | @Test 16 | void longPrefix(){ 17 | ApplicationEnvironment env = new ApplicationEnvironment("my-long-application-name", "my-long-env-name"); 18 | assertThat(env.prefix("my-long-prefix", 20)).isEqualTo("-name-my-long-prefix"); 19 | } 20 | 21 | } 22 | --------------------------------------------------------------------------------