Skip to content

Deployment

Jonathan Seitz edited this page Jul 1, 2019 · 5 revisions

Deployment Instructions

We'll be deploying our applications on AWS, using the following services:

  • ECS (Elastic Container Service) to run our containers
    • We'll use Fargate for a "serverless" deployment
  • RDS (Relational Database Service) for our database
    • Specifically the PostgreSQL-compatible Amazon Aurora db as it's what we already have running
  • ELB (Elastic Load Balancer) to manage traffic to our container

We'll also make use of Docker Hub for our container registry, and TravisCI to build our container images.

Process

Overview

The goal is automate as much as possible.

  1. Merge pull request to develop branch.
  2. Travis will run all checks (tsc, lint, test, coverage).
  3. If everything passes, Travis will build a new image, and push it to Docker Hub with a 'beta' tag.
  4. Fargate will launch the new container using a reference to the digest hash.
  5. After QA testing, we can merge develop into master.
  6. Repeat steps 2-4, tagging the container with 'latest' and using the new hash.
  7. Fargate redeploys the new production instance.

Fargate

Note: Fargate is not a standalone service from AWS -- it's a deployment type within the ECS service -- but for comprehension's sake, we'll refer to it as if it is.

Setting up a deployment on Fargate requires configuring multiple smaller services that work in concert. You'll need:

  • Two Security Groups
  • An Application Load Balancer
    • LB Target Group
    • LB Listener
  • A Log Group
  • A Cluster in which your containers will run
  • A Task Definition that configures how your containers will run
  • A Service that Actually runs your containers

You'll also need a VPC with at least two subnets, but we can use the defaults for those.

These resources can be created from the AWS console, but using CloudFormation makes it easier to handle those dependencies in a single stack.

CloudFormation

All CloudFormation examples are in YAML format, which is preferred as it allows for inline comments.

When setting up our CloudFormation template, we'll need to ask the users for a couple of variables up front. When using the console, these will be entered in a form with text/select fields:

AWSTemplateFormatVersion: 2010-09-09
Description: AWS CloudFormation Template for launching an app on fargate
Parameters:
  VPC:
    Type: AWS::EC2::VPC::Id
    Description: Choose the VPC in which the Load Balance will be created.
  Subnets:
    Description: Choose at least 2 subnets for the Application Load Balancer
    Type: List<AWS::EC2::Subnet::Id>
  ECSRoleARN:
    Type: String
    Description: The role for executing ECS
  DBImage:
    Type: String
    Description: Image of the Database
    Default: mongo
  APPImage:
    Type: String
    Description: Image of the APP
    Default: seascomputing/react-boilerplate@SHA......
Resources:
 # Additional templates follow

Security Groups

First, we'll create two security groups. One will allow all traffic to our Load Balancer from the public Internet, and the other will allow the load balancer to communicate with our containers.

  LBtoInternet:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${AWS::StackName}_LB_TO_INTERNET
      GroupDescription: Internet access to 80 and 443
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
  ECStoLB:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupName: !Sub ${AWS::StackName}_ECS_TO_LOADBALANCER
      GroupDescription: Allow Load Balancer to reach containers
      VpcId: !Ref VPC
      SecurityGroupIngress:
        - IpProtocol: -1
          FromPort: -1
          ToPort: -1
          SourceSecurityGroupId: !GetAtt LBtoInternet.GroupId

The SourceSecurityGroupId in ECStoLB uses a Reference to the previous Security Groups to allow traffic between them

Application Load Balancer

We'll use an Application Load Balancer to distribute traffic from the public Internet to our containers running in our private VPC.

  LoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      Name: !Sub ${AWS::StackName}-LB
      Subnets: !Ref Subnets
      SecurityGroups:
        - !Ref LBtoInternet
      Tags:
        - Key: Name
          Value: !Ref AWS::StackName
      Type: application

Target Group

The Target Group defines the collection of containers (or other resources) that will receive traffic from the load balancer.

  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      Name: !Sub ${AWS::StackName}-TG-1
      VpcId: !Ref VPC
      Port: 3000
      Protocol: HTTP
      TargetType: ip
      Matcher:
        HttpCode: 200-399

Load Balancer Listener

The listener connects the target group and load balancer, using references to the previously created resources.

  LoadBalancerListener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      LoadBalancerArn: !Ref LoadBalancer
      Port: 80
      Protocol: HTTP
      DefaultActions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup

Logging

To connect the stdout from our containers to CloudWatch, we'll define a Log Group.

  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub ${AWS::StackName}_LOGS

Cluster

Clusters are a way to logically group a set of running containers. This is particularly useful when launching services with the EC2 launch type because it offers additional options for scaling instances. But in a Fargate deployment it's only really used for grouping things in the console.

  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Sub ${AWS::StackName}_CLUSTER

Task

A Task is the configuration for how a container or group of containers will run. The data contained in a task is loosely analogous to what's in a docker-compose file.

  Task:
    Type: AWS::ECS::TaskDefinition
    Properties:
      # equivalent to a .25 vCPU
      Cpu: 256
      # 512MB of RAM
      Memory: 512
      # passed as a parameter
      ExecutionRoleArn: !Ref ECSRoleARN
      Family: !Ref AWS::StackName
      RequiresCompatibilities:
        - FARGATE
      NetworkMode: awsvpc
      ContainerDefinitions:
        - Name: !Sub ${AWS::StackName}_APP
          Image: !Ref APPImage
          PortMappings:
            - ContainerPort: 3000
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Ref LogGroup
              awslogs-region: us-east-1
              awslogs-stream-prefix: !Sub ${AWS::StackName}
          Environment:
            - Name: NODE_ENV
              Value: production
            - Name: APP_NAME
              Value: !Ref AWS::StackName
            - Name: DB_PORT
              Value: 27017
            - Name: DB_DATABASE
              Value: fargate-test
            - Name: SERVER_PORT
              Value: 3000
            - Name: SESSION_SECRET
              Value: correcthorsebatterystaple
            - Name: EXTERNAL_URL
              Value: 0.0.0.0
          # Sensitive details can be passed from the SecretsManager
          Secret:
            - Name: DB_USERNAME
              ValueFrom: # application userame
            - Name: DB_PASSWORD
              ValueFrom: # application password
            - Name: DB_HOSTNAME
              ValueFrom: # hostname for the database service

The environment field in container definitions is where you'll specify the values from the .env file in the project repo. For sensitive value, you should to use AWS Secrets Manager, which reads in values from a resource identifier.

Service

Lastly, we'll create a service that will be responsible for starting, stopping, and restarting our containers. The NetworkConfiguration.AwsvpcConfiguration.AssignPublicIp setting is a little unintuitive, as we don't want our containers to be directly accessibly from the web--and they won't be, even with this set to ENABLED--but we do need the Tasks to be assigned Elastic Network Interfaces (ENIs) so that the load balancer can actually forward traffic to them.

  Service:
    Type: AWS::ECS::Service
    DependsOn: LoadBalancerListener
    Properties:
      ServiceName: !Sub ${AWS::StackName}_SERVICE
      Cluster: !Ref Cluster
      TaskDefinition: !Ref Task
      DesiredCount: 2
      LaunchType: FARGATE
      LoadBalancers:
        - TargetGroupArn:
            Ref: TargetGroup
          ContainerPort: 3000
          ContainerName: !Sub ${AWS::StackName}_APP
      NetworkConfiguration:
        AwsvpcConfiguration:
          # PublicIps are required for traffic routing, but containers won't be globally public
          AssignPublicIp: ENABLED
          Subnets: !Ref Subnets
          SecurityGroups:
            - !Ref ECStoLB
      SchedulingStrategy: REPLICA