Elastic Container Service (ECS) is a docker container deployment service provided by AWS. In this blog, we will be using AWS CloudFormation to write all the infrastructure needed for the deployment, as a Code (IaC). This blog is a complete guide that will cover How to Deploy Docker container with ECS (a “hello world” node app), from containerizing it to deploying it in ECS and making it accessible from a load balancer URL.

After reading this blog you will be able to deploy a docker container with ECS with the following steps.

Time needed for complete deploy: 1 hour and 30 minutes. Deploying a docker container with AWS ECS: Build a hello world express node app Build a simple hello world express app Containerize the app using docker Write a Docker file to containerize the app



Push the docker image to amazon container registry ECR Use a container registry where the docker image can be stored Build a loadbalancer A loadbalancer to access the app from the internet

Deploy the node app to an ECS cluster ECS cluster is the place where you build service and deploy the container to its tasks

Check your application running Go the loadbalancer URL and check the service running

The full functional code for this blog can be found on GitHub ecs-node-app. Without further delay let jump into the journey of complete steps to deploy a docker container with ECS.

Steps to deploy a node application:

1. Create a node app

Let’s get started by creating a node app. First, create a directory ecs-node-app in your workspace:

Note: Make sure that node is installed

> mkdir ecs-node-app > cd ecs-node-app

Initialize node app inside the directory:

> npm init

We will be using an express server. So let’s install the express server as a node package:

> npm install express --save

Now create a file server.js and initialize the express server:

// server.js const express = require('express') const app = express() const port = 3000 app.get('/', (req, res) => res.send('Hello World!')) app.listen(port, () => console.log(`Example app listening on port ${port}!`))

This will create a simple server that will print ‘Hello World!’. Try running the app using the following command:

> node app.js

Load http://localhost:3000/ in browser to see ‘Hello world’ from the node app

2. Containerize the node app with docker

Inside the ecs-node-app directory create a file called Dockerfile

// Dockerfile FROM node:8 # Create app directory WORKDIR /usr/src/app # Install app dependencies COPY package*.json ./ RUN npm install COPY . . EXPOSE 8080 CMD [ "npm", "start" ]

Now try building the container:

docker build -t node-sample .

This will create an image with a tag node-sample. Now let’s run this image:

docker run -p 8080:8080 -d node-sample

Load http://localhost:8080/ in browser to see ‘Hello world’ from the node app running in a docker container.

3. Push the image to ECR – Cloudformation

Now we have a container image ready, we need a mechanism to store this image somewhere in a container registry so that later the ECS can get the image from the repository and run the image.

For the registry, we will be using AWS Elastic Container Registry(ECR). For creating the ECR resource we will be using CloudFormation. If you are new to CloudFormation learn about it in the blog A Brief Introduction to AWS CloudFormation

Let’s create a registry with name “milap-nodejs” using Cloudformation:

// ecr.yaml AWSTemplateFormatVersion: "2010-09-09" Description: ECR repo test WM Node Test Resources: AWSTrainingTestRepo: Type: AWS::ECR::Repository Properties: RepositoryName: "milap-nodejs" Outputs: AWSTrainingTestArn: Value: !GetAtt AWSTrainingTestRepo.Arn Export: Name: AWSTrainingTestArn

Create a stack using the following command:

> aws cloudformation create-stack --stack-name training-ecr-repo --template-body file://ecr.yaml

This will create an ECR registry where we can now push our node image to.

// ECR login > eval `aws ecr get-login --no-include-email // Tag the image > docker tag node-sample:latest <account_id>.dkr.ecr.<region>.amazonaws.com/node-sample:latest // Push the image > docker push <account_id>.dkr.ecr.<region>.amazonaws.com/node-sample:latest

4. Loadbalancer – Cloudformation

Now, before we create the ECS cluster where we can deploy our service, first let’s create a loadbalancer that will helo access the services.

// loadbalancer.yaml LoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Name: training-aws-lb Subnets: - Fn::ImportValue: !Join [":", [ !Ref EnvironmentName, "SubnetAZ1Public" ]] - Fn::ImportValue: !Join [ ":", [ !Ref EnvironmentName, "SubnetAZ2Public" ]] SecurityGroups: - !Ref LoadBalancerSecurityGroup

This loadbalancer uses a subnet which is already created and exported as !Join [":", [ !Ref EnvironmentName, "SubnetAZ1Public" ]]

For more info about VPC, security groups check https://aws.amazon.com/vpc/

Let’s also attach a security group to the loadbalancer

// loadbalancer.yaml LoadBalancerSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Security group for loadbalancer VpcId: Fn::ImportValue: !Join [ ":", [ !Ref EnvironmentName, "VPC" ]] SecurityGroupIngress: - CidrIp: 0.0.0.0/0 IpProtocol: -1

Here VpcId is an already exported VPC with output. You can use a default VPC here instead. !Join [ ":", [ !Ref EnvironmentName, "VPC" ]]

Now, let’s add a loadbalancer listener which can be used by the ECS:

LoadBalancerListener:

Type: AWS::ElasticLoadBalancingV2::Listener

Properties:

LoadBalancerArn: !Ref LoadBalancer

Protocol: HTTP

Port: 80

DefaultActions:

- Type: forward

TargetGroupArn: !Ref DefaultTargetGroup

DefaultTargetGroup:

Type: AWS::ElasticLoadBalancingV2::TargetGroup

