Deploy Docker Image with AWS ECS (Part 2)

In Part 1 we uploaded a Docker image to AWS ECR. In this post, we will complete building the ECS Cluster and deploy the container image onto the cluster.

Note: The lab I worked on was recreated. The container image was renamed from webfront to testweb.

Before we start, you need to understand some ECS basic concepts.

Task Definition

A task definition describes one or more containers, their relationships, how they should be launched etc. It’s basically a JSON file contains the configuration details of the container(s).

Task

A task is the instantiation of a task definition. They are created based on the task definitions you provided.

Service (Scheduler)

In short, the service or service scheduler controls the tasks running across the ECS cluster.

Cluster

Cluster is the mothership of those tasks. It hosts the containers launched from the tasks.

Container Instance

By now, AWS ECS offers two types of ECS Cluster: Fargate and EC2. With Fargate, AWS manages the cluster resources for you. With EC2, ECS provisions EC2 instances based on your specification and you are in charge of maintaining those EC2 instances. Those EC2 instances are “Container Instances” as they are the ones host containers.

Container Agent

For a EC2 ECS cluster, container agent runs on each Container Instance. It is the key component that controls ECS tasks and resource utilization.

To learn more about ECS basic concepts, you can refer to this AWS document.

1. Create Task Definition

The first step we take is to create a new Task Definition.

Choose EC2 as the launch type.

Name the Task Definition as testweb-task.

Leave Network Mode as , which will be Bridged for Linux instance.

Set Task size as below.

image

Click Add container to add the container image from ECR.

Name the container as testweb.

Under Image, put in the ECR repo URL with tag, below is an example.

1234567891011.dkr.ecr.ap-southeast-1.amazonaws.com/testweb:latest

For memory limits, set hard limit as “512” and soft limit as “256”.

Set Port mappings as 80 to 80 tcp.

Click Add to add the container configuration.

Click Create to create the Task Definition.

The Task Definition can also be created with JSON. Below is the code.

{  
  "executionRoleArn": null,  
  "containerDefinitions": \[  
    {  
      "dnsSearchDomains": null,  
      "logConfiguration": null,  
      "entryPoint": null,  
      "portMappings": \[  
        {  
          "hostPort": 80,  
          "protocol": "tcp",  
          "containerPort": 80  
        }  
      \],  
      "command": null,  
      "linuxParameters": null,  
      "cpu": 512,  
      "environment": \[\],  
      "ulimits": null,  
      "dnsServers": null,  
      "mountPoints": \[\],  
      "workingDirectory": null,  
      "dockerSecurityOptions": null,  
      "memory": 512,  
      "memoryReservation": 256,  
      "volumesFrom": \[\],  
      "image": "12345678910.dkr.ecr.ap-southeast-1.amazonaws.com/testweb:latest",  
      "disableNetworking": null,  
      "interactive": null,  
      "healthCheck": null,  
      "essential": true,  
      "links": null,  
      "hostname": null,  
      "extraHosts": null,  
      "pseudoTerminal": null,  
      "user": null,  
      "readonlyRootFilesystem": null,  
      "dockerLabels": null,  
      "systemControls": null,  
      "privileged": null,  
      "name": "testweb"  
    }  
  \],  
  "placementConstraints": \[\],  
  "memory": "512",  
  "taskRoleArn": null,  
  "compatibilities": \[  
    "EC2"  
  \],  
  "taskDefinitionArn": "arn:aws:ecs:ap-southeast-1:297012811963:task-definition/testweb-task:1",  
  "family": "testweb-task",  
  "requiresAttributes": \[  
    {  
      "targetId": null,  
      "targetType": null,  
      "value": null,  
      "name": "com.amazonaws.ecs.capability.ecr-auth"  
    },  
    {  
      "targetId": null,  
      "targetType": null,  
      "value": null,  
      "name": "com.amazonaws.ecs.capability.docker-remote-api.1.21"  
    }  
  \],  
  "requiresCompatibilities": \[  
    "EC2"  
  \],  
  "networkMode": null,  
  "cpu": "1024",  
  "revision": 1,  
  "status": "ACTIVE",  
  "volumes": \[\]  
}  

2. Create ECS Cluster

Under Amazon ECS click Clusters and click Create Cluster.

Select EC2 Linux + Networking and click Next step.

Name the cluster. I named it as testweb-clu, which is a bad name, you never want to name your cluster with a single image name, as the container suppose to present a micro service of your application.

Choose the EC2 instance type. For testing we will use t2.micro and 1 for Number of instances. Select a keypair so we can log into the Container Instances for some troubleshooting if needed.

image

Under Networking, Create a new VPC, along with two new subnets.

Create a new security group and add rule to allow public access to TCP port 80.

image

To allow the EC2 Container Instance to access ECS, we ill need to assign an IAM role to it. The IAM role contains 2 Amazon policies.

image

Click Create to create the Cluster. Interestingly, you can see the cluster provision is actually done through a CloudFormation template.

image

From the CloudFormation stack details, we can see all the script did was to provision a Launch Configuration and a AutoScaling Group.

image

Here is the CloudFormation Template code.

AWSTemplateFormatVersion: '2010-09-09'  
Description: \>  
  AWS CloudFormation template to create a new VPC  
  or use an existing VPC for ECS deployment  
  in Create Cluster Wizard. Requires exactly 1  
  Instance Types for a Spot Request.  
Parameters:  
  EcsClusterName:  
    Type: String  
    Description: \>  
      Specifies the ECS Cluster Name with which the resources would be  
      associated  
    Default: default  
  EcsAmiId:  
    Type: String  
    Description: Specifies the AMI ID for your container instances.  
  EcsInstanceType:  
    Type: CommaDelimitedList  
    Description: \>  
      Specifies the EC2 instance type for your container instances.  
      Defaults to m4.large  
    Default: m4.large  
    ConstraintDescription: must be a valid EC2 instance type.  
  KeyName:  
    Type: String  
    Description: \>  
      Optional - Specifies the name of an existing Amazon EC2 key pair  
      to enable SSH access to the EC2 instances in your cluster.  
    Default: ''  
  VpcId:  
    Type: String  
    Description: \>  
      Optional - Specifies the ID of an existing VPC in which to launch  
      your container instances. If you specify a VPC ID, you must specify a list of  
      existing subnets in that VPC. If you do not specify a VPC ID, a new VPC is created  
      with atleast 1 subnet.  
    Default: ''  
    ConstraintDescription: \>  
      VPC Id must begin with 'vpc-' or leave blank to have a  
      new VPC created  
  SubnetIds:  
    Type: CommaDelimitedList  
    Description: \>  
      Optional - Specifies the Comma separated list of existing VPC Subnet  
      Ids where ECS instances will run  
    Default: ''  
  SecurityGroupId:  
    Type: String  
    Description: \>  
      Optional - Specifies the Security Group Id of an existing Security  
      Group. Leave blank to have a new Security Group created  
    Default: ''  
  VpcCidr:  
    Type: String  
    Description: Optional - Specifies the CIDR Block of VPC  
    Default: ''  
  SubnetCidr1:  
    Type: String  
    Description: Specifies the CIDR Block of Subnet 1  
    Default: ''  
  SubnetCidr2:  
    Type: String  
    Description: Specifies the CIDR Block of Subnet 2  
    Default: ''  
  SubnetCidr3:  
    Type: String  
    Description: Specifies the CIDR Block of Subnet 3  
    Default: ''  
  AsgMaxSize:  
    Type: Number  
    Description: \>  
      Specifies the number of instances to launch and register to the cluster.  
      Defaults to 1.  
    Default: '1'  
  IamRoleInstanceProfile:  
    Type: String  
    Description: \>  
      Specifies the Name or the Amazon Resource Name (ARN) of the instance  
      profile associated with the IAM role for the instance  
  SecurityIngressFromPort:  
    Type: Number  
    Description: \>  
      Optional - Specifies the Start of Security Group port to open on  
      ECS instances - defaults to port 0  
    Default: '0'  
  SecurityIngressToPort:  
    Type: Number  
    Description: \>  
      Optional - Specifies the End of Security Group port to open on ECS  
      instances - defaults to port 65535  
    Default: '65535'  
  SecurityIngressCidrIp:  
    Type: String  
    Description: \>  
      Optional - Specifies the CIDR/IP range for Security Ports - defaults  
      to 0.0.0.0/0  
    Default: 0.0.0.0/0  
  EcsEndpoint:  
    Type: String  
    Description: \>  
      Optional - Specifies the ECS Endpoint for the ECS Agent to connect to  
    Default: ''  
  VpcAvailabilityZones:  
    Type: CommaDelimitedList  
    Description: \>  
      Specifies a comma-separated list of 3 VPC Availability Zones for  
      the creation of new subnets. These zones must have the available status.  
    Default: ''  
  EbsVolumeSize:  
    Type: Number  
    Description: \>  
      Optional - Specifies the Size in GBs, of the newly created Amazon  
      Elastic Block Store (Amazon EBS) volume  
    Default: '0'  
  EbsVolumeType:  
    Type: String  
    Description: Optional - Specifies the Type of (Amazon EBS) volume  
    Default: ''  
    AllowedValues:  
      \- ''  
      \- standard  
      \- io1  
      \- gp2  
      \- sc1  
      \- st1  
    ConstraintDescription: Must be a valid EC2 volume type.  
  DeviceName:  
    Type: String  
    Description: Optional - Specifies the device mapping for the Volume  
  UseSpot:  
    Type: String  
    Default: 'false'  
  IamSpotFleetRoleArn:  
    Type: String  
    Default: ''  
  SpotPrice:  
    Type: String  
    Default: ''  
  SpotAllocationStrategy:  
    Type: String  
    Default: 'diversified'  
    AllowedValues:  
      \- 'lowestPrice'  
      \- 'diversified'  
  UserData:  
    Type: String  
  IsWindows:  
    Type: String  
    Default: 'false'  
