Automate Docker container deployment to AWS ECS using CloudFormation
Tags: aws, docker
Published: 2020-05-11 13:59:19

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](https://hub.docker.com/_/nginx) 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.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/vmcxlbich656u0vshua6.png)

**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](https://plugins.jetbrains.com/plugin/7371-aws-cloudformation/) 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*:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cclv7x625dufd6d1ycui.png)

###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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qzjgi44nvm1v316sbw08.png)

#Getting a handle on our ECS resources#

Head on over to *ECS > Services* and we'll check out what's been created. 🔍

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/08k7fsch74gxls3iewpo.PNG)

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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/kceonmax45fj4jbwhymv.PNG)

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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/qbky1c8ujf7dsvmh7lsr.PNG)

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:

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zec107g1lirqq64nnrqj.png)

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.