Deploying Docker containers to AWS Elastic Container Service (ECS) is straightforward and automated when you make use of CloudFormation to define your infrastructure in a YAML template. Here we'll be running through a simple example where we'll setup everything required to run an NGINX container in AWS and access it over the internet.
AWS ECS overview
We've chosen to run the NGINX official Docker image as it will allow us to browse to port 80 and view the response to prove the container is running. To get this deployed into ECS, we'll need the following buildings blocks:
- ECS Task Definition: a specification of your container, including what Docker image to use, what ports to expose, and what hardware resources to allocate
- ECS Task: a running instance of the ECS Task Definition. Equivalent to a running Docker container.
- ECS Service: responsible for running instances of your task definition, including how many to deploy, networking, and security
- ECS Cluster: a grouping of ECS services and tasks
- ECS Task Execution role: an IAM role which the task will assume, in our case allowing log events to be written to CloudWatch
- Security Group: a security group can be attached to an ECS Service. We will use it to define rules to allow access into the container on port 80.
AWS ECS Launch Types
ECS tasks can be run in 2 modes, depending on your requirements:
- EC2: you are responsible for provisioning the EC2 instances on which your tasks will run.
- Fargate: AWS will provision the hardware on which your tasks will run. All you need to do is specify the memory and CPU requirements. Note that Fargate currently only supports nonpersistent storage volumes.
We'll be using the Fargate launch type in this example as it's the quickest way to get started. ✔️
Prerequisites
To keep this example as simple as possible, we're going to assume you already have the following setup:
- an AWS account with AWS CLI access setup
- a default VPC (AWS creates this by default when you create an AWS account)
Building the ECS using CloudFormation
We're going to use the YAML flavour of CloudFormation, and build up a stack piece by piece until we have an NGINX container running which we can access over the internet.
I recommend IntelliJ IDEA for editing CloudFormation templates, as it has a plugin which will provide syntax validation.
Creating the ECS cluster, log group, execution role, and security group
Start off by creating a file ecs.yml, and adding the following definitions:
AWSTemplateFormatVersion: "2010-09-09"
Parameters:
SubnetID:
Type: String
Resources:
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: deployment-example-cluster
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: deployment-example-log-group
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: deployment-example-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupName: ContainerSecurityGroup
GroupDescription: Security group for NGINX container
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: 0.0.0.0/0
Parameters
Our template takes only one parameter, SubnetID, to specify which subnet to deploy the ECS Task into. On a normal production setup, you'll want to deploy to multiple subnets across availability zones for high availability.
Cluster
The AWS::ECS::Cluster
resource requires no configuration other than a name.
Log group
The ECS task will log the application logs to this log group.
Execution role
This is the role that will be assumed by the ECS Task during execution. As such, it needs the provided assume role policy document, which allows ECS Tasks to assume this role.
It also has attached the AmazonECSTaskExecutionRolePolicy which contains the logs:CreateLogStream and logs:PutLogEvents actions, amongst others.
Security group
The security group defines what network traffic will be allowed access to the ECS Task. In our case, we just need to access port 80, the default NGINX port.
Let's apply this template with the following AWS CLI command, which creates a CloudFormation stack provisioning the above resources. Remember to replace <subnet-id> with your own subnet.
$ aws cloudformation create-stack --stack-name example-deployment --template-body file://./ecs.yml --capabilities CAPABILITY_NAMED_IAM --parameters 'ParameterKey=SubnetID,ParameterValue=<subnet-id>'
Eventually you'll see that the following resources have been created if you navigate in the AWS Console to CloudFormation > Stacks> example-deployment > Resources:
Creating the task definition and service
Add the following definition to the end of your ecs.yml CloudFormation template:
//previous template code
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: deployment-example-task
Cpu: 256
Memory: 512
NetworkMode: awsvpc
ExecutionRoleArn: !Ref ExecutionRole
ContainerDefinitions:
- Name: deployment-example-container
Image: nginx:1.17.7
PortMappings:
- ContainerPort: 80
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Ref AWS::Region
awslogs-group: !Ref LogGroup
awslogs-stream-prefix: ecs
RequiresCompatibilities:
- EC2
- FARGATE
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: deployment-example-service
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: 1
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: ENABLED
Subnets:
- !Ref SubnetID
SecurityGroups:
- !GetAtt ContainerSecurityGroup.GroupId
Task definition
We're defining an AWS::ECS::TaskDefinition
with the following important properties:
- the family is a way to group different versions of the same task definition
- we're specifying how much hardware resources to dedicate to this task
- we're using network mode awsvpc which is required for the Fargate launch type. This network mode means that our task will have the same networking capabilities as an EC2 instance, such as it's own IP address.
- the execution role which we defined earlier
- a container definition, specifying the image, the container port, and the logging configuration to tell it to log using the awslogs log driver (i.e to CloudWatch)
- we specify that this task definition is compatible with both the EC2 and Fargate launch types (although we'll be using Fargate)
Service
We're defining an AWS::ECS::Service
with the following properties:
- the ECS cluster into which this service will deploy tasks
- the task definition to be deployed
- the number of instances to run. For this simple example, we'll run 1, but for high availability, you'll want to run at least 2.
- the launch type of Fargate so we don't have to worry about provisioning hardware
- a network configuration which specifies the fact that we want a public IP address, the subnet to use for the service, and the security group to apply
Let's update the CloudFormation stack now with an update-stack command:
$ aws cloudformation update-stack --stack-name example-deployment --template-body file://./ecs.yml --capabilities CAPABILITY_NAMED_IAM --parameters 'ParameterKey=SubnetID,ParameterValue=<subnet-id>'
Wait a few moments, then you can see that some more resources have been created in our CloudFormation stack:
Getting a handle on our ECS resources
Head on over to ECS > Services and we'll check out what's been created. 🔍
You'll see the deployment-example-cluster
which importantly has 1 service and 1 running task:
Click on the cluster, then click on the Tasks tab:
Here you can see we're using the task definition we defined in the CloudFormation, the task status is running, and the launch type is Fargate.
Click on the task id for more details. Here's the Network section of the details page:
You can see here we've been provided with the public IP address of the task. Go ahead and try hitting that IP in your browser:
Looks like we got ourselves an NGINX!
Final words
To cleanup, just run the delete-stack command:
$ aws cloudformation delete-stack --stack-name example-deployment
Hopefully you've seen that it's straightforward to run Docker containers in ECS, and that AWS provides plenty of configuration options to have things working exactly as you like.
With CloudFormation, making incremental changes is straightforward, and it's a good option for managing an ECS Cluster.
Top comments (1)
Nice tutorial, Dmitriy. This helped me.
"With CloudFormation, making incremental changes is straightforward, and it's a good option for managing an ECS Cluster. "
How would I make this "incremental change", like actually configuring the website? Wouldn't the container have to change? Or at least point to a static site populated from S3?
The article is long enough, so just a link would suffice.