Conditions:  
  CreateEC2LCWithKeyPair:  
    !Not \[!Equals \[!Ref KeyName, ''\]\]  
  SetEndpointToECSAgent:  
    !Not \[!Equals \[!Ref EcsEndpoint, ''\]\]  
  CreateNewSecurityGroup:  
    !Equals \[!Ref SecurityGroupId, ''\]  
  CreateNewVpc:  
    !Equals \[!Ref VpcId, ''\]  
  CreateSubnet1: !And  
    \- !Not \[!Equals \[!Ref SubnetCidr1, ''\]\]  
    \- !Condition CreateNewVpc  
  CreateSubnet2: !And  
    \- !Not \[!Equals \[!Ref SubnetCidr2, ''\]\]  
    \- !Condition CreateSubnet1  
  CreateSubnet3: !And  
    \- !Not \[!Equals \[!Ref SubnetCidr3, ''\]\]  
    \- !Condition CreateSubnet2  
  CreateWithSpot: !Equals \[!Ref UseSpot, 'true'\]  
  CreateWithASG: !Not \[!Condition CreateWithSpot\]  
  CreateWithSpotPrice: !Not \[!Equals \[!Ref SpotPrice, ''\]\]  
Resources:  
  Vpc:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::VPC  
    Properties:  
      CidrBlock: !Ref VpcCidr  
      EnableDnsSupport: 'true'  
      EnableDnsHostnames: 'true'  
  PubSubnetAz1:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::Subnet  
    Properties:  
      VpcId: !Ref Vpc  
      CidrBlock: !Ref SubnetCidr1  
      AvailabilityZone: !Select \[ 0, !Ref VpcAvailabilityZones \]  
      MapPublicIpOnLaunch: true  
  PubSubnetAz2:  
    Condition: CreateSubnet2  
    Type: AWS::EC2::Subnet  
    Properties:  
      VpcId: !Ref Vpc  
      CidrBlock: !Ref SubnetCidr2  
      AvailabilityZone: !Select \[ 1, !Ref VpcAvailabilityZones \]  
      MapPublicIpOnLaunch: true  
  PubSubnetAz3:  
    Condition: CreateSubnet3  
    Type: AWS::EC2::Subnet  
    Properties:  
      VpcId: !Ref Vpc  
      CidrBlock: !Ref SubnetCidr3  
      AvailabilityZone: !Select \[ 2, !Ref VpcAvailabilityZones \]  
      MapPublicIpOnLaunch: true  
  InternetGateway:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::InternetGateway  
  AttachGateway:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::VPCGatewayAttachment  
    Properties:  
      VpcId: !Ref Vpc  
      InternetGatewayId: !Ref InternetGateway  
  RouteViaIgw:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::RouteTable  
    Properties:  
      VpcId: !Ref Vpc  
  PublicRouteViaIgw:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::Route  
    DependsOn: AttachGateway  
    Properties:  
      RouteTableId: !Ref RouteViaIgw  
      DestinationCidrBlock: 0.0.0.0/0  
      GatewayId: !Ref InternetGateway  
  PubSubnet1RouteTableAssociation:  
    Condition: CreateSubnet1  
    Type: AWS::EC2::SubnetRouteTableAssociation  
    Properties:  
      SubnetId: !Ref PubSubnetAz1  
      RouteTableId: !Ref RouteViaIgw  
  PubSubnet2RouteTableAssociation:  
    Condition: CreateSubnet2  
    Type: AWS::EC2::SubnetRouteTableAssociation  
    Properties:  
      SubnetId: !Ref PubSubnetAz2  
      RouteTableId: !Ref RouteViaIgw  
  PubSubnet3RouteTableAssociation:  
    Condition: CreateSubnet3  
    Type: AWS::EC2::SubnetRouteTableAssociation  
    Properties:  
      SubnetId: !Ref PubSubnetAz3  
      RouteTableId: !Ref RouteViaIgw  
  EcsSecurityGroup:  
    Condition: CreateNewSecurityGroup  
    Type: AWS::EC2::SecurityGroup  
    Properties:  
      GroupDescription: ECS Allowed Ports  
      VpcId: !If \[ CreateSubnet1, !Ref Vpc, !Ref VpcId \]  
      SecurityGroupIngress:  
        IpProtocol: tcp  
        FromPort: !Ref SecurityIngressFromPort  
        ToPort: !Ref SecurityIngressToPort  
        CidrIp: !Ref SecurityIngressCidrIp  
  EcsInstanceLc:  
    Type: AWS::AutoScaling::LaunchConfiguration  
    Condition: CreateWithASG  
    Properties:  
      ImageId: !Ref EcsAmiId  
      InstanceType: !Select \[ 0, !Ref EcsInstanceType \]  
      AssociatePublicIpAddress: true  
      IamInstanceProfile: !Ref IamRoleInstanceProfile  
      KeyName: !If \[ CreateEC2LCWithKeyPair, !Ref KeyName, !Ref "AWS::NoValue" \]  
      SecurityGroups: \[ !If \[ CreateNewSecurityGroup, !Ref EcsSecurityGroup, !Ref SecurityGroupId \] \]  
      BlockDeviceMappings:  
      \- DeviceName: !Ref DeviceName  
        Ebs:  
         VolumeSize: !Ref EbsVolumeSize  
         VolumeType: !Ref EbsVolumeType  
      UserData:  
        Fn::Base64: !Ref UserData  
  EcsInstanceAsg:  
    Type: AWS::AutoScaling::AutoScalingGroup  
    Condition: CreateWithASG  
    Properties:  
      VPCZoneIdentifier: !If  
        \- CreateSubnet1  
        \- !If  
          \- CreateSubnet2  
          \- !If  
            \- CreateSubnet3  
            \- \[ !Sub "${PubSubnetAz1},  ${PubSubnetAz2},  ${PubSubnetAz3}" \]  
            \- \[ !Sub "${PubSubnetAz1},  ${PubSubnetAz2}" \]  
          \- \[ !Sub "${PubSubnetAz1}" \]  
        \- !Ref SubnetIds  
      LaunchConfigurationName: !Ref EcsInstanceLc  
      MinSize: '0'  
      MaxSize: !Ref AsgMaxSize  
      DesiredCapacity: !Ref AsgMaxSize  
      Tags:  
        \-  
          Key: Name  
          Value: !Sub "ECS  Instance  \-  ${AWS::StackName}"  
          PropagateAtLaunch: 'true'  
        \-  
          Key: Description  
          Value: "This  instance  is  the  part  of  the  Auto  Scaling  group  which  was  created  through  ECS  Console"  
          PropagateAtLaunch: 'true'  
  EcsSpotFleet:  
    Condition: CreateWithSpot  
    Type: AWS::EC2::SpotFleet  
    Properties:  
      SpotFleetRequestConfigData:  
        AllocationStrategy: !Ref SpotAllocationStrategy  
        IamFleetRole: !Ref IamSpotFleetRoleArn  
        TargetCapacity: !Ref AsgMaxSize  
        SpotPrice: !If \[ CreateWithSpotPrice, !Ref SpotPrice, !Ref 'AWS::NoValue' \]  
        TerminateInstancesWithExpiration: true  
        LaunchSpecifications:   
            \-  
              IamInstanceProfile:  
                Arn: !Ref IamRoleInstanceProfile  
              ImageId: !Ref EcsAmiId  
              InstanceType: !Select \[ 0, !Ref EcsInstanceType \]  
              KeyName: !If \[ CreateEC2LCWithKeyPair, !Ref KeyName, !Ref "AWS::NoValue" \]  
              Monitoring:  
                Enabled: true  
              SecurityGroups:  
                \- GroupId: !If \[ CreateNewSecurityGroup, !Ref EcsSecurityGroup, !Ref SecurityGroupId \]  
              SubnetId: !If  
                      \- CreateSubnet1  
                      \- !If  
                        \- CreateSubnet2  
                        \- !If  
                          \- CreateSubnet3  
                          \- !Join \[ "," , \[ !Ref PubSubnetAz1, !Ref PubSubnetAz2, !Ref PubSubnetAz3 \] \]  
                          \- !Join \[ "," , \[ !Ref PubSubnetAz1, !Ref PubSubnetAz2 \] \]  
                        \- !Ref PubSubnetAz1  
                      \- !Join \[ "," , !Ref SubnetIds \]  
              BlockDeviceMappings:  
                    \- DeviceName: !Ref DeviceName  
                      Ebs:  
                       VolumeSize: !Ref EbsVolumeSize  
                       VolumeType: !Ref EbsVolumeType  
              UserData:  
                    Fn::Base64: !Ref UserData  
