Skip to content

Instantly share code, notes, and snippets.

@richardsonlima
Created February 17, 2019 15:41
Show Gist options
  • Save richardsonlima/3635ec643f9a5b3fcfa1844ac5d678e2 to your computer and use it in GitHub Desktop.
Save richardsonlima/3635ec643f9a5b3fcfa1844ac5d678e2 to your computer and use it in GitHub Desktop.
---
# Copyright 2018 widdix GmbH
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECS: service that runs on an ECS cluster based on ecs/cluster.yaml and uses a dedicated ALB, a cloudonaut.io template'
Metadata:
'AWS::CloudFormation::Interface':
ParameterGroups:
- Label:
default: 'Parent Stacks'
Parameters:
- ParentVPCStack
- ParentClusterStack
- ParentAuthProxyStack
- ParentAlertStack
- ParentZoneStack
- ParentS3StackAccessLog
- Label:
default: 'Load Balancer Parameters'
Parameters:
- LoadBalancerScheme
- LoadBalancerCertificateArn
- LoadBalancerIdleTimeout
- LoadBalancerDeregistrationDelay
- Label:
default: 'Task Parameters'
Parameters:
- Image
- Label:
default: 'Service Parameters'
Parameters:
- SubDomainNameWithDot
- DesiredCount
- AutoScaling
- MaxCapacity
- MinCapacity
- HealthCheckGracePeriod
Parameters:
ParentVPCStack:
Description: 'Stack name of parent VPC stack based on vpc/vpc-*azs.yaml template.'
Type: String
ParentClusterStack:
Description: 'Stack name of parent Cluster stack based on ecs/cluster.yaml template.'
Type: String
ParentAuthProxyStack:
Description: 'Optional stack name of parent auth proxy stack based on security/auth-proxy-*.yaml template.'
Type: String
Default: ''
ParentAlertStack:
Description: 'Optional but recommended stack name of parent alert stack based on operations/alert.yaml template.'
Type: String
Default: ''
ParentZoneStack:
Description: 'Optional stack name of parent zone stack based on vpc/zone-*.yaml template.'
Type: String
Default: ''
ParentS3StackAccessLog:
Description: 'Optional stack name of parent s3 stack based on state/s3.yaml template (with Access set to ElbAccessLogWrite) to store access logs.'
Type: String
Default: ''
LoadBalancerScheme:
Description: 'Indicates whether the load balancer in front of the ECS service is internet-facing or internal.'
Type: String
Default: 'internet-facing'
AllowedValues:
- 'internet-facing'
- internal
LoadBalancerCertificateArn:
Description: 'Optional Amazon Resource Name (ARN) of the certificate to associate with the load balancer.'
Type: String
Default: ''
LoadBalancerIdleTimeout:
Description: 'The idle timeout value, in seconds.'
Type: Number
Default: 60
MinValue: 1
MaxValue: 4000
LoadBalancerDeregistrationDelay:
Description: 'The amount time (in seconds) to wait before changing the state of a deregistering target from draining to unused.'
Type: Number
Default: 60
ConstraintDescription: 'Must be in the range [0-3600]'
MinValue: 0
MaxValue: 3600
Image:
Description: 'The image to use for a container, which is passed directly to the Docker daemon. You can use images in the Docker Hub registry or specify other repositories (repository-url/image:tag).'
Type: String
DesiredCount:
Description: 'The number of simultaneous tasks, which you specify by using the TaskDefinition property, that you want to run on the cluster.'
Type: Number
Default: 2
ConstraintDescription: 'Must be >= 1'
MinValue: 1
MaxCapacity:
Description: 'The maximum number of simultaneous tasks, that you want to run on the cluster.'
Type: Number
Default: 4
ConstraintDescription: 'Must be >= 1'
MinValue: 1
MinCapacity:
Description: 'The minimum number of simultaneous tasks, that you want to run on the cluster.'
Type: Number
Default: 2
ConstraintDescription: 'Must be >= 1'
MinValue: 1
SubDomainNameWithDot:
Description: 'Name that is used to create the DNS entry with trailing dot, e.g. §{SubDomainNameWithDot}§{HostedZoneName}. Leave blank for naked (or apex and bare) domain. Requires ParentZoneStack parameter!'
Type: String
Default: ''
AutoScaling:
Description: 'Scale number of tasks based on CPU load?'
Type: String
Default: 'true'
AllowedValues: ['true', 'false']
HealthCheckGracePeriod:
Description: 'The period of time, in seconds, that the Amazon ECS service scheduler ignores unhealthy Elastic Load Balancing target health checks after a task has first started.'
Type: Number
Default: 60
MinValue: 0
MaxValue: 1800
Conditions:
HasAuthProxySecurityGroup: !Not [!Equals [!Ref ParentAuthProxyStack, '']]
HasNotAuthProxySecurityGroup: !Equals [!Ref ParentAuthProxyStack, '']
HasLoadBalancerSchemeInternal: !Equals [!Ref LoadBalancerScheme, 'internal']
HasLoadBalancerCertificateArn: !Not [!Equals [!Ref LoadBalancerCertificateArn, '']]
HasAuthProxySecurityGroupAndLoadBalancerCertificateArn: !And [!Condition HasAuthProxySecurityGroup, !Condition HasLoadBalancerCertificateArn]
HasNotAuthProxySecurityGroupAndLoadBalancerCertificateArn: !And [!Condition HasNotAuthProxySecurityGroup, !Condition HasLoadBalancerCertificateArn]
HasAlertTopic: !Not [!Equals [!Ref ParentAlertStack, '']]
HasZone: !Not [!Equals [!Ref ParentZoneStack, '']]
HasS3Bucket: !Not [!Equals [!Ref ParentS3StackAccessLog, '']]
HasAutoScaling: !Equals [!Ref AutoScaling, 'true']
Resources:
TaskDefinition:
Type: 'AWS::ECS::TaskDefinition'
Properties:
Family: !Ref 'AWS::StackName'
NetworkMode: bridge
ContainerDefinitions:
- Name: main # if you change this, you also must change the AWS::ECS::Service
Image: !Ref Image
Memory: 128
PortMappings:
- ContainerPort: 80 # if you change this, you also must change the AWS::ECS::Service
Protocol: tcp
Essential: true
LogConfiguration:
LogDriver: awslogs
Options:
'awslogs-region': !Ref 'AWS::Region'
'awslogs-group': {'Fn::ImportValue': !Sub '${ParentClusterStack}-LogGroup'}
'awslogs-stream-prefix': !Ref 'AWS::StackName'
ALBSecurityGroup:
Type: 'AWS::EC2::SecurityGroup'
Properties:
GroupDescription: 'ecs-cluster-alb'
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
ALBSecurityGroupInHttpWorld:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasNotAuthProxySecurityGroup
Properties:
GroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: '0.0.0.0/0'
ALBSecurityGroupInHttpsWorld:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasNotAuthProxySecurityGroupAndLoadBalancerCertificateArn
Properties:
GroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
CidrIp: '0.0.0.0/0'
ALBSecurityGroupInHttpAuthProxy:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasAuthProxySecurityGroup
Properties:
GroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 80
ToPort: 80
SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentAuthProxyStack}-SecurityGroup'}
ALBSecurityGroupInHttpsAuthProxy:
Type: 'AWS::EC2::SecurityGroupIngress'
Condition: HasAuthProxySecurityGroupAndLoadBalancerCertificateArn
Properties:
GroupId: !Ref ALBSecurityGroup
IpProtocol: tcp
FromPort: 443
ToPort: 443
SourceSecurityGroupId: {'Fn::ImportValue': !Sub '${ParentAuthProxyStack}-SecurityGroup'}
SecurityGroupInALB:
Type: 'AWS::EC2::SecurityGroupIngress'
Properties:
GroupId: {'Fn::ImportValue': !Sub '${ParentClusterStack}-SecurityGroup'}
IpProtocol: tcp
FromPort: 0
ToPort: 65535
SourceSecurityGroupId: !Ref ALBSecurityGroup
DefaultTargetGroup: # not monitored, but LoadBalancer is monitored!
Type: 'AWS::ElasticLoadBalancingV2::TargetGroup'
Properties:
HealthCheckIntervalSeconds: 15
HealthCheckPath: '/'
HealthCheckProtocol: HTTP
HealthCheckTimeoutSeconds: 10
HealthyThresholdCount: 2
UnhealthyThresholdCount: 2
Matcher:
HttpCode: '200-299'
Port: 80
Protocol: HTTP
VpcId: {'Fn::ImportValue': !Sub '${ParentVPCStack}-VPC'}
TargetGroupAttributes:
- Key: deregistration_delay.timeout_seconds
Value: !Ref LoadBalancerDeregistrationDelay
HTTPCodeELB5XXTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer returns 5XX HTTP status codes'
Namespace: 'AWS/ApplicationELB'
MetricName: HTTPCode_ELB_5XX_Count
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt LoadBalancer.LoadBalancerFullName
HTTPCodeTarget5XXTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer receives 5XX HTTP status codes from targets'
Namespace: 'AWS/ApplicationELB'
MetricName: HTTPCode_Target_5XX_Count
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt LoadBalancer.LoadBalancerFullName
RejectedConnectionCountTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer rejected connections because the load balancer had reached its maximum number of connections'
Namespace: 'AWS/ApplicationELB'
MetricName: RejectedConnectionCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt LoadBalancer.LoadBalancerFullName
TargetConnectionErrorCountTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Application load balancer could not connect to targets'
Namespace: 'AWS/ApplicationELB'
MetricName: TargetConnectionErrorCount
Statistic: Sum
Period: 60
EvaluationPeriods: 1
ComparisonOperator: GreaterThanThreshold
Threshold: 0
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
Dimensions:
- Name: LoadBalancer
Value: !GetAtt LoadBalancer.LoadBalancerFullName
RecordSet:
Condition: HasZone
Type: 'AWS::Route53::RecordSet'
Properties:
AliasTarget:
HostedZoneId: !GetAtt 'LoadBalancer.CanonicalHostedZoneID'
DNSName: !GetAtt 'LoadBalancer.DNSName'
HostedZoneId: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneId'}
Name: !Sub
- '${SubDomainNameWithDot}${HostedZoneName}'
- SubDomainNameWithDot: !Ref SubDomainNameWithDot
HostedZoneName: {'Fn::ImportValue': !Sub '${ParentZoneStack}-HostedZoneName'}
Type: A
LoadBalancer:
Type: 'AWS::ElasticLoadBalancingV2::LoadBalancer'
Properties:
LoadBalancerAttributes:
- Key: 'idle_timeout.timeout_seconds'
Value: !Ref LoadBalancerIdleTimeout
- Key: 'routing.http2.enabled'
Value: 'true'
- Key: 'access_logs.s3.enabled'
Value: !If [HasS3Bucket, 'true', 'false']
- !If [HasS3Bucket, {Key: 'access_logs.s3.prefix', Value: !Ref 'AWS::StackName'}, !Ref 'AWS::NoValue']
- !If [HasS3Bucket, {Key: 'access_logs.s3.bucket', Value: {'Fn::ImportValue': !Sub '${ParentS3StackAccessLog}-BucketName'}}, !Ref 'AWS::NoValue']
Scheme: !Ref LoadBalancerScheme
SecurityGroups:
- !Ref ALBSecurityGroup
Subnets: !If
- HasLoadBalancerSchemeInternal
- !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPrivate'}]
- !Split [',', {'Fn::ImportValue': !Sub '${ParentVPCStack}-SubnetsPublic'}]
HttpListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Properties:
DefaultActions:
- TargetGroupArn: !Ref DefaultTargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: 80
Protocol: HTTP
HttpsListener:
Type: 'AWS::ElasticLoadBalancingV2::Listener'
Condition: HasLoadBalancerCertificateArn
Properties:
Certificates:
- CertificateArn: !Ref LoadBalancerCertificateArn
DefaultActions:
- TargetGroupArn: !Ref DefaultTargetGroup
Type: forward
LoadBalancerArn: !Ref LoadBalancer
Port: 443
Protocol: HTTPS
ServiceRole:
Type: 'AWS::IAM::Role'
Properties:
ManagedPolicyArns: # TODO get rid of managed policy
- 'arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceRole'
AssumeRolePolicyDocument:
Version: '2008-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'ecs.amazonaws.com'
Action: 'sts:AssumeRole'
Service:
Type: 'AWS::ECS::Service'
DependsOn: HttpListener
Properties:
Cluster: {'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster'}
DeploymentConfiguration:
MaximumPercent: 200
MinimumHealthyPercent: 50
DesiredCount: !Ref DesiredCount
HealthCheckGracePeriodSeconds: !Ref HealthCheckGracePeriod
LoadBalancers:
- ContainerName: main
ContainerPort: 80
TargetGroupArn: !Ref DefaultTargetGroup
PlacementStrategies:
- Type: spread
Field: 'attribute:ecs.availability-zone'
- Type: spread
Field: instanceId
Role: !GetAtt 'ServiceRole.Arn'
TaskDefinition: !Ref TaskDefinition
CPUUtilizationTooHighAlarm:
Condition: HasAlertTopic
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Average CPU utilization over last 10 minutes higher than 80%'
Namespace: 'AWS/ECS'
Dimensions:
- Name: ClusterName
Value: {'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster'}
- Name: ServiceName
Value: !GetAtt 'Service.Name'
MetricName: CPUUtilization
ComparisonOperator: GreaterThanThreshold
Statistic: Average
Period: 300
EvaluationPeriods: 1
Threshold: 80
AlarmActions:
- {'Fn::ImportValue': !Sub '${ParentAlertStack}-TopicARN'}
ScalableTargetRole: # based on http://docs.aws.amazon.com/AmazonECS/latest/developerguide/autoscale_IAM_role.html
Condition: HasAutoScaling
Type: 'AWS::IAM::Role'
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: 'application-autoscaling.amazonaws.com'
Action: 'sts:AssumeRole'
Path: '/'
Policies:
- PolicyName: ecs
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'ecs:DescribeServices'
- 'ecs:UpdateService'
Resource: '*'
- PolicyName: cloudwatch
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- 'cloudwatch:DescribeAlarms'
Resource: '*'
ScalableTarget:
Condition: HasAutoScaling
Type: 'AWS::ApplicationAutoScaling::ScalableTarget'
Properties:
MaxCapacity: !Ref MaxCapacity
MinCapacity: !Ref MinCapacity
ResourceId: !Sub
- 'service/${Cluster}/${Service}'
- Cluster: {'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster'}
Service: !GetAtt 'Service.Name'
RoleARN: !GetAtt 'ScalableTargetRole.Arn'
ScalableDimension: 'ecs:service:DesiredCount'
ServiceNamespace: ecs
ScaleUpPolicy:
Condition: HasAutoScaling
Type: 'AWS::ApplicationAutoScaling::ScalingPolicy'
Properties:
PolicyName: !Sub '${AWS::StackName}-scale-up'
PolicyType: StepScaling
ScalingTargetId: !Ref ScalableTarget
StepScalingPolicyConfiguration:
AdjustmentType: PercentChangeInCapacity
Cooldown: 300
MinAdjustmentMagnitude: 1
StepAdjustments:
- MetricIntervalLowerBound: 0
ScalingAdjustment: 25
ScaleDownPolicy:
Condition: HasAutoScaling
Type: 'AWS::ApplicationAutoScaling::ScalingPolicy'
Properties:
PolicyName: !Sub '${AWS::StackName}-scale-down'
PolicyType: StepScaling
ScalingTargetId: !Ref ScalableTarget
StepScalingPolicyConfiguration:
AdjustmentType: PercentChangeInCapacity
Cooldown: 300
MinAdjustmentMagnitude: 1
StepAdjustments:
- MetricIntervalUpperBound: 0
ScalingAdjustment: -25
CPUUtilizationHighAlarm:
Condition: HasAutoScaling
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Service is running out of CPU'
Namespace: 'AWS/ECS'
Dimensions:
- Name: ClusterName
Value: {'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster'}
- Name: ServiceName
Value: !GetAtt 'Service.Name'
MetricName: CPUUtilization
ComparisonOperator: GreaterThanThreshold
Statistic: Average
Period: 300
EvaluationPeriods: 1
Threshold: 60
AlarmActions:
- !Ref ScaleUpPolicy
CPUUtilizationLowAlarm:
Condition: HasAutoScaling
Type: 'AWS::CloudWatch::Alarm'
Properties:
AlarmDescription: 'Service is wasting CPU'
Namespace: 'AWS/ECS'
Dimensions:
- Name: ClusterName
Value: {'Fn::ImportValue': !Sub '${ParentClusterStack}-Cluster'}
- Name: ServiceName
Value: !GetAtt 'Service.Name'
MetricName: CPUUtilization
ComparisonOperator: LessThanThreshold
Statistic: Average
Period: 300
EvaluationPeriods: 3
Threshold: 30
AlarmActions:
- !Ref ScaleDownPolicy
Outputs:
TemplateID:
Description: 'cloudonaut.io template id.'
Value: 'ecs/service-dedicated-alb'
TemplateVersion:
Description: 'cloudonaut.io template version.'
Value: '__VERSION__'
StackName:
Description: 'Stack name.'
Value: !Sub '${AWS::StackName}'
DNSName:
Description: 'The DNS name for the ECS cluster/service load balancer.'
Value: !GetAtt 'LoadBalancer.DNSName'
Export:
Name: !Sub '${AWS::StackName}-DNSName'
URL:
Description: 'URL to the ECS service.'
Value: !Sub 'http://${LoadBalancer.DNSName}'
Export:
Name: !Sub '${AWS::StackName}-URL'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment