-
Notifications
You must be signed in to change notification settings - Fork 1
Deployment
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.
The goal is automate as much as possible.
- Merge pull request to
develop
branch. - Travis will run all checks (tsc, lint, test, coverage).
- If everything passes, Travis will build a new image, and push it to Docker Hub with a 'beta' tag.
- Fargate will launch the new container using a reference to the digest hash.
- After QA testing, we can merge
develop
intomaster
. - Repeat steps 2-4, tagging the container with 'latest' and using the new hash.
- Fargate redeploys the new production instance.
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.
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
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
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
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
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
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
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
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.
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