Automate Docker container deployment to AWS ECS using CloudFormation
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. ✔️
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
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.
AWS::ECS::Cluster resource requires no configuration other than a name.
The ECS task will log the application logs to this log group.
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.
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
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)
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!
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.