How to deploy a docker container with AWS ECS using cloudformation


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.

AWS ECS
AWS ECS architecture

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:

  1. Build a hello world express node app

    Build a simple hello world express appnodejs

  2. Containerize the app using docker

    Write a Docker file to containerize the app

  3. Push the docker image to amazon container registry ECR

    Use a container registry where the docker image can be stored

  4. Build a loadbalancer

    A loadbalancer to access the app from the internet

  5. Deploy the node app to an ECS cluster

    ECS cluster is the place where you build service and deploy the container to its tasks

  6. 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://resources.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:

  1. Create a hello world node app
  2. Containerize the node app with docker
  3. Push the docker image to ECR
  4. Creating a LoadBalancer for the ECS service
  5. Create an ECS cluster
  6. 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

Leave a Reply

Your email address will not be published. Required fields are marked *