Properties:

Name: aws-training-default

VpcId:

Fn::ImportValue:

!Join [ ":", [ !Ref EnvironmentName, "VPC" ]]

Protocol: HTTP

Port: 80

We need the loadbalancer security group, DNS and loadbalancer listener further in the next steps, so let’s add them to the output section.

// loadbalancer.yaml Outputs: LoadBalancerDNS: Description: Domain name for the loadbalancer Value: !GetAtt LoadBalancer.DNSName Export: Name: !Join [':', [ !Ref EnvironmentName, TrainingDomainName ]] LoadBalancerListener: Description: loadbalancer listener Value: !Ref LoadBalancerListener Export: Name: !Join [':', [ !Ref EnvironmentName, TrainingLoadBalancerListener ]] LoadBalancerSecurityGroup: Description: Loadbalancer security group Value: !Ref LoadBalancerSecurityGroup Export: Name: !Join [':', [ !Ref EnvironmentName, TrainingLoadBalancerSecurityGroup ]]

Check here to see how the final loadbalancer.yaml looks like.

Create the LoadBalancer using the above CloudFormation template with the following command:

> aws cloudformation create-stack --stack-name training-loadbalancer --template-body file://loadbalancer.yaml

5. ECS Cluster

The next step is now to create an ECS cluster. An ECS cluster is a grouping of containers. Let’s create a cluster for our node container:

// ecs.yaml AWSTrainingECSCluster: Type: AWS::ECS::Cluster Properties: ClusterName: AWSTrainingECSCluster

Cluster definition is very simple and that’s it.

6. ECS Service

Now with the cluster in place, let’s create a service inside this cluster where our node container will run. The LoadBalancer we created previously will now be mapped to this service:

// ecs.yaml AWSTrainingECSService: Type: AWS::ECS::Service DependsOn: WebListenerRule Properties: Cluster: !Ref AWSTrainingECSCluster DesiredCount: 1 LaunchType: FARGATE LoadBalancers: - ContainerName: AWSTraining ContainerPort: 8080 TargetGroupArn: !Ref WebTargetGroup NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: DISABLED Subnets: - Fn::ImportValue: !Sub ${EnvironmentName}:SubnetAZ1Private - Fn::ImportValue: !Sub ${EnvironmentName}:SubnetAZ2Private SecurityGroups: - !Ref ContainerSecurityGroup ServiceName: !Sub aws-training-${EnvironmentName} TaskDefinition: !Ref AWSTrainingTaskDefinition DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 50

Here we are using the launch type as FARGATE. There are two ways the instance can be launched in ECS FARGATE or EC2. If we use EC2 all the underlining management like autoscaling of the instance should be done by us, with FARGET that management is done by AWS.

The NetworkConfiguration option helps to set the subnets and security groups.

We can also specify the deployment configuration for the service. Here we have set a maximum percent to 200 and minimum healthy percent to 50 of the number of tasks we want to run inside the container while deploying. The expected number of tasks for the service can be set using DesiredCount property which in our case is 1.

7. ECS Task

ECS task is the actual place where the containers run. These tasks are present inside the ECS service. Let’s look into how we can create ECS tasks:

// ecs.yaml CloudWatchLogsGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: /aws/ecs/training RetentionInDays: 7 AWSTrainingTaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Cpu: "256" Memory: "512" ExecutionRoleArn: !Ref ECSTaskRole Family: aws-training NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ContainerDefinitions: - Name: AWSTraining # Replace image link with the docker image path along with the tag Image: # <image_link> PortMappings: - ContainerPort: 8080 LogConfiguration: LogDriver: awslogs Options: awslogs-group: !Ref CloudWatchLogsGroup awslogs-region: !Ref AWS::Region awslogs-stream-prefix: aws-training

Here we define the CPU size and memory of the instance where we run the container. We define the port which the task will be accessed from, the docker image location and the CloudWatch log configuration.

7. IAM roles for task

The task will be connecting to CloudWatch to write the logs there and also permission to fetch the image from the ECR registry, so we need to attach it to an IAM role with the permission to CloudWatch and ECR registry.

ECSTaskRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: [ecs-tasks.amazonaws.com] Action: ['sts:AssumeRole'] Path: / Policies: - PolicyName: AWSTrainingAmazonECSTaskExecutionRolePolicy PolicyDocument: Statement: - Effect: Allow Action: # ECS Tasks to download images from ECR - 'ecr:GetAuthorizationToken' - 'ecr:BatchCheckLayerAvailability' - 'ecr:GetDownloadUrlForLayer' - 'ecr:BatchGetImage' # ECS tasks to upload logs to CloudWatch - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: '*'

Create the cluster, server, tasks and related resources with the following command:

> aws cloudformation create-stack --stack-name training-ecs --template-body file://ecs.yaml

With this we are all set, now we can check the LoadBalancer URL and see the output from the node app “Hello World”!

In this blog post we learned to:

Create a hello world node app Containerize the node app with docker Push the docker image to ECR Creating a LoadBalancer for the ECS service Create an ECS cluster Create ECS service and task with IAM role and CloudWatch group

With this setup, we are ready for a production-grade Docker container deployment. You should now be able to deploy a docker container with ECS.

Learn more from other blogs:

A Brief Introduction to AWS CloudFormation

How does a CPU work?

Learning Golang – zero to hero