├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── NOTICE
├── README.md
├── cdk.json
├── imgs
├── app-blue.png
├── app-green.png
├── app-url.png
├── architecture-general.png
├── architecture_general.png
├── codedeploy-deployment.png
├── pipeline-1.png
├── pipeline-2.png
├── region-multi.png
├── region-single.png
├── region_multi.png
├── region_single.png
└── stacks.png
├── package.json
├── pom.xml
└── src
├── main
└── java
│ └── com
│ └── example
│ └── demo
│ ├── Constants.java
│ ├── Demo.java
│ ├── service
│ ├── Service.java
│ ├── api-bootstrap
│ │ ├── Dockerfile
│ │ ├── index.html
│ │ └── nginx.conf
│ └── api
│ │ ├── Dockerfile
│ │ ├── ExampleResource.java
│ │ ├── Main.java
│ │ └── example.html
│ └── toolchain
│ ├── CodeDeployStep.java
│ ├── Toolchain.java
│ └── codedeploy
│ ├── codedeploy_configuration.sh
│ ├── template-appspec.yaml
│ └── template-taskdef.json
└── test
└── java
└── com
└── example
└── MyResourceTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | dist/
3 | .vscode/
4 | cdk.out/
5 | pom.xml.tag
6 | pom.xml.releaseBackup
7 | pom.xml.versionsBackup
8 | pom.xml.next
9 | release.properties
10 | dependency-reduced-pom.xml
11 | buildNumber.properties
12 | .mvn/timing.properties
13 | # https://github.com/takari/maven-wrapper#usage-without-binary-jar
14 | .mvn/wrapper/maven-wrapper.jar
15 | .DS_Store
16 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | ## Code of Conduct
2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
4 | opensource-codeofconduct@amazon.com with any additional questions or comments.
5 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *main* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) Amazon.com, Inc. or its affiliates. All Rights Reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Blue/Green Deployments to Amazon ECS using AWS CDK and AWS CodeDeploy
2 |
3 | The project deploys a Java-based microservice using a CI/CD pipeline. The pipeline is implemented using the CDK Pipelines construct. The deployment uses AWS CodeDeploy Blue/Green deployment strategy. The service can be deployed in **single** or **cross-account** and **single** or **cross-Region** scenarios.
4 |
5 | 
6 |
7 | The AWS CDK application defines a top-level stack that deploys the CI/CD pipeline using AWS CodeDeploy in the specified AWS account and region. The pipeline can deploy the *Service* to a single environment or multiple environments. The **blue** version of the *Service* runtime code is deployed only once when the Service is deployed the first time in an environment. Onwards, the **green** version of the Service runtime code is deployed using AWS CodeDeploy. This Git repository contains the code of the Service and its toolchain as a self-contained solution.
8 |
9 | [Considerations when managing ECS blue/green deployments using CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/blue-green.html#blue-green-considerations) documentation includes the following: _"When managing Amazon ECS blue/green deployments using CloudFormation, you can't include updates to resources that initiate blue/green deployments and updates to other resources in the same stack update"_. The approach used in this project allows to update the Service infrastructure and runtime code in a single commit. To achieve that, the project leverages AWS CodeDeploy's [deployment model](https://docs.aws.amazon.com/codedeploy/latest/userguide/deployment-configurations.html#deployment-configuration-ecs) using configuration files to allow updating all resources in the same Git commit.
10 |
11 | ## Prerequisites
12 |
13 | The project requires the following tools:
14 | * Amazon Corretto 8 - See installation instructions in the [user guide](https://docs.aws.amazon.com/corretto/latest/corretto-11-ug/what-is-corretto-11.html)
15 | * Apache Maven - See [installation instructions](https://maven.apache.org/install.html)
16 | * Docker - See [installation instructions](https://docs.docker.com/engine/install/)
17 | * AWS CLI v2 - See [installation instructions](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
18 | * Node.js - See [installation instructions](https://nodejs.org/en/download/package-manager/)
19 |
20 | Although instructions in this document are specific for Linux environments, the project can also be built and executed from a Windows environment.
21 |
22 | ## Installation
23 |
24 | After all prerequisites are met, it usually takes around 10 minutes to follow the instructions below and deploy the AWS CDK Application for the first time. This approach supports all combinations of deploying the microservice and its toolchain to AWS accounts and Regions.
25 |
26 | ### Push the project to AWS CodeCommit
27 |
28 | To make it easier following the example, the next steps creates an AWS CodeCommit repository and use it as source. In this example, I'm authenticating into AWS CodeCommit using [git-remote-codecommit](https://docs.aws.amazon.com/codecommit/latest/userguide/setting-up-git-remote-codecommit.html). Once you have `git-remote-codecommit` configured, you can copy and paste the following commands:
29 |
30 | ```
31 | git clone https://github.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy.git
32 | cd bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy
33 | repo_name=$(aws codecommit create-repository \
34 | --repository-name Demo \
35 | --output text \
36 | --query repositoryMetadata.repositoryName)
37 | git remote set-url --push origin codecommit://${repo_name}
38 | git add .
39 | git commit -m "initial import"
40 | git push
41 | ```
42 |
43 | ## Deploy
44 |
45 | This approach supports all combinations of deploying the Service and its toolchain to AWS accounts and Regions. Below you can find a walkthrough for two scenarios: 1/ single account and single region 2/ cross-account and cross-region.
46 |
47 | *Note: As of today, single account and cross-Region scenario leads to a circular dependency during `synth` phase. If you want details about this issue, please refer to [this](https://github.com/aws/aws-cdk/issues/26691) report. As an option, you can deploy cross-account and cross-Region.*
48 |
49 | ### Single Account and Single Region
50 |
51 | If you already executed the cross-account scenario you should [cleanup](#cleanup) first.
52 |
53 | 
54 |
55 | Service is a component from the *Demo* application. Let's deploy the Service in single account and single Region scenario. Demo application belongs to the *Example Corp*. This can be accomplished in 5 steps.
56 |
57 | **1. Configure environment**
58 |
59 | Edit `src/main/java/com/example/demo/Demo.java` and update value of the following 2 properties: account number and region:
60 | ```java
61 |
62 | public static final String TOOLCHAIN_ACCOUNT = "111111111111";
63 | public static final String TOOLCHAIN_REGION = "us-east-1";
64 | ```
65 |
66 | **2. Install AWS CDK locally and Synth**
67 | ```
68 | npm install
69 | mvn clean package
70 | npx cdk synth
71 | ```
72 |
73 | **3. Push configuration changes to AWS CodeCommit**
74 |
75 | ```
76 | git add src/main/java/com/example/demo/Demo.java
77 | git add cdk.context.json
78 | git commit -m "initial config"
79 | git push codecommit://${repo_name}
80 | ```
81 |
82 | **4. One-Time Bootstrap**
83 |
84 | - **AWS CDK**
85 |
86 | Deploying AWS CDK apps into an AWS environment (a combination of an AWS account and region) requires that you provision resources the AWS CDK needs to perform the deployment. Use the AWS CDK Toolkit's `cdk bootstrap` command to achieve that. See the [documentation](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) for more information.
87 |
88 | You need to run cdk bootstrap once for each deployment environment (account and Region). Below is an example of bootstrapping the account `111111111111` for both toolchain and service:
89 | ```
90 | npx cdk bootstrap 111111111111/us-east-1
91 | ```
92 |
93 |
94 |
95 | **5. Deploy the Toolchain stack**
96 | It will deploy the microservice in the same account and region as the toolchain:
97 | ```
98 | npx cdk deploy DemoToolchain
99 | ```
100 |
101 | ### Cross-Acccount and Cross-Region
102 |
103 | If you already executed the single account and single region scenario you should [clean up](#cleanup) first.
104 |
105 | 
106 |
107 | Service is a component from the *Demo* application. Demo application belongs to the *Example Corp*. Let's deploy the Demo service in cross-account and cross-Region scenario. This can be accomplished in 5 steps:
108 |
109 | **1. Configure environment:**
110 |
111 | Edit `src/main/java/com/example/demo/Demo.java` and update the following environment variables:
112 | ```java
113 | //this is the account and region where the pipeline will be deployed
114 | public static final String TOOLCHAIN_ACCOUNT = "111111111111";
115 | public static final String TOOLCHAIN_REGION = "us-east-1";
116 |
117 | //this is the account and region where the component (service) will be deployed
118 | public static final String SERVICE_ACCOUNT = "222222222222";
119 | public static final String SERVICE_REGION = "us-east-2";
120 | ```
121 |
122 | **2. Install AWS CDK locally and Synth**
123 | ```
124 | npm install
125 | mvn clean package
126 | npx cdk synth
127 | ```
128 |
129 | **3. Push configuration changes to AWS CodeCommit**
130 | ```
131 | git add src/main/java/com/example/demo/Demo.java
132 | git add cdk.context.json
133 | git commit -m "cross-account config"
134 | git push codecommit://${repo_name}
135 | ```
136 |
137 | **4. One-Time Bootstrap**
138 |
139 | - **AWS CDK**
140 |
141 | Deploying AWS CDK apps into an AWS environment (a combination of an AWS account and region) requires that you provision resources the AWS CDK needs to perform the deployment. Use the AWS CDK Toolkit's `cdk bootstrap` command to achieve that. See the [documentation](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html) for more information.
142 |
143 | You need to run `cdk bootstrap` once for each deployment environment (account and region). For cross-account scenarios, you should add the parameter `--trust`. For more information, please see the [AWS CDK Bootstrapping documentation](https://docs.aws.amazon.com/cdk/v2/guide/bootstrapping.html). Below is an example for service in account `222222222222` and toolchain in account `111111111111`:
144 |
145 |
146 | ```
147 | npx cdk bootstrap 111111111111/us-east-1
148 | ```
149 | ```
150 | #make sure your aws credentials are pointing to account 222222222222
151 | npx cdk bootstrap 222222222222/us-east-2 --trust 111111111111
152 | ```
153 |
154 | **5. Deploy the Toolchain stack**
155 | ```
156 | npx cdk deploy DemoToolchain
157 | ```
158 | ## **The CI/CD Pipeline**
159 |
160 | The `Toolchain` Stack instantiates a CI/CD pipeline that builds Java based HTTP microservices. As a result, each new `Pipeline` comes with 2 stages: source and build and we configure as many deployment stages as needed. The example below shows how to create a new `Toolchain` pipeline using a builder pattern:
161 |
162 | ```java
163 | Toolchain.Builder.create(this, "BlueGreenPipeline")
164 | .setGitRepo(Toolchain.CODECOMMIT_REPO)
165 | .setGitBranch(Toolchain.CODECOMMIT_BRANCH)
166 | .addStage("UAT",
167 | EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
168 | Environment.builder()
169 | .account(Toolchain.SERVICE_ACCOUNT)
170 | .region(Toolchain.SERVICE_REGION)
171 | .build())
172 | .build();
173 | ```
174 |
175 | The `addStage` method needs to be invoked at least once to add a deployment stage. When invoked, it creates deployment stages to different AWS accounts and regions. This feature enables the implementation of different scenarios, going from single region to cross-region deployment (DR).
176 |
177 | In the example below, there is a pipeline that has three deployment stages: `UAT` (User Acceptance Test), `Prod` and `DR` (Disaster Recovery). Each deployment stage has a name, a [deployment configuration](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-bluegreen.html) and environment information, such as, account and Region where the component should be deployed.
178 |
179 | In detail:
180 |
181 | ```java
182 |
183 | public class Demo {
184 |
185 | private static final String TOOLCHAIN_ACCOUNT = "111111111111";
186 | private static final String TOOLCHAIN_REGION = "us-east-1";
187 | //CodeCommit account is the same as the toolchain account
188 | public static final String CODECOMMIT_REPO = "DemoService";
189 | public static final String CODECOMMIT_BRANCH = "main";
190 |
191 | public static final String SERVICE_ACCOUNT = "222222222222";
192 | public static final String SERVICE_REGION = "us-east-1";
193 |
194 | public static final String SERVICE_DR_ACCOUNT = "333333333333";
195 | public static final String SERVICE_DR_REGION = "us-east-2";
196 |
197 | public static void main(String args[]) {
198 |
199 | super(scope, id, props);
200 | Toolchain.Builder.create(app, Constants.APP_NAME)
201 | .stackProperties(StackProps.builder()
202 | .env(Environment.builder()
203 | .account(Demo.TOOLCHAIN_ACCOUNT)
204 | .region(Demo.TOOLCHAIN_REGION)
205 | .build())
206 | .stackName(Constants.APP_NAME)
207 | .build())
208 | .setGitRepo(Demo.CODECOMMIT_REPO)
209 | .setGitBranch(Demo.CODECOMMIT_BRANCH)
210 | .addStage("UAT",
211 | EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
212 | Environment.builder()
213 | .account(Demo.COMPONENT_ACCOUNT)
214 | .region(Demo.COMPONENT_REGION)
215 | .build())
216 | .addStage("Prod",
217 | EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
218 | Environment.builder()
219 | .account(Demo.COMPONENT_ACCOUNT)
220 | .region(Demo.COMPONENT_REGION)
221 | .build())
222 | .addStage("DR",
223 | EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
224 | Environment.builder()
225 | .account(Demo.COMPONENT_DR_ACCOUNT)
226 | .region(Demo.COMPONENT_DR_REGION)
227 | .build())
228 | .build();
229 | }
230 | }
231 |
232 | ```
233 |
234 | Instances of `Toolchain` create self-mutating pipelines. This means that changes to the pipeline code that are added to the repository will be reflected to the existing pipeline next time it runs the stage `UpdatePipeline`. This is a convenience for adding stages as new environments need to be created.
235 |
236 | Self-Mutating pipelines promote the notion of a self-contained solution where the toolchain code, microservice infrastructure code and microservice runtime code are all maintained inside the same Git repository. For more information, please check [this](https://aws.amazon.com/pt/blogs/developer/cdk-pipelines-continuous-delivery-for-aws-cdk-applications/) blog about CDK Pipelines.
237 |
238 | The image below shows an example pipeline created with a deployment stage named `UAT`:
239 |
240 |
241 |
242 |
243 | ## **Stacks Created**
244 |
245 | In a minimal deployment scenario, AWS CloudFormation will display two stacks: `DemoToolchain` and `DemoService-UAT`. CDKPipelines takes care of configuring permissions to CodeDeploy, KMS and S3 (pipeline artifacts). The `DemoToolchain` stack deploys the pipeline and the `DemoService-UAT` stack deploys the component in the `UAT` environment. In this case, pipeline and `UAT` were deployed in the same account and region.
246 |
247 |
248 | ## Clean up
249 |
250 | - Clean the S3 bucket used to store the pipeline artifacts. Bucket name should be similar to the one from the example below:
251 | ```
252 | aws s3 rm --recursive s3://demotoolchain-pipelinedemopipelineartifactsbucket-1e9tkte03ib30
253 | ```
254 |
257 | - Destroy the stacks:
258 |
259 | ```
260 | npx cdk destroy "**" #Includes the deployments done by the pipeline
261 | ```
262 | If, for some reason, the destroy fails, just wait for it to finish and try again.
263 |
264 | - Delete the repository:
265 |
266 | ```
267 | aws codecommit delete-repository --repository-name DemoService
268 | ```
269 | ## Testing
270 |
271 | Once the deployment of the blue task is complete, you can find the public URL of the application load balancer in the Outputs tab of the CloudFormation stack named `DemoService-UAT` (image below) to test the application. If you open the application before the green deployment is completed, you can see the rollout live.
272 |
273 |
274 |
275 | Once acessed, thhe service displays a hello-world screen with some coloured circules representing the version of the service. At this point, refreshing the page repeatedly will show the different versions of the same service. The Blue and Green versions will appear as in the images below:
276 |
277 | 
278 |
279 | At the same time, you can view the deployment details using the console of the CodeDeploy: for that, Sign in to the AWS Management Console and open the CodeDeploy console at https://console.aws.amazon.com/codedeploy. In the navigation pane, expand **Deploy**, and then choose **Deployments**. Click to view the details of the deployment from application **DemoService-UAT** and you will be able to see the deployment status and traffic shifting progress (image below) :
280 |
281 |
282 |
283 | ## Update log location
284 |
285 | The deployment model using AWS CodeDeploy will require changes to the properties of the task to be added into the `template-taskdef.json` file, located inside the directory `/src/main/java/com/example/demo/toolchain/codedeploy`. As an example, to update the log location for the microservice, your file will look like the following:
286 |
287 | ```json
288 | {
289 | "executionRoleArn": "TASK_EXEC_ROLE",
290 | "containerDefinitions": [
291 | {
292 | "essential": true,
293 | "image": "",
294 | "logConfiguration": {
295 | "logDriver": "awslogs",
296 | "options": {
297 | "awslogs-group" : "/ecs/Service",
298 | "awslogs-region": "us-east-1",
299 | "awslogs-create-group": "true",
300 | "awslogs-stream-prefix": "ecs"
301 | }
302 | },
303 | "name": "APPLICATION",
304 | "portMappings": [
305 | {
306 | "containerPort": 8080,
307 | "hostPort": 8080,
308 | "protocol": "tcp"
309 | }
310 | ]
311 | }
312 | ],
313 | "cpu": "256",
314 | "family": "fargate-task-definition",
315 | "memory": "512",
316 | "networkMode": "awsvpc",
317 | "requiresCompatibilities": [
318 | "FARGATE"
319 | ]
320 | }
321 | ```
322 |
323 | ## License
324 |
325 | This project is licensed under the [MIT-0](LICENSE) license.
326 |
--------------------------------------------------------------------------------
/cdk.json:
--------------------------------------------------------------------------------
1 | {
2 | "app": "mvn -e -q compile exec:java",
3 | "watch": {
4 | "include": [
5 | "**"
6 | ],
7 | "exclude": [
8 | "README.md",
9 | "cdk*.json",
10 | "target",
11 | "pom.xml",
12 | "src/test"
13 | ]
14 | },
15 | "context": {
16 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
17 | "@aws-cdk/core:stackRelativeExports": true,
18 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
19 | "@aws-cdk/aws-lambda:recognizeVersionProps": true,
20 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true
21 | }
22 | }
--------------------------------------------------------------------------------
/imgs/app-blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/app-blue.png
--------------------------------------------------------------------------------
/imgs/app-green.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/app-green.png
--------------------------------------------------------------------------------
/imgs/app-url.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/app-url.png
--------------------------------------------------------------------------------
/imgs/architecture-general.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/architecture-general.png
--------------------------------------------------------------------------------
/imgs/architecture_general.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/architecture_general.png
--------------------------------------------------------------------------------
/imgs/codedeploy-deployment.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/codedeploy-deployment.png
--------------------------------------------------------------------------------
/imgs/pipeline-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/pipeline-1.png
--------------------------------------------------------------------------------
/imgs/pipeline-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/pipeline-2.png
--------------------------------------------------------------------------------
/imgs/region-multi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/region-multi.png
--------------------------------------------------------------------------------
/imgs/region-single.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/region-single.png
--------------------------------------------------------------------------------
/imgs/region_multi.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/region_multi.png
--------------------------------------------------------------------------------
/imgs/region_single.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/region_single.png
--------------------------------------------------------------------------------
/imgs/stacks.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aws-samples/bluegreen-to-amazon-ecs-using-aws-cdk-aws-codedeploy/42061f95e92183a483d1c0a0f45199f77f6e8ee9/imgs/stacks.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "aws-cdk": "2.79.0"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 | 4.0.0
5 |
6 | com.example.demo
7 | service
8 | jar
9 | 1.0-SNAPSHOT
10 |
11 | service
12 |
13 |
14 | UTF-8
15 | 11
16 | 11
17 | 2.79.0
18 | src/main/java/com/example/demo/${project.name}/api
19 | src/main/java/com/example/demo/toolchain/codedeploy
20 | 3.1.1
21 | 5.4.0
22 | 2.35.0
23 | 2.10.0
24 |
25 |
26 |
27 |
28 | org.junit.jupiter
29 | junit-jupiter-params
30 | ${junit.version}
31 | test
32 |
33 |
34 | org.glassfish.jersey.containers
35 | jersey-container-grizzly2-http
36 |
37 |
38 | org.glassfish.jersey.inject
39 | jersey-hk2
40 |
41 |
42 | jakarta.activation
43 | jakarta.activation-api
44 | 2.0.1
45 |
46 |
47 | software.amazon.awscdk
48 | aws-cdk-lib
49 | ${cdk.version}
50 |
51 |
52 |
53 |
54 |
55 |
56 | src/main/java
57 |
58 | **/*.java
59 | **/codedeploy/*
60 |
61 | ${project.build.directory}/classes
62 |
63 | **/*
64 |
65 |
66 |
67 |
68 |
69 | com.diffplug.spotless
70 | spotless-maven-plugin
71 | ${spotless.version}
72 |
73 |
74 |
75 | src/main/java/**/*.java
76 | src/test/java/**/*.java
77 |
78 |
79 |
80 |
81 |
82 | /* (C)$YEAR */
83 |
84 |
85 |
86 | ${palantirJavaFormat.version}
87 |
88 |
89 |
90 |
91 |
92 | pom.xml
93 |
94 |
95 |
96 |
97 |
98 | maven-resources-plugin
99 | 3.0.2
100 |
101 |
102 | copy-dockerfile
103 | generate-sources
104 |
105 | copy-resources
106 |
107 |
108 | ${basedir}/target
109 |
110 |
111 | ${basedir}/${service.dockerfile.location}
112 |
113 | Dockerfile
114 |
115 |
116 |
117 |
118 |
119 |
120 | copy-codedeploy
121 | generate-sources
122 |
123 | copy-resources
124 |
125 |
126 | ${basedir}/cdk.out/codedeploy
127 |
128 |
129 | ${basedir}/${service.codedeploy.location}
130 |
131 | template-appspec.yaml
132 | template-taskdef.json
133 | codedeploy_configuration.sh
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | org.apache.maven.plugins
143 | maven-compiler-plugin
144 | 3.8.1
145 | true
146 |
147 | 1.8
148 | 1.8
149 |
150 |
151 |
152 | org.apache.maven.plugins
153 | maven-jar-plugin
154 | 3.0.2
155 |
156 |
157 |
158 | true
159 | lib/
160 | ${project.groupId}.${project.name}.api.Main
161 |
162 |
163 | lib/
164 |
165 |
166 |
167 | **/api-bootstrap/*
168 |
169 |
170 |
171 |
172 | org.apache.maven.plugins
173 | maven-dependency-plugin
174 | 3.1.2
175 |
176 |
177 | unpack-dependencies
178 | prepare-package
179 |
180 | unpack-dependencies
181 |
182 |
183 | system
184 | junit,org.mockito,org.hamcrest,software.amazon.awscdk
185 | ${project.build.directory}/classes
186 |
187 |
188 |
189 |
190 |
191 | org.codehaus.mojo
192 | exec-maven-plugin
193 |
194 | com.example.demo.Demo
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 | org.glassfish.jersey
204 | jersey-bom
205 | ${jersey.version}
206 | pom
207 | import
208 |
209 |
210 |
211 |
212 |
213 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/Constants.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo;
3 |
4 | public interface Constants {
5 |
6 | /**
7 | * This updates the name of most of the resources that are created. This will include the stack names.
8 | */
9 | public static final String APP_NAME = "Demo";
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/Demo.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo;
3 |
4 | import com.example.demo.toolchain.Toolchain;
5 |
6 | import software.amazon.awscdk.App;
7 | import software.amazon.awscdk.Environment;
8 | import software.amazon.awscdk.StackProps;
9 | import software.amazon.awscdk.services.codedeploy.EcsDeploymentConfig;
10 |
11 | /**
12 | * The application includes a Toolchain stack. This stack
13 | * creates a continuous deployment pipeline that builds and deploys
14 | * the Service component into one or multiple environments. It uses
15 | * AWS CodePipeline, AWS CodeBuild and AWS CodeDeploy to implement a
16 | * Blue/Green deployment. The Service component is part of a Demo
17 | * application that belongs to Example.com.
18 | *
19 | * The Blue/Green pipeline supports the single-account and
20 | * cross-account deployment models.
21 | *
22 | * See prerequisites (README.md) before running the application.
23 | */
24 | public class Demo {
25 |
26 | // This is the account and region for the toolchain
27 | private static final String TOOLCHAIN_ACCOUNT = "111111111111";
28 | private static final String TOOLCHAIN_REGION = "us-east-1";
29 | // CodeCommit account is the same as the toolchain account
30 | public static final String CODECOMMIT_REPO = Constants.APP_NAME;
31 | public static final String CODECOMMIT_BRANCH = "main";
32 | // This is the account and region for thhe service
33 | public static final String SERVICE_ACCOUNT = Demo.TOOLCHAIN_ACCOUNT;
34 | public static final String SERVICE_REGION = Demo.TOOLCHAIN_REGION;
35 |
36 | public static void main(String args[]) throws Exception {
37 |
38 | App app = new App();
39 |
40 | // note that the Toolchain build() method encapsulates
41 | // implementaton details for adding role permissions in cross-account scenarios
42 | Toolchain.Builder.create(app, Constants.APP_NAME+"Toolchain")
43 | .stackProperties(StackProps.builder()
44 | .env(Environment.builder()
45 | .account(Demo.TOOLCHAIN_ACCOUNT)
46 | .region(Demo.TOOLCHAIN_REGION)
47 | .build())
48 | .build())
49 | .setGitRepo(Demo.CODECOMMIT_REPO)
50 | .setGitBranch(Demo.CODECOMMIT_BRANCH)
51 | .addStage(
52 | "UAT",
53 | EcsDeploymentConfig.CANARY_10_PERCENT_5_MINUTES,
54 | Environment.builder()
55 | .account(Demo.SERVICE_ACCOUNT)
56 | .region(Demo.SERVICE_REGION)
57 | .build())
58 | .build();
59 |
60 | app.synth();
61 | }
62 | }
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/Service.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo.service;
3 |
4 | import java.util.Arrays;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 | import software.amazon.awscdk.CfnOutput;
8 | import software.amazon.awscdk.Duration;
9 | import software.amazon.awscdk.Stack;
10 | import software.amazon.awscdk.StackProps;
11 | import software.amazon.awscdk.services.codedeploy.EcsApplication;
12 | import software.amazon.awscdk.services.codedeploy.EcsBlueGreenDeploymentConfig;
13 | import software.amazon.awscdk.services.codedeploy.EcsDeploymentGroup;
14 | import software.amazon.awscdk.services.codedeploy.IEcsDeploymentConfig;
15 | import software.amazon.awscdk.services.ecr.assets.DockerImageAsset;
16 | import software.amazon.awscdk.services.ecs.ContainerDefinitionOptions;
17 | import software.amazon.awscdk.services.ecs.ContainerImage;
18 | import software.amazon.awscdk.services.ecs.DeploymentController;
19 | import software.amazon.awscdk.services.ecs.DeploymentControllerType;
20 | import software.amazon.awscdk.services.ecs.FargateTaskDefinition;
21 | import software.amazon.awscdk.services.ecs.PortMapping;
22 | import software.amazon.awscdk.services.ecs.Protocol;
23 | import software.amazon.awscdk.services.ecs.patterns.ApplicationLoadBalancedFargateService;
24 | import software.amazon.awscdk.services.elasticloadbalancingv2.AddApplicationActionProps;
25 | import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationListener;
26 | import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationProtocol;
27 | import software.amazon.awscdk.services.elasticloadbalancingv2.ApplicationTargetGroup;
28 | import software.amazon.awscdk.services.elasticloadbalancingv2.BaseApplicationListenerProps;
29 | import software.amazon.awscdk.services.elasticloadbalancingv2.ListenerAction;
30 | import software.amazon.awscdk.services.elasticloadbalancingv2.TargetType;
31 | import software.amazon.awscdk.services.iam.IRole;
32 | import software.amazon.awscdk.services.iam.ManagedPolicy;
33 | import software.amazon.awscdk.services.iam.Role;
34 | import software.amazon.awscdk.services.iam.ServicePrincipal;
35 | import software.constructs.Construct;
36 |
37 | public class Service extends Stack {
38 |
39 | private static final String ECS_TASK_CPU = "1024";
40 | private static final String ECS_TASK_MEMORY = "2048";
41 | private static final Integer ECS_CONTAINER_MEMORY_RESERVATION = 256;
42 | private static final Integer ECS_CONTAINER_MEMORY_LIMIT = 512;
43 | private static final Integer ECS_TASK_CONTAINER_PORT = 8080;
44 | private static final Integer ECS_TASK_CONTAINER_HOST_PORT = 8080;
45 |
46 | ApplicationTargetGroup tgGreen = null;
47 | ApplicationListener listenerGreen = null;
48 |
49 | public Service(Construct scope, String id, IEcsDeploymentConfig deploymentConfig, StackProps props) {
50 |
51 | super(scope, id, props);
52 |
53 | // uploading the green application to the ECR
54 | // maven default build dir is /target. Dockerfile is moved to /target so it can find the application jar (see
55 | // pom.xml)
56 | DockerImageAsset.Builder.create(this, "GreenContainer" + id)
57 | .directory("./target")
58 | .build();
59 |
60 | // L3 ECS Pattern
61 | ApplicationLoadBalancedFargateService albService = ApplicationLoadBalancedFargateService.Builder.create(
62 | this, "Service")
63 | .desiredCount(2)
64 | .serviceName(id)
65 | .deploymentController(DeploymentController.builder()
66 | .type(DeploymentControllerType.CODE_DEPLOY)
67 | .build())
68 | .taskDefinition(
69 | createECSTask(new HashMap(), id, createTaskRole(id), createTaskExecutionRole(id)))
70 | .loadBalancerName("Alb" + id)
71 | .listenerPort(80)
72 | .build();
73 |
74 | createGreenListener(albService, id);
75 |
76 | // configure AWS CodeDeploy Application and DeploymentGroup
77 | EcsApplication app = EcsApplication.Builder.create(this, "BlueGreenApplication")
78 | .applicationName(id)
79 | .build();
80 |
81 | EcsDeploymentGroup.Builder.create(this, "BlueGreenDeploymentGroup")
82 | .deploymentGroupName(id)
83 | .application(app)
84 | .service(albService.getService())
85 | .role(createCodeDeployExecutionRole(id))
86 | .blueGreenDeploymentConfig(EcsBlueGreenDeploymentConfig.builder()
87 | .blueTargetGroup(albService.getTargetGroup())
88 | .greenTargetGroup(tgGreen)
89 | .listener(albService.getListener())
90 | .testListener(listenerGreen)
91 | .terminationWaitTime(Duration.minutes(15))
92 | .build())
93 | .deploymentConfig(deploymentConfig)
94 | .build();
95 |
96 | CfnOutput.Builder.create(this, "VPC")
97 | .description("Arn of the VPC ")
98 | .value(albService.getCluster().getVpc().getVpcArn())
99 | .build();
100 |
101 | CfnOutput.Builder.create(this, "ECSCluster")
102 | .description("Name of the ECS Cluster ")
103 | .value(albService.getCluster().getClusterName())
104 | .build();
105 |
106 | CfnOutput.Builder.create(this, "TaskRole")
107 | .description("Role name of the Task being executed ")
108 | .value(albService.getService().getTaskDefinition().getTaskRole().getRoleName())
109 | .build();
110 |
111 | CfnOutput.Builder.create(this, "ExecutionRole")
112 | .description("Execution Role name of the Task being executed ")
113 | .value(albService
114 | .getService()
115 | .getTaskDefinition()
116 | .getExecutionRole()
117 | .getRoleName())
118 | .build();
119 |
120 | CfnOutput.Builder.create(this, "ServiceURL")
121 | .description("Application is acessible from this url")
122 | .value("http://" + albService.getLoadBalancer().getLoadBalancerDnsName())
123 | .build();
124 | }
125 |
126 | public FargateTaskDefinition createECSTask(
127 | Map env, String serviceName, IRole taskRole, IRole executionRole) {
128 |
129 | FargateTaskDefinition taskDef = null;
130 |
131 | taskDef = FargateTaskDefinition.Builder.create(this, "EcsTaskDef" + serviceName)
132 | .taskRole(taskRole)
133 | .executionRole(executionRole)
134 | .cpu(Integer.parseInt(Service.ECS_TASK_CPU))
135 | .memoryLimitMiB(Integer.parseInt(Service.ECS_TASK_MEMORY))
136 | .family(serviceName)
137 | .build();
138 |
139 | taskDef.addContainer(
140 | "App" + serviceName,
141 | ContainerDefinitionOptions.builder()
142 | .containerName(serviceName)
143 | .memoryReservationMiB(ECS_CONTAINER_MEMORY_RESERVATION)
144 | .memoryLimitMiB(ECS_CONTAINER_MEMORY_LIMIT)
145 | .image(ContainerImage.fromDockerImageAsset(
146 | DockerImageAsset.Builder.create(this, "BlueContainer" + serviceName)
147 | .directory(getPathDockerfile())
148 | .build()))
149 | .essential(Boolean.TRUE)
150 | .portMappings(Arrays.asList(PortMapping.builder()
151 | .containerPort(Service.ECS_TASK_CONTAINER_PORT)
152 | .hostPort(Service.ECS_TASK_CONTAINER_HOST_PORT)
153 | .protocol(Protocol.TCP)
154 | .build()))
155 | .environment(env)
156 | .build());
157 |
158 | return taskDef;
159 | }
160 |
161 | /**
162 | * The Dockerfile of the blue version of the application is inside
163 | * a directory relative to this classpath (./compute/runtime-bootstrap)
164 | * It gets loaded from /target/classes after project is built. This is the
165 | * default build dir for java/maven
166 | */
167 | private String getPathDockerfile() {
168 |
169 | String path = "./target/classes/";
170 | path += this.getClass()
171 | .getName()
172 | .substring(0, this.getClass().getName().lastIndexOf("."))
173 | .replace(".", "/");
174 | path += "/api-bootstrap";
175 |
176 | return path;
177 | }
178 |
179 | Role createTaskRole(final String id) {
180 |
181 | return Role.Builder.create(this, "EcsTaskRole" + id)
182 | .assumedBy(ServicePrincipal.Builder.create("ecs-tasks.amazonaws.com")
183 | .build())
184 | .managedPolicies(Arrays.asList(
185 | ManagedPolicy.fromAwsManagedPolicyName("CloudWatchFullAccess"),
186 | ManagedPolicy.fromAwsManagedPolicyName("AWSXRayDaemonWriteAccess"),
187 | ManagedPolicy.fromAwsManagedPolicyName("AWSAppMeshEnvoyAccess"),
188 | ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")))
189 | .build();
190 | }
191 |
192 | Role createTaskExecutionRole(final String id) {
193 |
194 | return Role.Builder.create(this, "EcsExecutionRole" + id)
195 | .roleName(id)
196 | .assumedBy(ServicePrincipal.Builder.create("ecs-tasks.amazonaws.com")
197 | .build())
198 | .managedPolicies(Arrays.asList(
199 | ManagedPolicy.fromManagedPolicyArn(
200 | this,
201 | "ecsTaskExecutionManagedPolicy",
202 | "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"),
203 | ManagedPolicy.fromAwsManagedPolicyName("CloudWatchAgentServerPolicy")))
204 | .build();
205 | }
206 |
207 | private Role createCodeDeployExecutionRole(final String id) {
208 |
209 | return Role.Builder.create(this, "CodeDeployExecRole" + id)
210 | .assumedBy(ServicePrincipal.Builder.create("codedeploy.amazonaws.com")
211 | .build())
212 | .description("CodeBuild Execution Role for " + id)
213 | .path("/")
214 | .managedPolicies(Arrays.asList(
215 | ManagedPolicy.fromAwsManagedPolicyName("AWSCodeBuildDeveloperAccess"),
216 | ManagedPolicy.fromAwsManagedPolicyName("AmazonEC2ContainerRegistryFullAccess"),
217 | ManagedPolicy.fromAwsManagedPolicyName("AmazonECS_FullAccess"),
218 | ManagedPolicy.fromAwsManagedPolicyName("CloudWatchLogsFullAccess"),
219 | ManagedPolicy.fromAwsManagedPolicyName("AWSCodeDeployRoleForECS"),
220 | ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")))
221 | .build();
222 | }
223 |
224 | public void createGreenListener(ApplicationLoadBalancedFargateService albService, String id) {
225 |
226 | // create the green listener and target group
227 | String tgGreenName = "GreenTG" + id;
228 | tgGreenName = tgGreenName.length() > 32 ? tgGreenName.substring(tgGreenName.length() - 32) : tgGreenName;
229 |
230 | ApplicationTargetGroup tgGreen = ApplicationTargetGroup.Builder.create(this, "GreenTg" + id)
231 | .protocol(ApplicationProtocol.HTTP)
232 | .targetGroupName(tgGreenName)
233 | .targetType(TargetType.IP)
234 | .vpc(albService.getCluster().getVpc())
235 | .build();
236 |
237 | ApplicationListener listenerGreen = albService
238 | .getLoadBalancer()
239 | .addListener(
240 | "GreenListener",
241 | BaseApplicationListenerProps.builder()
242 | .port(8080)
243 | .defaultTargetGroups(Arrays.asList(tgGreen))
244 | .protocol(ApplicationProtocol.HTTP)
245 | .build());
246 |
247 | listenerGreen.addAction(
248 | "GreenListenerAction" + id,
249 | AddApplicationActionProps.builder()
250 | .action(ListenerAction.forward(Arrays.asList(tgGreen)))
251 | .build());
252 |
253 | this.tgGreen = tgGreen;
254 | this.listenerGreen = listenerGreen;
255 | }
256 | }
257 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/api-bootstrap/Dockerfile:
--------------------------------------------------------------------------------
1 | # nginx state for serving content
2 | FROM public.ecr.aws/nginx/nginx:alpine
3 | ##TEST USING THE FOLLOWING VERSION public.ecr.aws/nginx/nginx:1.20.0-alpine
4 | # Set working directory to nginx asset directory
5 | WORKDIR /usr/share/nginx/html
6 | # Remove default nginx static assets
7 | RUN rm -rf ./*
8 | # Copy static assets over
9 | COPY ./index.html ./
10 | COPY ./nginx.conf /etc/nginx/
11 | # set file permissions for nginx user
12 | RUN chown -R nginx:nginx /var/cache/nginx /etc/nginx/
13 | # User guest on Alpine
14 | USER nginx
15 | HEALTHCHECK CMD curl http://localhost:8080 || exit 1
16 | # Containers run nginx with global directives and daemon off
17 | ENTRYPOINT ["nginx", "-g", "daemon off;"]
18 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/api-bootstrap/nginx.conf:
--------------------------------------------------------------------------------
1 | user nginx;
2 | worker_processes auto;
3 |
4 | error_log /var/log/nginx/error.log warn;
5 | pid /dev/shm/nginx.pid;
6 |
7 | events {
8 | worker_connections 1024;
9 | }
10 |
11 | http {
12 | include /etc/nginx/mime.types;
13 | default_type application/octet-stream;
14 | sendfile off;
15 | access_log off;
16 | keepalive_timeout 3000;
17 | server {
18 | listen 8080;
19 | root /usr/share/nginx/html;
20 | index index.html;
21 | server_name localhost;
22 | client_max_body_size 16m;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/api/Dockerfile:
--------------------------------------------------------------------------------
1 | #During runtime this file will be moved to $PROJECT_HOME/target
2 | FROM public.ecr.aws/amazoncorretto/amazoncorretto:20-al2-jdk
3 | RUN mkdir -p /u01/deploy
4 | WORKDIR /u01/deploy
5 | COPY service-1.0-SNAPSHOT.jar service.jar
6 | #Guest user on alpine linux
7 | USER nobody
8 | HEALTHCHECK CMD curl http://localhost:8080 || exit 1
9 | ENTRYPOINT [ "sh", "-c", "java -jar /u01/deploy/service.jar"]
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/api/ExampleResource.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo.service.api;
3 |
4 | import jakarta.ws.rs.GET;
5 | import jakarta.ws.rs.Path;
6 | import jakarta.ws.rs.Produces;
7 | import jakarta.ws.rs.core.MediaType;
8 | import java.io.BufferedReader;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.io.InputStreamReader;
12 | import java.util.stream.Collectors;
13 |
14 | /**
15 | * Example resource returns example.html directly at the root context.
16 | */
17 | @Path("/")
18 | public class ExampleResource {
19 |
20 | private static String HTML = null;
21 |
22 | public ExampleResource() {
23 | if (ExampleResource.HTML == null) ExampleResource.HTML = getFile(getPathHTML() + "example.html");
24 | }
25 |
26 | public String getPathHTML() {
27 | return this.getClass()
28 | .getName()
29 | .substring(0, this.getClass().getName().lastIndexOf("."))
30 | .replace(".", "/") + "/";
31 | }
32 |
33 | /**
34 | * Method handling HTTP GET requests. The returned object will be sent
35 | * to the client as "text/plain" media type.
36 | *
37 | * @return String that will be returned as a text/plain response.
38 | */
39 | @GET
40 | @Produces(MediaType.TEXT_HTML)
41 | public String example() {
42 | return ExampleResource.HTML;
43 | }
44 |
45 | private String getFile(String filename) {
46 |
47 | try (InputStream in = Thread.currentThread().getContextClassLoader().getResourceAsStream(filename);
48 | BufferedReader reader = new BufferedReader(new InputStreamReader(in))) {
49 | // Use resource
50 | final String fileAsText = reader.lines().collect(Collectors.joining());
51 | return fileAsText;
52 | } catch (IOException ioe) {
53 | System.out.println("ExampleResource::Cannot HTML file " + filename + ". IOException:" + ioe.getMessage());
54 | ioe.printStackTrace();
55 | return "";
56 | }
57 | }
58 |
59 | public static void main(String args[]) {
60 |
61 | ExampleResource e = new ExampleResource();
62 | System.out.println(e.getPathHTML());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/service/api/Main.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo.service.api;
3 |
4 | import java.io.IOException;
5 | import java.net.InetAddress;
6 | import java.net.URI;
7 | import java.net.UnknownHostException;
8 | import org.glassfish.grizzly.http.server.HttpServer;
9 | import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
10 | import org.glassfish.jersey.server.ResourceConfig;
11 |
12 | /**
13 | * Jersey Example microservice implemented using a Grizzly HTTP server.
14 | *
15 | * @author Luiz Decaro
16 | */
17 | public class Main {
18 |
19 | public Main() {}
20 |
21 | public static void main(String[] args) {
22 |
23 | try {
24 | System.out.println("Running Java Version: " + System.getProperty("java.version"));
25 | System.out.println("\"Example\" Service");
26 |
27 | HttpServer server = (new Main()).startServer();
28 |
29 | System.out.println("Application started.\n"
30 | + "Try accessing " + Main.getBaseURI() + " in the browser.\n"
31 | + "Hit ^C to stop the application...");
32 | Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
33 | @Override
34 | public void run() {
35 | server.shutdownNow();
36 | }
37 | }));
38 |
39 | Thread.currentThread().join();
40 |
41 | } catch (Exception e) {
42 | e.printStackTrace();
43 | System.out.println("Exception msg:" + e.getMessage());
44 | } catch (Throwable t) {
45 | t.printStackTrace();
46 | System.out.println("Throwable msg:" + t.getMessage());
47 | }
48 | }
49 |
50 | /**
51 | * Creates a JSON HTTP Web Service using Grizzly and Jersey.
52 | *
53 | * @return new instance of the Grizzly HTTP server
54 | * @throws IOException
55 | */
56 | HttpServer startServer() throws IOException {
57 |
58 | final ResourceConfig rc =
59 | new ResourceConfig().packages(this.getClass().getPackage().getName());
60 | return GrizzlyHttpServerFactory.createHttpServer(Main.getBaseURI(), rc);
61 | }
62 |
63 | static URI getBaseURI() throws UnknownHostException {
64 | String addr = "http://"
65 | + InetAddress.getLocalHost()
66 | .toString()
67 | .substring(0, InetAddress.getLocalHost().toString().indexOf("/")) + ":" + getPort(8080) + "/";
68 | System.out.println(addr);
69 | return URI.create(addr);
70 | }
71 |
72 | private static int getPort(int defaultPort) {
73 | final String port = System.getProperty("jersey.config.test.container.port");
74 | if (null != port) {
75 | try {
76 | return Integer.parseInt(port);
77 | } catch (NumberFormatException e) {
78 | System.out.println("Value of jersey.config.test.container.port property"
79 | + " is not a valid positive integer [" + port + "]."
80 | + " Reverting to default [" + defaultPort + "].");
81 | }
82 | }
83 | return defaultPort;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/toolchain/CodeDeployStep.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo.toolchain;
3 |
4 | import java.util.Arrays;
5 |
6 | import software.amazon.awscdk.pipelines.CodePipelineActionFactoryResult;
7 | import software.amazon.awscdk.pipelines.FileSet;
8 | import software.amazon.awscdk.pipelines.ICodePipelineActionFactory;
9 | import software.amazon.awscdk.pipelines.ProduceActionOptions;
10 | import software.amazon.awscdk.pipelines.Step;
11 | import software.amazon.awscdk.services.codedeploy.IEcsDeploymentGroup;
12 | import software.amazon.awscdk.services.codepipeline.Artifact;
13 | import software.amazon.awscdk.services.codepipeline.IStage;
14 | import software.amazon.awscdk.services.codepipeline.actions.CodeDeployEcsContainerImageInput;
15 | import software.amazon.awscdk.services.codepipeline.actions.CodeDeployEcsDeployAction;
16 |
17 | class CodeDeployStep extends Step implements ICodePipelineActionFactory {
18 |
19 | FileSet fileSet = null;
20 | IEcsDeploymentGroup deploymentGroup = null;
21 | String envType = null;
22 |
23 | public CodeDeployStep(String id, FileSet fileSet, IEcsDeploymentGroup deploymentGroup, String stageName) {
24 | super(id);
25 | this.fileSet = fileSet;
26 | this.deploymentGroup = deploymentGroup;
27 | this.envType = stageName;
28 | }
29 |
30 | @Override
31 | public CodePipelineActionFactoryResult produceAction(IStage stage, ProduceActionOptions options) {
32 |
33 | Artifact artifact = options.getArtifacts().toCodePipeline(fileSet);
34 |
35 | stage.addAction(CodeDeployEcsDeployAction.Builder.create()
36 | .actionName("Deploy")
37 | .appSpecTemplateInput(artifact)
38 | .taskDefinitionTemplateInput(artifact)
39 | .runOrder(options.getRunOrder())
40 | .containerImageInputs(Arrays.asList(CodeDeployEcsContainerImageInput.builder()
41 | .input(artifact)
42 | .taskDefinitionPlaceholder("IMAGE1_NAME")
43 | .build()))
44 | .deploymentGroup(deploymentGroup)
45 | .variablesNamespace("deployment-" + envType)
46 | .build());
47 |
48 | return CodePipelineActionFactoryResult.builder().runOrdersConsumed(1).build();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/toolchain/Toolchain.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example.demo.toolchain;
3 |
4 | import java.util.ArrayList;
5 | import java.util.Arrays;
6 | import java.util.HashMap;
7 | import java.util.List;
8 | import java.util.Map;
9 |
10 | import com.example.demo.Constants;
11 | import com.example.demo.service.Service;
12 |
13 | import software.amazon.awscdk.Arn;
14 | import software.amazon.awscdk.ArnComponents;
15 | import software.amazon.awscdk.ArnFormat;
16 | import software.amazon.awscdk.Environment;
17 | import software.amazon.awscdk.Stack;
18 | import software.amazon.awscdk.StackProps;
19 | import software.amazon.awscdk.Stage;
20 | import software.amazon.awscdk.pipelines.CodeCommitSourceOptions;
21 | import software.amazon.awscdk.pipelines.CodePipeline;
22 | import software.amazon.awscdk.pipelines.CodePipelineSource;
23 | import software.amazon.awscdk.pipelines.ShellStep;
24 | import software.amazon.awscdk.pipelines.StageDeployment;
25 | import software.amazon.awscdk.pipelines.Step;
26 | import software.amazon.awscdk.services.codecommit.Repository;
27 | import software.amazon.awscdk.services.codedeploy.EcsApplication;
28 | import software.amazon.awscdk.services.codedeploy.EcsDeploymentGroup;
29 | import software.amazon.awscdk.services.codedeploy.EcsDeploymentGroupAttributes;
30 | import software.amazon.awscdk.services.codedeploy.IEcsApplication;
31 | import software.amazon.awscdk.services.codedeploy.IEcsDeploymentConfig;
32 | import software.amazon.awscdk.services.codedeploy.IEcsDeploymentGroup;
33 | import software.amazon.awscdk.services.codepipeline.actions.CodeCommitTrigger;
34 | import software.amazon.awscdk.services.iam.Effect;
35 | import software.amazon.awscdk.services.iam.PolicyStatement;
36 | import software.constructs.Construct;
37 |
38 | public class Toolchain extends Stack {
39 |
40 | private CodePipeline pipeline = null;
41 |
42 | private Toolchain(Construct scope, String id, String gitRepoURL, String gitBranch, StackProps props) {
43 |
44 | super(scope, id, props);
45 | pipeline = createPipeline(gitRepoURL, gitBranch);
46 | }
47 |
48 | private CodePipeline createPipeline(String repoURL, String branch) {
49 |
50 | CodePipelineSource source = CodePipelineSource.codeCommit(
51 | Repository.fromRepositoryName(this, "CodeRepository", repoURL),
52 | branch,
53 | CodeCommitSourceOptions.builder()
54 | .trigger(CodeCommitTrigger.POLL)
55 | .build());
56 |
57 | return CodePipeline.Builder.create(this, "Pipeline-" + Constants.APP_NAME)
58 | .publishAssetsInParallel(Boolean.FALSE)
59 | .dockerEnabledForSelfMutation(Boolean.TRUE)
60 | .crossAccountKeys(Boolean.TRUE)
61 | .synth(ShellStep.Builder.create(Constants.APP_NAME + "-synth")
62 | .input(source)
63 | .installCommands(Arrays.asList("npm install"))
64 | .commands(Arrays.asList("mvn -B clean package", "npx cdk synth"))
65 | .build())
66 | .build();
67 | }
68 |
69 | private Toolchain addStage(
70 | final String stageName,
71 | final IEcsDeploymentConfig ecsDeploymentConfig,
72 | final Environment env,
73 | final Boolean ADD_APPROVAL) {
74 |
75 | // The stage
76 | Stage stage = Stage.Builder.create(pipeline, stageName).env(env).build();
77 |
78 | final String SERVICE_NAME = Constants.APP_NAME + "Service-" + stageName;
79 |
80 | // My stack
81 | new Service(
82 | stage,
83 | SERVICE_NAME,
84 | ecsDeploymentConfig,
85 | StackProps.builder()
86 | .stackName(SERVICE_NAME)
87 | .description(SERVICE_NAME)
88 | .build());
89 |
90 | StageDeployment stageDeployment = pipeline.addStage(stage);
91 |
92 | // Configure AWS CodeDeploy
93 | Step configureCodeDeployStep = ShellStep.Builder.create("ConfigureBlueGreenDeploy")
94 | .input(pipeline.getCloudAssemblyFileSet())
95 | .primaryOutputDirectory("codedeploy")
96 | .commands(Arrays.asList(new String[] {
97 | "chmod a+x ./codedeploy/codedeploy_configuration.sh",
98 | "./codedeploy/codedeploy_configuration.sh",
99 | String.format(
100 | "./codedeploy/codedeploy_configuration.sh %s %s %s %s %s %s",
101 | env.getAccount(),
102 | env.getRegion(),
103 | Constants.APP_NAME,
104 | stageName,
105 | ((Construct) pipeline).getNode().getId(),
106 | SERVICE_NAME)
107 | }))
108 | .build();
109 |
110 | // At the time the toolchain is deployed, the CodeDeploy deployment action
111 | // is created, but the CodeDeploy application and deployment group will not exist.
112 | // They will be created when the pipeline runs and deploys the Service stack.
113 | // When the pipeline deploys the Service to a remote account, it will create the
114 | // CodeDeploy application and deployment group in the correct environment.
115 | Step deployStep = new CodeDeployStep(
116 | "codeDeploy"+stageName.toLowerCase(),
117 | configureCodeDeployStep.getPrimaryOutput(),
118 | referenceCodeDeployDeploymentGroup(env, SERVICE_NAME, ecsDeploymentConfig, stageName),
119 | stageName);
120 |
121 | deployStep.addStepDependency(configureCodeDeployStep);
122 |
123 | stageDeployment.addPost(
124 | configureCodeDeployStep,
125 | deployStep
126 | );
127 |
128 |
129 | return this;
130 | }
131 |
132 | /**
133 | * In cross-account scenarios, CDKPipelines creates a cross-account support stack that
134 | * will deploy the CodeDeploy Action role in the remote account. This cross-account
135 | * support stack is defined in a JSON file that needs to be published to the cdk assets
136 | * bucket in the target account. When self-mutation feature is on (default), the
137 | * UpdatePipeline stage will do a cdk deploy to deploy changes to the pipeline. In
138 | * cross-account scenarios, this deployment also involves deploying/updating the
139 | * cross-account support stack. The UpdatePipeline action needs to assume a
140 | * file-publishing and deploy roles in the remote account, but the role associated
141 | * with UpdatePipeline project in Amazon CodeBuild will not have these permissions.
142 | *
143 | * CDKPipelines cannot grant these permissions automatically, because the information
144 | * about the permissions that the cross-account-support stack needs is only available after
145 | * the CDK app finishes synthetizing. At that point, thhe permissions that the pipeline
146 | * has are already locked in. Hence, for cross-account scenarios, the toolchain extends
147 | * the pipeline's UpdatePipeline stage permissions to include the file-publishing and
148 | * deploy roles.
149 | */
150 | private void grantUpdatePipelineCrossAccoutPermissions(Map stageNameEnvironment) {
151 |
152 | if (!stageNameEnvironment.isEmpty()) {
153 |
154 | this.pipeline.buildPipeline();
155 | for (String stage : stageNameEnvironment.keySet()) {
156 |
157 | HashMap condition = new HashMap<>();
158 | condition.put(
159 | "iam:ResourceTag/aws-cdk:bootstrap-role",
160 | new String[] {"file-publishing", "deploy"});
161 | pipeline.getSelfMutationProject()
162 | .getRole()
163 | .addToPrincipalPolicy(PolicyStatement.Builder.create()
164 | .actions(Arrays.asList("sts:AssumeRole"))
165 | .effect(Effect.ALLOW)
166 | .resources(Arrays.asList("arn:*:iam::"
167 | + stageNameEnvironment.get(stage).getAccount() + ":role/*"))
168 | .conditions(new HashMap() {
169 | {
170 | put("ForAnyValue:StringEquals", condition);
171 | }
172 | })
173 | .build());
174 | }
175 | }
176 | }
177 |
178 | private IEcsDeploymentGroup referenceCodeDeployDeploymentGroup(
179 | final Environment env, final String serviceName, final IEcsDeploymentConfig ecsDeploymentConfig, final String stageName) {
180 |
181 | IEcsApplication codeDeployApp = EcsApplication.fromEcsApplicationArn(
182 | this,
183 | Constants.APP_NAME + "EcsCodeDeployApp-"+stageName,
184 | Arn.format(ArnComponents.builder()
185 | .arnFormat(ArnFormat.COLON_RESOURCE_NAME)
186 | .partition("aws")
187 | .region(env.getRegion())
188 | .service("codedeploy")
189 | .account(env.getAccount())
190 | .resource("application")
191 | .resourceName(serviceName)
192 | .build()));
193 |
194 | IEcsDeploymentGroup deploymentGroup = EcsDeploymentGroup.fromEcsDeploymentGroupAttributes(
195 | this,
196 | Constants.APP_NAME + "-EcsCodeDeployDG-"+stageName,
197 | EcsDeploymentGroupAttributes.builder()
198 | .deploymentGroupName(serviceName)
199 | .application(codeDeployApp)
200 | .deploymentConfig(ecsDeploymentConfig)
201 | .build());
202 |
203 | return deploymentGroup;
204 | }
205 |
206 | protected Boolean isSelfMutationEnabled() {
207 | return pipeline.getSelfMutationEnabled();
208 | }
209 |
210 | public static final class Builder implements software.amazon.jsii.Builder {
211 |
212 | private Construct scope;
213 | private String id;
214 | private String gitRepoURL;
215 | private String gitBranch;
216 | private List stages = new ArrayList<>();
217 |
218 | private software.amazon.awscdk.StackProps props;
219 |
220 | public void test() {}
221 |
222 | public Builder setGitRepo(String gitRepoURL) {
223 | this.gitRepoURL = gitRepoURL;
224 | return this;
225 | }
226 |
227 | public Builder setGitBranch(String gitBranch) {
228 | this.gitBranch = gitBranch;
229 | return this;
230 | }
231 |
232 | public Builder addStage(String name, IEcsDeploymentConfig deployConfig, Environment env) {
233 | this.stages.add(new StageConfig(name, deployConfig, env));
234 | return this;
235 | }
236 |
237 | public Toolchain build() {
238 |
239 | Map crossAccountEnvironment = new HashMap<>();
240 |
241 | Toolchain pipeline = new Toolchain(
242 | this.scope, this.id, this.gitRepoURL, this.gitBranch, this.props != null ? this.props : null);
243 | String pipelineAccount = pipeline.getAccount();
244 |
245 | for (StageConfig stageConfig : stages) {
246 |
247 | pipeline.addStage(
248 | stageConfig.getStageName(),
249 | stageConfig.getEcsDeployConfig(),
250 | stageConfig.getEnv(),
251 | stageConfig.getApproval());
252 |
253 | // if the pipeline is a self-mutating pipeline we need to add file-publishing
254 | if (pipeline.isSelfMutationEnabled()
255 | && !pipelineAccount.equals(stageConfig.getEnv().getAccount())) {
256 |
257 | crossAccountEnvironment.put(stageConfig.getStageName(), stageConfig.getEnv());
258 | }
259 | }
260 | if (!crossAccountEnvironment.isEmpty()) {
261 | pipeline.grantUpdatePipelineCrossAccoutPermissions(crossAccountEnvironment);
262 | }
263 | return pipeline;
264 | }
265 |
266 | private static final class StageConfig {
267 |
268 | String name;
269 | IEcsDeploymentConfig ecsDeploymentConfig;
270 | Environment env;
271 | Boolean approval = Boolean.FALSE;
272 |
273 | private StageConfig(String name, IEcsDeploymentConfig ecsDeploymentConfig, Environment env) {
274 | this.name = name;
275 | this.ecsDeploymentConfig = ecsDeploymentConfig;
276 | this.env = env;
277 | }
278 |
279 | public String getStageName() {
280 | return name;
281 | }
282 |
283 | public IEcsDeploymentConfig getEcsDeployConfig() {
284 | return ecsDeploymentConfig;
285 | }
286 |
287 | public Environment getEnv() {
288 | return env;
289 | }
290 |
291 | public Boolean getApproval() {
292 | return approval;
293 | }
294 | }
295 |
296 | /**
297 | * @return a new instance of {@link Builder}.
298 | * @param scope Parent of this stack, usually an `App` or a `Stage`, but could be any construct.
299 | * @param id The construct ID of this stack.
300 | */
301 | @software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Stable)
302 | public static Builder create(final software.constructs.Construct scope, final java.lang.String id) {
303 | return new Builder(scope, id);
304 | }
305 | /**
306 | * @return a new instance of {@link Builder}.
307 | * @param scope Parent of this stack, usually an `App` or a `Stage`, but could be any construct.
308 | */
309 | @software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Stable)
310 | public static Builder create(final software.constructs.Construct scope) {
311 | return new Builder(scope, null);
312 | }
313 | /**
314 | * @return a new instance of {@link Builder}.
315 | */
316 | @software.amazon.jsii.Stability(software.amazon.jsii.Stability.Level.Stable)
317 | public static Builder create() {
318 | return new Builder(null, null);
319 | }
320 |
321 | private Builder(final software.constructs.Construct scope, final java.lang.String id) {
322 | this.scope = scope;
323 | this.id = id;
324 | }
325 |
326 | public Builder stackProperties(StackProps props) {
327 | this.props = props;
328 | return this;
329 | }
330 | }
331 | }
332 |
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/toolchain/codedeploy/codedeploy_configuration.sh:
--------------------------------------------------------------------------------
1 | ##
2 | #!/bin/sh
3 | #
4 | # Action Configure AWS CodeDeploy
5 | # It customizes the files template-appspec.yaml and template-taskdef.json to the environment
6 | #
7 | # Account = The Account Id
8 | # AppName = Name of the application
9 | # StageName = Name of the stage
10 | # Region = Name of the region (us-east-1, us-east-2)
11 | # PipelineId = Id of the pipeline
12 | # ServiceName = Name of the service. It will be used to define the role and the task definition name
13 | #
14 | # Primary output directory is codedeploy/. All the 3 files created (appspec.json, imageDetail.json and
15 | # taskDef.json) will be located inside the codedeploy/ directory
16 | #
17 | ##
18 | Account=$1
19 | Region=$2
20 | AppName=$3
21 | StageName=$4
22 | PipelineId=$5
23 | ServiceName=$6
24 | echo "Account: "$Account
25 | echo "Region: "$Region
26 | echo "AppName: "$AppName
27 | echo "StageName: "$StageName
28 | echo "PipelineId: "$PipelineId
29 | echo "ServiceName: "$ServiceName
30 | ls -l
31 | ls -l codedeploy
32 | repo_name=$(cat assembly*$PipelineId-$StageName/*.assets.json | jq -r '.dockerImages[] | .destinations[] | .repositoryName' | head -1)
33 | tag_name=$(cat assembly*$PipelineId-$StageName/*.assets.json | jq -r '.dockerImages | to_entries[0].key')
34 | echo ${repo_name}
35 | echo ${tag_name}
36 | printf '{"ImageURI":"%s"}' "$Account.dkr.ecr.$Region.amazonaws.com/${repo_name}:${tag_name}" > codedeploy/imageDetail.json
37 | sed 's#APPLICATION#'$AppName'#g' codedeploy/template-appspec.yaml > codedeploy/appspec.yaml
38 | sed 's#APPLICATION#'$AppName'#g' codedeploy/template-taskdef.json | sed 's#TASK_EXEC_ROLE#arn:aws:iam::'$Account':role/'$ServiceName'#g' | sed 's#fargate-task-definition#'$ServiceName'#g' > codedeploy/taskdef.json
39 | cat codedeploy/appspec.yaml
40 | cat codedeploy/taskdef.json
41 | cat codedeploy/imageDetail.json
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/toolchain/codedeploy/template-appspec.yaml:
--------------------------------------------------------------------------------
1 | version: 0.0
2 | Resources:
3 | - TargetService:
4 | Type: AWS::ECS::Service
5 | Properties:
6 | TaskDefinition:
7 | LoadBalancerInfo:
8 | ContainerName: "APPLICATION"
9 | ContainerPort: 8080
10 | PlatformVersion: "LATEST"
--------------------------------------------------------------------------------
/src/main/java/com/example/demo/toolchain/codedeploy/template-taskdef.json:
--------------------------------------------------------------------------------
1 | {
2 | "executionRoleArn": "TASK_EXEC_ROLE",
3 | "containerDefinitions": [
4 | {
5 | "essential": true,
6 | "image": "",
7 | "name": "APPLICATION",
8 | "portMappings": [
9 | {
10 | "containerPort": 8080,
11 | "hostPort": 8080,
12 | "protocol": "tcp"
13 | }
14 | ]
15 | }
16 | ],
17 | "cpu": "256",
18 | "family": "fargate-task-definition",
19 | "memory": "512",
20 | "networkMode": "awsvpc",
21 | "requiresCompatibilities": [
22 | "FARGATE"
23 | ]
24 | }
--------------------------------------------------------------------------------
/src/test/java/com/example/MyResourceTest.java:
--------------------------------------------------------------------------------
1 | /* (C)2023 */
2 | package com.example;
3 |
4 | import static org.junit.jupiter.api.Assertions.assertTrue;
5 |
6 | import org.junit.jupiter.api.Test;
7 |
8 | public class MyResourceTest {
9 |
10 | /**
11 | * Test to see that the message "Got it!" is sent in the response.
12 | */
13 | @Test
14 | public void testGetIt() {
15 | assertTrue(true);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------