Automate Docker container deployment to AWS ECS using CloudFormation

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:

  1. EC2: you are responsible for provisioning the EC2 instances on which your tasks will run.
  2. 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.