Deploying a Docker container on Amazon ECS can be straightforward, but getting permissions and traffic routed correctly can be challenging.

In this post, we will see how we can deploy a web application to ECS using Fargate. This is so we don’t have to worry about provisioning or maintaining EC2 instances. More importantly, we will see how we can set up the necessary permissions and route traffic to a highly available Microservice using an Application Load Balancer.

Amazon ECS

Amazon Elastic Container Service (Amazon ECS) is a fully managed container orchestration service that makes it easy for you to deploy, manage, and scale containerized applications. Fargate removes the need to manage the lifecycle of compute infrastructure and can be used to run containers without having to provision and manage EC2 instances.

Web Application

First, we need to create a Docker image for use on Amazon ECS.

We will use a Spring Boot Application and write a simple Dockerfile that uses a production-ready distribution of OpenJDK to run our application as an executable jar. Spring Boot uses an embedded servlet container accepting HTTP traffic on port 8080 by default.

Dockerfile
1
2
3
4
FROM amazoncorretto:11-alpine-jdk
MAINTAINER michalisdaniilakis.com
COPY build/libs/app.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]

During image build, our jar gets copied from the build directory to the image.

./gradlew clean test bootJar && docker build -t spring-boot-docker .

ECR repository

Amazon Elastic Container Registry (Amazon ECR) is an AWS managed container image registry service. Using the AWS CLI, we can authenticate our Docker client to the Amazon ECR registry to which we will push our image.

Create repository

Using the AWS Console, we will first create a new repository and enable Tag immutability to avoid image tags from being overwritten.

Once the repository is created, we can use the AWS CLI to authenticate to Docker.

aws ecr get-login-password --region REGION | docker login --username AWS --password-stdin ACCOUNT.dkr.ecr.REGION.amazonaws.com

Replace REGION and ACCOUNT with your AWS region and account id.

Push image to repository

We have created a new ECR repository and authenticated to Docker. We can now tag and push our image with the following commands.

docker tag spring-boot-docker:latest ACCOUNT.dkr.ecr.REGION.amazonaws.com/spring-boot-docker:1.0.0
docker push ACCOUNT.dkr.ecr.REGION.amazonaws.com/spring-boot-docker:1.0.0

Replace REGION and ACCOUNT with your AWS region and account id.

Fargate service

We will use CloudFormation to deploy our container to AWS Fargate as a service. This allows us to describe all of our infrastructure as code that can be kept alongside our project.

AWS CloudFormation lets you model, provision, and manage AWS and third-party resources by treating infrastructure as code.

Deploying a highly available service that can scale based on resource utilization can be achieved by placing our service in multiple AZs and using an autoscaling policy (e.g. to keep CPU utilization below a certain threshold). An Application Load Balancer is used to route HTTP traffic to our service. SSL is terminated at the load balancer, and security groups are created for the container and load balancer.

There are many examples online on how to structure all of our AWS resources and deploy them using CloudFormation. I found this CloudFormation template easy to use as it defines all resources in one template, but this can also be achieved by splitting them in multiple templates for reusability.

Resources

There are multiple AWS resources that need to be created in order for our service to be operational. We will need a new ECS cluster; the following is just an example CloudFormation template in YAML.

Resources:
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: My ECS cluster

ECS Service

Next, we define our ECS Service that is responsible for running and maintaining the requested number of tasks. The following is just an example.

Resources:
  ...
  Service:
    Type: AWS::ECS::Service
    Properties:
      ServiceName: My ECS service
      Cluster: !Ref Cluster
      TaskDefinition: !Ref TaskDefinition
      ...
      DesiredCount: 2
      # This may need to be adjusted if the container takes a while to start up
      HealthCheckGracePeriodSeconds: 60
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          # change to DISABLED if you're using private subnets that have access to a NAT gateway
          AssignPublicIp: ENABLED
          Subnets:
            - !Ref SubnetA
            - !Ref SubnetB
          SecurityGroups:
            - !Ref ContainerSecurityGroup

The ECS Service network configuration defines the container subnets and security groups. Tasks running in our ECS Service are placed in 2 different subnets for increased availability. This means that if one AZ is out of service, traffic will be automatically redirected to the tasks running in the healthy AZ. We can of course specify more subnets depending on the regional VPC and subnet configuration on AWS.

Container security group

The container security group allows traffic on port 8080 only if the source has the LoadBalancerSecurityGroup attached. This blocks inbound traffic to our container except from the Load Balancer.

Resources:
  ...
  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allows traffic on port 8080 only if the source has the LoadBalancerSecurityGroup attached.
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup

Load Balancer

Our internet-facing Load Balancer accepts and forwards traffic to our container.

Resources:
  ...
  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        # this is the default, but is specified here in case it needs to be changed
        - Key: idle_timeout.timeout_seconds
          Value: 60
      Name: My ECS Service Load Balancer
      # "internal" is also an option
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Subnets:
        - !Ref SubnetA
        - !Ref SubnetB

The Load Balancer security group is configured to accept HTTPS traffic from anywhere. An HTTPS listener and target group are also needed to forward traffic to our container. The listener requires a valid SSL certificate. In this example, this is not shown as the Certificate may be issued from any provider and, later imported to ACM (AWS Certificate Manager).

Resources:
  ...
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allows HTTPS traffic from anywhere.
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0

We have seen examples of how to create an ECS Cluster, ECS Service using Fargate, Load Balancer, and security groups.

Deploying the service

In order to deploy our CloudFormation template, we need:

  • A domain name and hosted zone in Route53
  • A VPC (with 2 or more subnets)
  • An SSL certificate

Let’s put everything together in a CloudFormation template and use the AWS CLI to deploy our service. You will have to edit the deploy-params.json file and add all required parameters (vpc, subnets, certificate, etc).

aws cloudformation deploy \
  --template-file service.yml \
  --stack-name spring-boot-docker \
  --parameter-overrides file://deploy-params.json \
  --capabilities CAPABILITY_NAMED_IAM

All of our infrastructure should be provisioned within minutes, and we should have access to the Service using the A record created in Route53.

You can find the complete source of this guide on GitHub.

eof.