Outputs:  
  EcsInstanceAsgName:  
    Condition: CreateWithASG  
    Description: Auto Scaling Group Name for ECS Instances  
    Value: !Ref EcsInstanceAsg  
  EcsSpotFleetRequestId:  
      Condition: CreateWithSpot  
      Description: Spot Fleet Request for ECS Instances  
      Value: !Ref EcsSpotFleet  
  UsedByECSCreateCluster:  
    Description: Flag used by ECS Create Cluster Wizard  
    Value: 'true'  
  TemplateVersion:  
    Description: The version of the template used by Create Cluster Wizard  
    Value: '2.0.0'  

Here is the parameters for the template

AsgMaxSize: 1

DeviceName: /dev/xvdcz

EbsVolumeSize: 22

EbsVolumeType: gp2

EcsAmiId: ami-0a3f70f0111af1d29

EcsClusterName: testweb-clu

EcsEndpoint:

EcsInstanceType: t2.micro

IamRoleInstanceProfile: arn:aws:iam::123456789103:instance-profile/ecsInstanceRole

IamSpotFleetRoleArn:

IsWindows: false

KeyName: tom-testkey

SecurityGroupId: sg-014e7a96be118e111

SecurityIngressCidrIp: 0.0.0.0/0

SecurityIngressFromPort: 80

