AWSTemplateFormatVersion: '2010-09-09' Description: Deploy a service on AWS Fargate, hosted in a public subnet, and accessible via a public load balancer. Mappings: 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: # 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] Environment: - Name: REDIS_HOST Value: !GetAtt ElasticacheCluster.RedisEndpoint.Address - Name: REDIS_PORT Value: !GetAtt ElasticacheCluster.RedisEndpoint.Port - Name: DB_HOST Value: !GetAtt DatabaseInstance.Endpoint.Address - Name: DB_USER Value: !Join ['', ['{{ '{{' }}resolve:secretsmanager:', !Ref MyRDSInstanceRotationSecret, ':SecretString:username{{ '}}' }}' ]] - Name: DB_PASSWORD Value: !Join ['', ['{{ '{{' }}resolve:secretsmanager:', !Ref MyRDSInstanceRotationSecret, ':SecretString:password{{ '}}' }}' ]] - Name: DATABASE Value: '{{service_instance.inputs.DBName}}' 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}}' # 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' LoadBalancers: - ContainerName: '{{service_instance.name}}' ContainerPort: '{{service_instance.inputs.port}}' TargetGroupArn: !Ref 'TargetGroup' # 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: '{{service_instance.inputs.health_check_path}}' 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}}' # 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: - '{{service_instance.inputs.path}}' ListenerArn: '{{environment.outputs.LoadBalancerListner}}' Priority: '{{service_instance.inputs.uri_priority}}' # 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: '{{environment.outputs.LoadBalancerSG}}' # Redis ElasticacheSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: Elasticache Security Group VpcId: '{{environment.outputs.VpcId}}' SecurityGroupIngress: - IpProtocol: tcp FromPort: 6379 ToPort: 6379 SourceSecurityGroupId: '{{environment.outputs.ContainerSecurityGroup}}' SubnetGroup: Type: 'AWS::ElastiCache::SubnetGroup' Properties: Description: Cache Subnet Group CacheSubnetGroupName: {{service_instance.name}}-cache-subnetgroup SubnetIds: - '{{environment.outputs.PrivateSubnetOne}}' - '{{environment.outputs.PrivateSubnetTwo}}' ElasticacheCluster: Type: 'AWS::ElastiCache::CacheCluster' Properties: Engine: redis CacheNodeType: cache.r6g.large NumCacheNodes: '1' VpcSecurityGroupIds: - !GetAtt - ElasticacheSecurityGroup - GroupId CacheSubnetGroupName: {{service_instance.name}}-cache-subnetgroup DependsOn: SubnetGroup # Auora DatabaseCluster: Type: AWS::RDS::DBCluster Properties: Engine: aurora DBClusterIdentifier: '{{service_instance.name}}-Cluster' MasterUsername: !Join ['', ['{{ '{{' }}resolve:secretsmanager:', !Ref MyRDSInstanceRotationSecret, ':SecretString:username{{ '}}' }}' ]] MasterUserPassword: !Join ['', ['{{ '{{' }}resolve:secretsmanager:', !Ref MyRDSInstanceRotationSecret, ':SecretString:password{{ '}}' }}' ]] DBSubnetGroupName: !Ref "DatabaseSubnetGroup" VpcSecurityGroupIds: - !Ref DatabaseSecurityGroup DatabaseSecurityGroup: Type: 'AWS::EC2::SecurityGroup' Properties: GroupDescription: '{{service_instance.name}} Database Security Group' VpcId: '{{environment.outputs.VpcId}}' SecurityGroupIngress: - IpProtocol: tcp FromPort: 3306 ToPort: 3306 SourceSecurityGroupId: '{{environment.outputs.ContainerSecurityGroup}}' DatabaseInstance: Type: 'AWS::RDS::DBInstance' Properties: Engine: aurora DBClusterIdentifier: !Ref DatabaseCluster DBInstanceIdentifier: '{{service_instance.name}}-writer' DBSubnetGroupName: !Ref "DatabaseSubnetGroup" DBInstanceClass: '{{service_instance.inputs.DBInstanceClass}}' DatabaseSubnetGroup: Type: AWS::RDS::DBSubnetGroup Properties: DBSubnetGroupDescription: "{{service_instance.name}} DB subnet group" SubnetIds: - '{{environment.outputs.PrivateSubnetOne}}' - '{{environment.outputs.PrivateSubnetTwo}}' MyRDSInstanceRotationSecret: Type: AWS::SecretsManager::Secret Properties: Description: RDS instance secret for {{service_instance.name}} GenerateSecretString: SecretStringTemplate: '{"username": "admin"}' GenerateStringKey: password PasswordLength: 16 ExcludeCharacters: "\"@/\\" Outputs: ServiceEndpoint: Description: The URL to access the service Value: "http://{{environment.outputs.LoadBalancerDNS}}/"