AWSTemplateFormatVersion: '2010-09-09' Description: Deploy a service on AWS Fargate, hosted in a public subnet, and accessible via a public load balancer. Mappings: EnvironmentNameConfig: Environment: Name: '{{environment.name}}' TaskSize: x-small: cpu: 256 memory: 512 small: cpu: 512 memory: 1024 medium: cpu: 1024 memory: 2048 large: cpu: 2048 memory: 4096 x-large: cpu: 4096 memory: 8192 Resources: # Register service in ServiceDiscovery Service DiscoveryService: Type: AWS::ServiceDiscovery::Service Properties: Description: Discovery Service for the Demo Application DnsConfig: RoutingPolicy: MULTIVALUE DnsRecords: - TTL: 60 Type: A HealthCheckCustomConfig: FailureThreshold: 1 Name: '{{service_instance.inputs.service_discovery_name}}' NamespaceId: '{{environment.outputs.PrivateNamespace}}' # A log group for storing the stdout logs from this service's containers LogGroup: Type: AWS::Logs::LogGroup Properties: LogGroupName: '{{service.name}}/{{service_instance.name}}' # The task definition. This is a simple metadata description of what # container to run, and what resource requirements it has. TaskDefinition: Type: AWS::ECS::TaskDefinition Properties: Family: '{{service.name}}_{{service_instance.name}}' Cpu: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, cpu] Memory: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, memory] NetworkMode: awsvpc RequiresCompatibilities: - FARGATE ExecutionRoleArn: '{{environment.outputs.ECSTaskExecutionRole}}' TaskRoleArn: !Ref "AWS::NoValue" ContainerDefinitions: - Name: '{{service_instance.name}}' Cpu: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, cpu] Memory: !FindInMap [TaskSize, {{service_instance.inputs.task_size}}, memory] Image: '{{service_instance.inputs.image}}' PortMappings: - ContainerPort: '{{service_instance.inputs.port}}' LogConfiguration: LogDriver: 'awslogs' Options: awslogs-group: '{{service.name}}/{{service_instance.name}}' awslogs-region: !Ref 'AWS::Region' awslogs-stream-prefix: '{{service.name}}/{{service_instance.name}}' Tags: - Key: Name Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] # The service_instance.inputs. The service is a resource which allows you to run multiple # copies of a type of task, and gather up their logs and metrics, as well # as monitor the number of running tasks and replace any that have crashed Service: Type: AWS::ECS::Service DependsOn: LoadBalancerRule Properties: ServiceName: '{{service.name}}_{{service_instance.name}}' Cluster: '{{environment.outputs.ClusterName}}' LaunchType: FARGATE DeploymentConfiguration: MaximumPercent: 200 MinimumHealthyPercent: 75 DesiredCount: '{{service_instance.inputs.desired_count}}' NetworkConfiguration: AwsvpcConfiguration: AssignPublicIp: ENABLED SecurityGroups: - '{{environment.outputs.ContainerSecurityGroup}}' Subnets: - '{{environment.outputs.PublicSubnetOne}}' - '{{environment.outputs.PublicSubnetTwo}}' TaskDefinition: !Ref 'TaskDefinition' ServiceRegistries: - RegistryArn: !GetAtt DiscoveryService.Arn LoadBalancers: - ContainerName: '{{service_instance.name}}' ContainerPort: '{{service_instance.inputs.port}}' TargetGroupArn: !Ref 'TargetGroup' Tags: - Key: Name Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] # A target group. This is used for keeping track of all the tasks, and # what IP addresses / port numbers they have. You can query it yourself, # to use the addresses yourself, but most often this target group is just # connected to an application load balancer, or network load balancer, so # it can automatically distribute traffic across all the targets. TargetGroup: Type: AWS::ElasticLoadBalancingV2::TargetGroup Properties: HealthCheckIntervalSeconds: 6 HealthCheckPath: / HealthCheckProtocol: HTTP HealthCheckTimeoutSeconds: 5 HealthyThresholdCount: 2 TargetType: ip # Note that the Name property has a 32 character limit, which could be # reached by using either {{service.name}}, {{service_instance.name}} # or a combination of both as we're doing here, so we truncate the name to 29 characters # plus an ellipsis different from '...' or '---' to avoid running into errors. Name: '{{(service.name~"--"~service_instance.name)|truncate(29, true, 'zzz', 0)}}' Port: '{{service_instance.inputs.port}}' Protocol: HTTP UnhealthyThresholdCount: 2 VpcId: '{{environment.outputs.VpcId}}' Tags: - Key: Name Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] # Create a rule on the load balancer for routing traffic to the target group LoadBalancerRule: Type: AWS::ElasticLoadBalancingV2::ListenerRule Properties: Actions: - TargetGroupArn: !Ref 'TargetGroup' Type: 'forward' Conditions: - Field: path-pattern Values: - '*' ListenerArn: !Ref PublicLoadBalancerListener Priority: 1 # Enable autoscaling for this service ScalableTarget: Type: AWS::ApplicationAutoScaling::ScalableTarget DependsOn: Service Properties: ServiceNamespace: 'ecs' ScalableDimension: 'ecs:service:DesiredCount' ResourceId: Fn::Join: - '/' - - service - '{{environment.outputs.ClusterName}}' - '{{service.name}}_{{service_instance.name}}' MinCapacity: 1 MaxCapacity: 10 RoleARN: !Sub arn:aws:iam::${AWS::AccountId}:role/aws-service-role/ecs.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_ECSService # Create scaling policies for the service ScaleDownPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy DependsOn: ScalableTarget Properties: PolicyName: Fn::Join: - '/' - - scale - '{{service.name}}_{{service_instance.name}}' - down PolicyType: StepScaling ResourceId: Fn::Join: - '/' - - service - '{{environment.outputs.ClusterName}}' - '{{service.name}}_{{service_instance.name}}' ScalableDimension: 'ecs:service:DesiredCount' ServiceNamespace: 'ecs' StepScalingPolicyConfiguration: AdjustmentType: 'ChangeInCapacity' StepAdjustments: - MetricIntervalUpperBound: 0 ScalingAdjustment: -1 MetricAggregationType: 'Average' Cooldown: 60 ScaleUpPolicy: Type: AWS::ApplicationAutoScaling::ScalingPolicy DependsOn: ScalableTarget Properties: PolicyName: Fn::Join: - '/' - - scale - '{{service.name}}_{{service_instance.name}}' - up PolicyType: StepScaling ResourceId: Fn::Join: - '/' - - service - '{{environment.outputs.ClusterName}}' - '{{service.name}}_{{service_instance.name}}' ScalableDimension: 'ecs:service:DesiredCount' ServiceNamespace: 'ecs' StepScalingPolicyConfiguration: AdjustmentType: 'ChangeInCapacity' StepAdjustments: - MetricIntervalLowerBound: 0 MetricIntervalUpperBound: 15 ScalingAdjustment: 1 - MetricIntervalLowerBound: 15 MetricIntervalUpperBound: 25 ScalingAdjustment: 2 - MetricIntervalLowerBound: 25 ScalingAdjustment: 3 MetricAggregationType: 'Average' Cooldown: 60 # Create alarms to trigger these policies LowCpuUsageAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Fn::Join: - '-' - - low-cpu - '{{service.name}}_{{service_instance.name}}' AlarmDescription: Fn::Join: - ' ' - - "Low CPU utilization for service" - '{{service.name}}_{{service_instance.name}}' MetricName: CPUUtilization Namespace: AWS/ECS Dimensions: - Name: ServiceName Value: '{{service.name}}_{{service_instance.name}}' - Name: ClusterName Value: '{{environment.outputs.ClusterName}}' Statistic: Average Period: 60 EvaluationPeriods: 1 Threshold: 20 ComparisonOperator: LessThanOrEqualToThreshold AlarmActions: - !Ref ScaleDownPolicy HighCpuUsageAlarm: Type: AWS::CloudWatch::Alarm Properties: AlarmName: Fn::Join: - '-' - - high-cpu - '{{service.name}}_{{service_instance.name}}' AlarmDescription: Fn::Join: - ' ' - - "High CPU utilization for service" - '{{service.name}}_{{service_instance.name}}' MetricName: CPUUtilization Namespace: AWS/ECS Dimensions: - Name: ServiceName Value: '{{service.name}}_{{service_instance.name}}' - Name: ClusterName Value: '{{environment.outputs.ClusterName}}' Statistic: Average Period: 60 EvaluationPeriods: 1 Threshold: 70 ComparisonOperator: GreaterThanOrEqualToThreshold AlarmActions: - !Ref ScaleUpPolicy EcsSecurityGroupIngressFromPublicALB: Type: AWS::EC2::SecurityGroupIngress Properties: Description: Ingress from the public ALB GroupId: '{{environment.outputs.ContainerSecurityGroup}}' IpProtocol: -1 SourceSecurityGroupId: !Ref 'PublicLoadBalancerSG' # Public load balancer, hosted in public subnets that is accessible # to the public, and is intended to route traffic to one or more public # facing services. This is used for accepting traffic from the public # internet and directing it to public facing microservices PublicLoadBalancerSG: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Access to the public facing load balancer VpcId: '{{environment.outputs.VpcId}}' SecurityGroupIngress: # Allow access to ALB from anywhere on the internet - CidrIp: 0.0.0.0/0 IpProtocol: -1 Tags: - Key: Name Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] PublicLoadBalancer: Type: AWS::ElasticLoadBalancingV2::LoadBalancer Properties: Scheme: internet-facing LoadBalancerAttributes: - Key: idle_timeout.timeout_seconds Value: '30' Subnets: # The load balancer is placed into the public subnets, so that traffic # from the internet can reach the load balancer directly via the internet gateway - '{{environment.outputs.PublicSubnetOne}}' - '{{environment.outputs.PublicSubnetTwo}}' SecurityGroups: [!Ref 'PublicLoadBalancerSG'] Tags: - Key: Name Value: !FindInMap ['EnvironmentNameConfig', 'Environment', 'Name'] PublicLoadBalancerListener: Type: AWS::ElasticLoadBalancingV2::Listener DependsOn: - PublicLoadBalancer Properties: DefaultActions: - TargetGroupArn: !Ref 'TargetGroup' Type: 'forward' LoadBalancerArn: !Ref 'PublicLoadBalancer' Port: 80 Protocol: HTTP Outputs: ServiceEndpoint: Description: The URL to access the service Value: !Sub "http://${PublicLoadBalancer.DNSName}" ServiceDiscovery: Description: The registered service discovery Service Id Value: !Ref DiscoveryService