Skip to content

Instantly share code, notes, and snippets.

@esalaza
Created July 10, 2021 02:04
Show Gist options
  • Save esalaza/73387324bcf4a69296e6e343945d839b to your computer and use it in GitHub Desktop.
Save esalaza/73387324bcf4a69296e6e343945d839b to your computer and use it in GitHub Desktop.
# Template for a load-balanced ECS Fargate Web service (only plain HTTP currently) deployed from
# an ECR image which will be published from CI/CD. The ECS Fargate service will also be updated
# from CI/CD. Env vars are supplied to the Web Service via S3 env files. Permissions are added to
# the supplied CI/CD user to be able to use ECR/ECS.
#
# Based on https://github.com/1Strategy/fargate-cloudformation-example/blob/master/fargate.yaml
#
# Pre-requisites:
# - VPC with 2 public subnets
# - User which will push to ECR and update the ECS service (through CI/CD, 'buildkite-dev' by
# default)
# Notes
# - The name of the stack must not contain upperletter characters, otherwise, the ECR repo step
# will fail
# - Sample ECR instructions (from CI/CD):
# - IMAGE_REPO="<account-id>.dkr.ecr.us-east-1.amazonaws.com/<repo-name>"
# - IMAGE_BUILD_TAGGED="${IMAGE_REPO}:${BUILDKITE_BUILD_NUMBER}"
# - IMAGE_LATEST_TAGGED="${IMAGE_REPO}:latest"
# - docker build -f .../Dockerfile -t $IMAGE_BUILD_TAGGED .
# - docker tag $IMAGE_BUILD_TAGGED $IMAGE_LATEST_TAGGED
# - docker push $IMAGE_BUILD_TAGGED
# - docker push $IMAGE_LATEST_TAGGED
# - Sample ECS instructions (from CI/CD):
# - aws ecs update-service --cluster <cluster-name> --service <service-name> --force-new-deployment --no-cli-pager
# - aws ecs wait services-stable --cluster <cluster-name> --services <service-name>
# TODO
# - HTTPS
# - Database and bastion server, S3 bucket for frontend
# - Auto-scaling
# - No --force-new-deployment
# - ...a lot
AWSTemplateFormatVersion: 2010-09-09
Description: Infrastructure for the backend Web service
Parameters:
VPC:
Type: AWS::EC2::VPC::Id
PublicSubnetA:
Type: AWS::EC2::Subnet::Id
PublicSubnetB:
Type: AWS::EC2::Subnet::Id
CicdUser:
Type: String
Default: buildkite-dev
S3EnvVarsBucket:
Type: String
Default: arn:aws:s3:::<s3-bucket-name>
S3EnvVarsFile:
Type: String
Default: backend.env
ContainerPort:
Type: Number
Default: 5000
LoadBalancerHttpPort:
Type: Number
Default: 80
HealthCheckPath:
Type: String
Default: /
LogGroupName:
Type: String
Default: /company/project/backend
Resources:
ECRRepository:
Type: AWS::ECR::Repository
Properties:
RepositoryName: !Sub ${AWS::StackName}-repository
RepositoryPolicyText:
Version: "2012-10-17"
Statement:
- Sid: AllowPushPull
Effect: Allow
Principal:
AWS:
- !Sub arn:aws:iam::${AWS::AccountId}:user/${CicdUser}
Action:
- "ecr:BatchCheckLayerAvailability"
- "ecr:BatchGetImage"
- "ecr:CompleteLayerUpload"
- "ecr:GetDownloadUrlForLayer"
- "ecr:InitiateLayerUpload"
- "ecr:PutImage"
- "ecr:UploadLayerPart"
Cluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub ${AWS::StackName}-ecs-cluster
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
# Name of the task definition. Subsequent versions of the task definition are grouped together under this name.
Family: !Sub ${AWS::StackName}-ecs-cluster-task
# awsvpc is required for Fargate
NetworkMode: awsvpc
RequiresCompatibilities:
- FARGATE
# 256 (.25 vCPU) - Available memory values: 0.5GB, 1GB, 2GB
# 512 (.5 vCPU) - Available memory values: 1GB, 2GB, 3GB, 4GB
# 1024 (1 vCPU) - Available memory values: 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB
# 2048 (2 vCPU) - Available memory values: Between 4GB and 16GB in 1GB increments
# 4096 (4 vCPU) - Available memory values: Between 8GB and 30GB in 1GB increments
Cpu: "512"
# 0.5GB, 1GB, 2GB - Available cpu values: 256 (.25 vCPU)
# 1GB, 2GB, 3GB, 4GB - Available cpu values: 512 (.5 vCPU)
# 2GB, 3GB, 4GB, 5GB, 6GB, 7GB, 8GB - Available cpu values: 1024 (1 vCPU)
# Between 4GB and 16GB in 1GB increments - Available cpu values: 2048 (2 vCPU)
# Between 8GB and 30GB in 1GB increments - Available cpu values: 4096 (4 vCPU)
Memory: 1GB
# A role needed by ECS.
# "The ARN of the task execution role that containers in this task can assume. All containers
# in this task are granted the permissions that are specified in this role.". "There is an
# optional task execution IAM role that you can specify with Fargate to allow your Fargate
# tasks to make API calls to Amazon ECR."
ExecutionRoleArn: !GetAtt ExecutionRole.Arn
# "The Amazon Resource Name (ARN) of an AWS Identity and Access Management (IAM) role that
# grants containers in the task permission to call AWS APIs on your behalf."
TaskRoleArn: !Ref TaskRole
ContainerDefinitions:
- Name: !Sub ${AWS::StackName}-backend-container
Image: !GetAtt ECRRepository.RepositoryUri
EnvironmentFiles:
- Value: !Sub ${S3EnvVarsBucket}/${S3EnvVarsFile}
Type: "s3"
PortMappings:
- ContainerPort: !Ref ContainerPort
# Send logs to CloudWatch Logs
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-region: !Ref AWS::Region
awslogs-group: !Ref LogGroup
awslogs-stream-prefix: ecs-backend
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Ref LogGroupName
RetentionInDays: 7
# A role needed by ECS
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-ecs-execution-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
ManagedPolicyArns:
- "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
Policies:
- PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "s3:GetObject"
Resource:
- !Sub ${S3EnvVarsBucket}/${S3EnvVarsFile}
- Effect: Allow
Action:
- "s3:GetBucketLocation"
Resource:
- !Ref S3EnvVarsBucket
PolicyName: AllowS3GetObject
# A role for the containers
TaskRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-ecs-executtaskion-role
AssumeRolePolicyDocument:
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: "sts:AssumeRole"
Service:
Type: AWS::ECS::Service
# This dependency is needed so that the load balancer is setup correctly in time
DependsOn:
- ListenerHTTP
Properties:
ServiceName: !Sub ${AWS::StackName}-ecs-service
Cluster: !Ref Cluster
TaskDefinition: !Ref TaskDefinition
DeploymentConfiguration:
MinimumHealthyPercent: 100
MaximumPercent: 200
DesiredCount: 2
# This may need to be adjusted if the container takes a while to start up
HealthCheckGracePeriodSeconds: 30
LaunchType: FARGATE
NetworkConfiguration:
AwsvpcConfiguration:
# change to DISABLED if you're using private subnets that have access to a NAT gateway
AssignPublicIp: ENABLED
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
SecurityGroups:
- !Ref ContainerSecurityGroup
LoadBalancers:
- ContainerName: !Sub ${AWS::StackName}-backend-container
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref TargetGroup
ContainerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub ${AWS::StackName}-ecs-service-container-sg
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref ContainerPort
ToPort: !Ref ContainerPort
SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
LoadBalancerSecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: !Sub ${AWS::StackName}-load-balancer-security-group
VpcId: !Ref VPC
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: !Ref LoadBalancerHttpPort
ToPort: !Ref LoadBalancerHttpPort
CidrIp: 0.0.0.0/0
ListenerHTTP:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
DefaultActions:
- TargetGroupArn: !Ref TargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: !Ref LoadBalancerHttpPort
Protocol: HTTP
TargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
HealthCheckIntervalSeconds: 10
# will look for a 200 status code by default unless specified otherwise
HealthCheckPath: !Ref HealthCheckPath
HealthCheckTimeoutSeconds: 5
UnhealthyThresholdCount: 2
HealthyThresholdCount: 2
Name: !Sub ${AWS::StackName}-target-group
Port: !Ref ContainerPort
Protocol: HTTP
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: "60" # default is 300
TargetType: ip
VpcId: !Ref VPC
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: !Sub ${AWS::StackName}-load-balancer
# "internal" is also an option
Scheme: internet-facing
SecurityGroups:
- !Ref LoadBalancerSecurityGroup
Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
# Permission error if this doesn't exist: An error occurred (AccessDeniedException) when calling
# the UpdateService operation: User: arn:aws:iam::<id>:user/buildkite-dev is not authorized
# to perform: ecs:UpdateService/ecs:DescribeServices on resource:
# arn:aws:ecs:us-east-1:<id>:service/<cluster>/<service>
ECSServiceUpdatePolicy:
Type: AWS::IAM::ManagedPolicy
Properties:
ManagedPolicyName: !Sub ${AWS::StackName}-ecs-update-policy
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Action:
- "ecs:RegisterTaskDefinition"
- "ecs:UpdateService"
- "ecs:DescribeServices"
Resource:
- !Ref Service
Users: [!Sub '${CicdUser}']
Outputs:
LoadBalancerDNSName:
Description: The DNS Name of the load balancer
Value: !GetAtt LoadBalancer.DNSName
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment