Deploy a Container Web App on Amazon ECS Using Amazon CodeCatalyst
Learn to build and deploy a container-based web application using ECS and CodeCatalyst.
What Are the Components of ECS?
Compute Capacity Planning, and Options
Create the Code for the Resource Stack
Create CodeCatalyst CodeCatalystPreviewDevelopmentAdministrator Role
Create CodeCatalyst Repository
Create CodeCatalyst Dev Environment
Create a CodeCatalyst CI/CD Environment
Create a CodeCatalyst Workflow
Add a Render Amazon ECS Task Definition Action
Add a Deploy To Amazon ECS Action
Cleaning Up Your AWS Environment
- Create the infrastructure to run your container with Amazon ECS
- Deploy a containerized application to Amazon ECS using Amazon CodeCatalyst
About | |
---|---|
✅ AWS Level | 200 - Intermediate |
⏱ Time to complete | 15-20 minutes |
💰 Cost to complete | Less than $0.02 USD if completed in under an hour. |
🧩 Prerequisites | - AWS Account with administrator-level access* - CodeCatalyst Account [*]Accounts created within the past 24 hours might not yet have access to the services required for this tutorial. |
💻 Code Sample | Code sample used in tutorial on GitHub |
📢 Feedback | Any feedback, issues, or just a 👍 / 👎 ? |
⏰ Last Updated | 2023-04-26 |
- An AWS Account (if you don't yet have one, you can create one and set up your environment here).
- CDK installed: Visit the Get Started with AWS CDK guide to learn more.
- The example project code downloaded to extract the SampleApp.
- Docker installed and running.
- CodeCatalyst Account and Space setup with Space administrator role assigned to you (if you don't have a CodeCatalyst setup already, you can follow the Amazon CodeCatalyst setting up guide).
1
cdk --version
1
2
3
mkdir cdk-ecs-infra
cd cdk-ecs-infra
cdk init app --language typescript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Applying project template app for typescript
# Welcome to your CDK TypeScript project
This is a blank project for CDK development with TypeScript.
The `cdk.json` file tells the CDK Toolkit how to execute your app.
## Useful commands
* `npm run build` compile typescript to js
* `npm run watch` watch for changes and compile
* `npm run test` perform the jest unit tests
* `cdk deploy` deploy this stack to your default AWS account/region
* `cdk diff` compare deployed stack with current state
* `cdk synth` emits the synthesized CloudFormation template
Initializing a new git repository...
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint:
hint: git config --global init.defaultBranch <name>
hint:
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint:
hint: git branch -m <name>
Executing npm install...
npm WARN deprecated w3c-hr-time@1.0.2: Use your platform's native performance.now() and performance.timeOrigin.
✅ All done!
lib/cdk-ecs-infra-stack.ts
. This is where you will write the code for the resource stack you are going to create.- IAM role: This role will be assigned to the container to allow it to call other AWS services.
- ECS Task definition: The specific parameters to use when launching the container.
- ECS Pattern for Fargate load balanced service: This abstracts away the complexity of all the components required to create the cluster, load balancer, and service, and configures everything to work together.
aws-ec2
, aws-ecs
, aws-ecr
, and aws-cdk-lib/aws-ecs-patterns
ones. To add them, edit the lib/cdk-ecs-infra-stack.ts
file to add the dependency at the top of the file:1
2
3
4
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as ecs_patterns from "aws-cdk-lib/aws-ecs-patterns";
1
2
3
4
// Look up the default VPC
const vpc = ec2.Vpc.fromLookup(this, "VPC", {
isDefault: true
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const repository = new ecr.Repository(this, 'My-Repository',{
repositoryName: 'my-respository-cdkecsinfrastack'
});
const taskIamRole = new cdk.aws_iam.Role(this, "AppRole", {
roleName: "AppRole",
assumedBy: new cdk.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskIamRole.addManagedPolicy(
cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonECSTaskExecutionRolePolicy"
));
const taskDefinition = new ecs.FargateTaskDefinition(this, 'Task', {
taskRole: taskIamRole,
family : 'CdkEcsInfraStackTaskDef',
});
taskDefinition.addContainer('MyContainer', {
image: ecs.ContainerImage.fromRegistry('nginx:latest'),
portMappings: [{ containerPort: 80 }],
memoryReservationMiB: 256,
cpu: 256,
});
FargateTaskDefintion
, and by using ContainerImage.fromRegistry. CDK will use the Nginx container image from DockerHub.1
2
3
4
5
6
7
8
9
new ecs_patterns.ApplicationLoadBalancedFargateService(this, "MyApp", {
vpc: vpc,
taskDefinition: taskDefinition,
desiredCount: 1,
serviceName: 'MyWebApp',
assignPublicIp: true,
publicLoadBalancer: true,
healthCheckGracePeriod: cdk.Duration.seconds(5),
});
desiredCount
indicates how many copies you want, serviceName
designates what you want to call the service, and publicLoadBalancer
is set to true so that you can access it over the internet. The line setting the assignPublicIp
to true is important in this example because we are using the default VPC that does not have private subnets. Best practice recommends launching services in private subnets, but that is outside the scope of this guide.ecs_pattern.ApplicationLoadBalancedFargateService
block:1
2
3
4
5
6
7
8
9
// Output the URL of the site
new cdk.CfnOutput(this, "MyApp URL", {
value: "http://" + ecs_app.loadBalancer.loadBalancerDnsName
});
// Output the ARN of the ECS Cluster
new cdk.CfnOutput(this, "ECS cluster ARN", {
value: ecs_app.cluster.clusterArn
});
bin/cdk-ecs-infra.ts
file, and uncomment line 14:1
env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },
1
2
3
4
5
cdk bootstrap
⏳ Bootstrapping environment aws://0123456789012/<region>...
✅ Environment aws://0123456789012/<region> bootstrapped
cdk deploy
to deploy the container, cluster, and all the other infrastructure required. Please note that Docker must be running to build the containerized application. You should see output similar to the following:y
and then Enter
to deploy. CDK will now set up all the infrastructure you defined, and it will take a few minutes to complete.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ecs_patterns from "aws-cdk-lib/aws-ecs-patterns";
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
export class CdkEcsInfraStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Look up the default VPC
const vpc = ec2.Vpc.fromLookup(this, "VPC", {
isDefault: true
});
const repository = new ecr.Repository(this, 'My-Repository',{
repositoryName: 'my-respository-cdkecsinfrastack'
});
const taskIamRole = new cdk.aws_iam.Role(this, "AppRole", {
roleName: "AppRole",
assumedBy: new cdk.aws_iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
});
taskIamRole.addManagedPolicy(
cdk.aws_iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonECSTaskExecutionRolePolicy"
));
const taskDefinition = new ecs.FargateTaskDefinition(this, 'Task', {
taskRole: taskIamRole,
family : 'CdkEcsInfraStackTaskDef',
});
taskDefinition.addContainer('MyContainer', {
image: ecs.ContainerImage.fromRegistry('nginx:latest'),
portMappings: [{ containerPort: 80 }],
memoryReservationMiB: 256,
cpu: 256,
});
new ecs_patterns.ApplicationLoadBalancedFargateService(this, "MyApp", {
vpc: vpc,
taskDefinition: taskDefinition,
desiredCount: 1,
serviceName: 'MyWebApp',
assignPublicIp: true,
publicLoadBalancer: true,
});
// Output the URL of the site
new cdk.CfnOutput(this, "MyApp URL", {
value: "http://" + ecs_app.loadBalancer.loadBalancerDnsName
});
// Output the ARN of the ECS Cluster
new cdk.CfnOutput(this, "ECS cluster ARN", {
value: ecs_app.cluster.clusterArn
});
}
}
CodeCatalystPreviewDevelopmentAdministrator
role. The CodeCatalystPreviewDevelopmentAdministrator
role has broad permissions which may pose a security risk, so we recommend that you only use this role in tutorials and scenarios where security is less of a concern.CodeCatalystPreviewDevelopmentAdministrator
role.zip
file, uncompress it, and move it into our project folder with the following commands (the adb68cd
part of the directory name may be different for you, please confirm the value, and replace as needed):1
2
3
4
5
wget -O SampleApp.zip https://github.com/build-on-aws/automate-web-app-amazon-ecs-cdk-codecatalyst/zipball/main/
unzip SampleApp.zip
mv build-on-aws-automate-web-app-amazon-ecs-cdk-codecatalyst-adb68cd/SampleApp/* SampleApp/
rm -rf build-on-aws-automate-web-app-amazon-ecs-cdk-codecatalyst-adb68cd
rm SampleApp.zip
task.json
in the SampleApp
folder, and replace the <account ID>
with your account ID.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"executionRoleArn": "arn:aws:iam::<account ID>:role/AppRole",
"containerDefinitions": [
{
"name": "MyContainer",
"image": "$REPOSITORY_URI:$IMAGE_TAG",
"essential": true,
"portMappings": [
{
"hostPort": 80,
"protocol": "tcp",
"containerPort": 80
}
]
}
],
"requiresCompatibilities": [
"FARGATE"
],
"networkMode": "awsvpc",
"cpu": "256",
"memory": "512",
"family": "CdkEcsInfraStackTaskDef"
}
SampleApp
directory, run the following:1
2
3
git add .
git commit -m "Adding sample app"
git push
main
branch, and will have the following content:1
2
3
4
5
6
7
8
9
10
11
Name: Workflow_752b
SchemaVersion: "1.0"
# Optional - Set automatic triggers.
Triggers:
- Type: Push
Branches:
- main
# Required - Define action configurations.
Actions:
BuildAndDeployToECS
by changing the first line to Name: BuildAndDeployToECS
. You can configure a workflow using either yaml
, or the visual editor. Let's define the steps we need to create, and then add them using the yml
editor. We will need to create the following:- Build the application to generate a Docker image using a Build action in the workflow and define a set of input variables.
- Region is used to configure the AWS region in which you would like the application to be deployed. Provide the same region where your ECS cluster infrastructure is provisioned.
- Registry is used to configure the Docker ECR endpoints, which will be used in the further build steps.
- Image is the name of your ECR repository, which is created within the CDK project, and should be
my-respository-cdkecsinfrastack
if you used the default values provided.
- Tag the Docker image
- Push the tagged Docker image to our ECR repository.
- Export the ECR URl for the image we just created to pass it to the deploy step
- Deploy the container image to the ECS cluster.
<Account-Id>
with your account ID, and <CodeCatalystPreviewDevelopmentAdministrator role>
(if you used the default, it should be CodeCatalystWorkflowDevelopmentRole-AWS
):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Actions:
Build_application:
Identifier: aws/build@v1
Inputs:
Sources:
- WorkflowSource
Variables:
- Name: region
Value: us-west-2
- Name: registry
Value: <Account-Id>.dkr.ecr.us-west-2.amazonaws.com
- Name: image
Value: my-respository-cdkecsinfrastack
Outputs:
AutoDiscoverReports:
Enabled: false
Variables:
- IMAGE
Compute:
Type: EC2
Environment:
Connections:
- Role: <CodeCatalystPreviewDevelopmentAdministrator role>
# Add account id within quotes. Eg: "123456789012"
Name: "<Account-Id>"
Name: Non-prod
Configuration:
Steps:
- Run: export account=`aws sts get-caller-identity --output text | awk '{ print $1 }'`
- Run: aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}
- Run: docker build -t appimage .
- Run: docker tag appimage ${registry}/${image}:${WorkflowSource.CommitId}
- Run: docker push --all-tags ${registry}/${image}
- Run: export IMAGE=${registry}/${image}:${WorkflowSource.CommitId}
Validate
button - it should add a green banner with The workflow definition is valid.
at the top. You can see the details of any errors by clicking on Warnings: 1
at the bottom of the page - the 1
indicates how many errors were detected. You can also now click on the Visual
button to inspect the workflow using the Visual editor.task.json
) we created earlier with the Docker image URI that we exported in the Build action as the IMAGE
output. This value will change every time we make changes to our sample application's source code, and we need a step in our workflow to handle this update to task.json
for us.task.json
file in the SampleApp repository. This task will output an updated task definition file at the runtime.RenderAmazonECStaskdefinition
line should be indented with 2 spaces at the start:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
RenderAmazonECStaskdefinition:
Identifier: aws/ecs-render-task-definition@v1
Configuration:
image: ${Build_application.IMAGE}
container-name: MyContainer
task-definition: task.json
Outputs:
Artifacts:
- Name: TaskDefinition
Files:
- task-definition*
DependsOn:
- Build_application
Inputs:
Sources:
- WorkflowSource
<ECS cluster ARN>
with your ECS cluster ARN - this was part of the output after running cdk deploy
, and should be something like arn:aws:ecs:us-west-2:<account Id>:CdkEcsInfraStack-EcsDefaultClusterMnL3mNNYNVPC9C1EC7A3-UdnuAwVtK2q2
. Configure service to the ECS Service's name, which we defined in the CDK project. Alternatively, you can find the ECS service name in the AWS Console - Amazon Elastic Container Service - Clusters. Region is used to configure the AWS Region in which you would like your application to be deployed. Provide the same region where your ECS cluster infrastructure is provisioned. Provide the environment connection and CI/CD environment information under Environments.DeploytoAmazonECS
:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DeploytoAmazonECS:
Identifier: aws/ecs-deploy@v1
Configuration:
task-definition: /artifacts/DeploytoAmazonECS/TaskDefinition/${RenderAmazonECStaskdefinition.task-definition}
service: MyWebApp
cluster: <ECS cluster ARN>
region: us-west-2
Compute:
Type: EC2
Fleet: Linux.x86-64.Large
Environment:
Connections:
- Role: <CodeCatalystPreviewDevelopmentAdministrator role>
# Add account id within quotes. Eg: "12345678"
Name: "<Account Id>"
Name: Non-Prod
DependsOn:
- RenderAmazonECStaskdefinition
Inputs:
Artifacts:
- TaskDefinition
Sources:
- WorkflowSource
<account ID>
, <CodeCatalystPreviewDevelopmentAdministrator role>
, and <ECS cluster ARN>
values replaces with appropriate values):1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
Name: BuildAndDeployToECS
SchemaVersion: "1.0"
# Optional - Set automatic triggers.
Triggers:
- Type: Push
Branches:
- main
# Required - Define action configurations.
Actions:
Build_application:
Identifier: aws/build@v1
Inputs:
Sources:
- WorkflowSource
Variables:
- Name: region
Value: us-west-2
- Name: registry
Value: <Account-Id>.dkr.ecr.us-west-2.amazonaws.com
- Name: image
Value: my-respository-cdkecsinfrastack
Outputs:
AutoDiscoverReports:
Enabled: false
Variables:
- IMAGE
Compute:
Type: EC2
Environment:
Connections:
- Role: <CodeCatalystPreviewDevelopmentAdministrator role>
# Add account id within quotes. Eg: "123456789012"
Name: "<Account-Id>"
Name: Non-prod
Configuration:
Steps:
- Run: export account=`aws sts get-caller-identity --output text | awk '{ print $1 }'`
- Run: aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}
- Run: docker build -t appimage .
- Run: docker tag appimage ${registry}/${image}:${WorkflowSource.CommitId}
- Run: docker push --all-tags ${registry}/${image}
- Run: export IMAGE=${registry}/${image}:${WorkflowSource.CommitId}
RenderAmazonECStaskdefinition:
Identifier: aws/ecs-render-task-definition@v1
Configuration:
image: ${Build_application.IMAGE}
container-name: MyContainer
task-definition: task.json
Outputs:
Artifacts:
- Name: TaskDefinition
Files:
- task-definition*
DependsOn:
- Build_application
Inputs:
Sources:
- WorkflowSource
DeploytoAmazonECS:
Identifier: aws/ecs-deploy@v1
Configuration:
task-definition: /artifacts/DeploytoAmazonECS/TaskDefinition/${RenderAmazonECStaskdefinition.task-definition}
service: MyWebApp
cluster: <ECS cluster ARN>
region: us-west-2
Compute:
Type: EC2
Fleet: Linux.x86-64.Large
Environment:
Connections:
- Role: <CodeCatalystPreviewDevelopmentAdministrator role>
# Add account id within quotes. Eg: "12345678"
Name: "<Account Id>"
Name: Non-Prod
DependsOn:
- RenderAmazonECStaskdefinition
Inputs:
Artifacts:
- TaskDefinition
Sources:
- WorkflowSource
Commit
button to add the workflow to the repository - it will be added in the .codecatalyst/workflows
directory as BuildAndDeployToECS.yaml
. After it is committed, it should start running automatically. You can click on the Run-xxxxx
link under Recent runs
to view the execution. Once completed, it should show all the step as successful with a green checkmark:1
cdk destroy
1
2
3
4
Are you sure you want to delete: CdkEcsInfraStack (y/n)? y
CdkEcsInfraStack: destroying...
✅ CdkEcsInfraStack: destroyed
- Delete CodeCatalyst dev environment
- Delete CodeCatalyst source-repository
- Delete CodeCatalyst project
Any opinions in this post are those of the individual author and may not reflect the opinions of AWS.