SecurityIngressToPort: 80

SpotAllocationStrategy: diversified

SpotPrice:

SubnetCidr1: 10.0.0.0/24

SubnetCidr2: 10.0.1.0/24

SubnetCidr3:

SubnetIds: subnet-057b8e2cd11183e28

UserData: #!/bin/bash echo ECS_CLUSTER=testweb-clu » /etc/ecs/ecs.config;echo ECS_BACKEND_HOST= » /etc/ecs/ecs.config;

UseSpot: false

VpcAvailabilityZones: ap-southeast-1a,ap-southeast-1b,ap-southeast-1c

VpcCidr: 10.0.0.0/16

VpcId: vpc-08d111d48c2f68111

3. Create Service

Back in ECS, click the Cluster name.

Under Services tab, click Create.

Choose EC2 as the Launch Type, select the Task Definition we created in step1, select the cluster we created in step 2. Set Number of tasks to 1.

Use AZ Balanced Spread for Task Placement.

image

Leave everything unchanged in Configure network.

Leave AutoScaling as “Do not adjust…”.

Click Create Service to create the service.

4. Test

Once the service is successfully created, The cluster will then start to provision the Container Instance and deploy the container image onto it. This process does seem to take time. In my case, it took around 55 minutes for the container to be started! So you definitely want to warm up your cluster in production.

image

Under Tasks, you should see a running task there.

image

Go to EC2 and find the public IP of the Container Instance.

image

In browser, type http://publicIP and you should see the boring but exciting Hello World message!

image

In case you run into any issues, like the task refuse to launch, one place to look is the ECS Agent log on the Container Instance. The log can be found at /var/logs.