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
Set Task size as below.
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.
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.
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.
Click Create to create the Cluster. Interestingly, you can see the cluster provision is actually done through a CloudFormation template.
From the CloudFormation stack details, we can see all the script did was to provision a Launch Configuration and a AutoScaling Group.
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.
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.
Under Tasks, you should see a running task there.
Go to EC2 and find the public IP of the Container Instance.
In browser, type http://publicIP and you should see the boring but exciting Hello World message!
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.