Carlos Aguni

Highly motivated self-taught IT analyst. Always learning and ready to explore new skills. An eternal apprentice.


AWS ECS Cloudformation Auto Scaling

20 Sep 2022 »

https://github.com/thomasstep/aws-cloudformation-reference/blob/main/fargate/auto-scaling/auto-scaling-service.yml

ECR

Dockerfile

FROM python:3.7.4

EXPOSE 8080
CMD python3 -m http.server 8080

https://docs.aws.amazon.com/AmazonECR/latest/userguide/docker-push-ecr-image.html

ECS Cluster Stack

aws cloudformation deploy --template-file template.yml --stack-name ecs-cluster-stack --capabilities CAPABILITY_IAM

template.yml

AWSTemplateFormatVersion: 2010-09-09
Description: Starter template for ECS

Parameters:
  VpcStack:
    Type: String
    Description: Name of VPC stack to build off of
    Default: vpc-stack
  vpcid:
    Type: String
    Default: vpc-e3fda484
  subneta:
    Type: String
    Default: subnet-98bb2bfd
  subnetb:
    Type: String
    Default: subnet-4e17fc63

Resources:
  # https://docs.aws.amazon.com/AmazonECR/latest/userguide/registry_auth.html
  EcrRepo:
    Type: AWS::ECR::Repository

  EcsCluster:
    Type: AWS::ECS::Cluster

  DefaultSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
        GroupDescription: Allow http to client host
        VpcId: !Ref vpcid
        #Fn::ImportValue: !Sub ${VpcStack}-vpc-id
        SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 8080
          ToPort: 8080
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 443
          ToPort: 443
          CidrIp: 0.0.0.0/0
        SecurityGroupEgress:
        - IpProtocol: "-1"
          CidrIp: 0.0.0.0/0

  DefaultRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
              - ec2.amazonaws.com
              - ecs.amazonaws.com
              - ecs-tasks.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: /
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
        - arn:aws:iam::aws:policy/AmazonECS_FullAccess
        - arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly


  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      IpAddressType: ipv4
      Scheme: internet-facing
      SecurityGroups:
        - !Ref DefaultSecurityGroup
      Subnets:
        - !Ref subneta
        - !Ref subnetb
          #- Fn::ImportValue: !Sub ${VpcStack}-public-subnet-a-id
          #- Fn::ImportValue: !Sub ${VpcStack}-public-subnet-b-id
      Type: application


Outputs:
  EcsClusterExport:
    Description: A reference to the created ECS Cluster
    Value: !Ref EcsCluster
    Export:
      Name: !Sub ${AWS::StackName}-ecs-cluster

  DefaultSecurityGroupExport:
    Description: A reference to the created SG
    Value: !Ref DefaultSecurityGroup
    Export:
      Name: !Sub ${AWS::StackName}-default-security-group

  DefaultRoleExport:
    Description: A reference to the created IAM Role
    Value: !Ref DefaultRole
    Export:
      Name: !Sub ${AWS::StackName}-default-role

  ApplicationLoadBalancerArnExport:
    Description: A reference to the created ALB
    Value: !Ref ApplicationLoadBalancer
    Export:
      Name: !Sub ${AWS::StackName}-alb-arn

ECS Service + Task

aws cloudformation deploy --template-file template.yml --stack-name ecs-cluster-task --capabilities CAPABILITY_IAM

AWSTemplateFormatVersion: 2010-09-09
Description: Starter template for ECS

Parameters:
  VpcStack:
    Type: String
    Description: Name of VPC stack to build off of
    Default: vpc-stack

  EcsClusterStack:
    Type: String
    Description: Name of ECS Cluster stack to build off of
    Default: ecs-cluster-stack

  Image:
    Type: String
    Description: URI of image you would like to use
    Default: crashlaker/nodejstest:latest
  vpcid:
    Type: String
    Default: vpc-e3fda484
  subneta:
    Type: String
    Default: subnet-98bb2bfd
  subnetb:
    Type: String
    Default: subnet-4e17fc63

Resources:
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckPort: 8080
      HealthCheckProtocol: HTTP
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      VpcId: !Ref vpcid
          #Fn::ImportValue: !Sub ${VpcStack}-vpc-id

  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Order: 1
          TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-alb-arn
      Port: 8080
      Protocol: HTTP
    DependsOn:
      - TargetGroup

  FargateService:
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Fn::ImportValue: !Sub ${EcsClusterStack}-ecs-cluster
      DeploymentController:
        Type: ECS
      DesiredCount: 1
      HealthCheckGracePeriodSeconds: 60
      LaunchType: FARGATE
      LoadBalancers:
        -
          ContainerName: hello-world
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroup
      NetworkConfiguration:
          AwsvpcConfiguration:
            AssignPublicIp: ENABLED
            SecurityGroups:
              -
                Fn::ImportValue: !Sub ${EcsClusterStack}-default-security-group
            Subnets:
              - !Ref subneta
              - !Ref subnetb
                #-
                #Fn::ImportValue: !Sub ${VpcStack}-private-subnet-a-id
                #-
                #Fn::ImportValue: !Sub ${VpcStack}-private-subnet-b-id
      TaskDefinition: !Ref FargateServiceTaskDefinition

  FargateServiceLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

  FargateServiceTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        -
          Name: hello-world
          Essential: true
          Image: !Ref Image
          LogConfiguration:
              LogDriver: awslogs
              Options:
                awslogs-group: !Ref FargateServiceLogGroup
                awslogs-stream-prefix: hello-world-container
                awslogs-region: !Ref AWS::Region
          PortMappings:
            -
              ContainerPort: 8080
              HostPort: 8080
              Protocol: tcp
      Cpu: '256'
      ExecutionRoleArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-default-role
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        -  FARGATE
      TaskRoleArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-default-role

Outputs:
  FargateServiceName:
    Description: A reference to the created Fargate Service
    Value: !GetAtt FargateService.Name
    Export:
      Name: !Sub ${AWS::StackName}-fargate-service-name

ECS Service + Task + Auto Scaling

aws cloudformation deploy --template-file template.yml --stack-name ecs-cluster-task-auto-scaling --capabilities CAPABILITY_IAM

AWSTemplateFormatVersion: 2010-09-09
Description: Template for auto scaling Fargate service based on basic serverless container API

Parameters:
  VpcStack:
    Type: String
    Description: Name of VPC stack to build off of
    Default: vpc-stack

  EcsClusterStack:
    Type: String
    Description: Name of ECS Cluster stack to build off of
    Default: ecs-cluster-stack

  Image:
    Type: String
    Description: URI of image you would like to use
    Default: crashlaker/nodejstest:latest

  MaxContainers:
    Type: Number
    Description: Max containers to scale to
    Default: 10

  vpcid:
    Type: String
    Default: vpc-e3fda484
  subneta:
    Type: String
    Default: subnet-98bb2bfd
  subnetb:
    Type: String
    Default: subnet-4e17fc63

Resources:
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckEnabled: true
      HealthCheckPort: 8080
      HealthCheckProtocol: HTTP
      Port: 8080
      Protocol: HTTP
      TargetType: ip
      VpcId: !Ref vpcid
          #Fn::ImportValue: !Sub ${VpcStack}-vpc-id

  Listener:
    Type: AWS::ElasticLoadBalancingV2::Listener
    Properties:
      DefaultActions:
        - Order: 1
          TargetGroupArn: !Ref TargetGroup
          Type: forward
      LoadBalancerArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-alb-arn
      Port: 8080
      Protocol: HTTP
    DependsOn:
      - TargetGroup

  FargateService:
    Type: AWS::ECS::Service
    Properties:
      Cluster:
        Fn::ImportValue: !Sub ${EcsClusterStack}-ecs-cluster
      DeploymentController:
        Type: ECS
      DesiredCount: 1
      HealthCheckGracePeriodSeconds: 60
      LaunchType: FARGATE
      LoadBalancers:
        -
          ContainerName: serverless-container-api
          ContainerPort: 8080
          TargetGroupArn: !Ref TargetGroup
      NetworkConfiguration:
          AwsvpcConfiguration:
            AssignPublicIp: ENABLED
            SecurityGroups:
              -
                Fn::ImportValue: !Sub ${EcsClusterStack}-default-security-group
            Subnets:
              - !Ref subneta
              - !Ref subnetb
                #-
                #Fn::ImportValue: !Sub ${VpcStack}-private-subnet-a-id
                #-
                #Fn::ImportValue: !Sub ${VpcStack}-private-subnet-b-id
      TaskDefinition: !Ref FargateServiceTaskDefinition

  FargateServiceLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 7

  FargateServiceTaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      ContainerDefinitions:
        -
          Name: serverless-container-api
          Essential: true
          Image: !Ref Image
          LogConfiguration:
              LogDriver: awslogs
              Options:
                awslogs-group: !Ref FargateServiceLogGroup
                awslogs-stream-prefix: serverless-container-api
                awslogs-region: !Ref AWS::Region
          PortMappings:
            -
              ContainerPort: 8080
              HostPort: 8080
              Protocol: tcp
      Cpu: '256'
      ExecutionRoleArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-default-role
      Memory: '512'
      NetworkMode: awsvpc
      RequiresCompatibilities:
        -  FARGATE
      TaskRoleArn:
        Fn::ImportValue: !Sub ${EcsClusterStack}-default-role

# -----------------------------------------------------------
#  AUTO SCALING
# -----------------------------------------------------------

  AutoScalingRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - ecs-tasks.amazonaws.com
            Action:
              - 'sts:AssumeRole'
      Path: '/'
      Policies:
        - PolicyName: root
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ecs:DescribeServices
                  - ecs:UpdateService
                  - cloudwatch:DeleteAlarms
                  - cloudwatch:DescribeAlarms
                  - cloudwatch:PutMetricAlarm
                Resource: '*'

  AutoScalingTarget:
    Type: AWS::ApplicationAutoScaling::ScalableTarget
    Properties:
      MinCapacity: 1
      MaxCapacity: !Ref MaxContainers
      ResourceId: !Join
        - '/'
        - - service
          - Fn::ImportValue: !Sub ${EcsClusterStack}-ecs-cluster
          - !GetAtt FargateService.Name
      ScalableDimension: ecs:service:DesiredCount
      ServiceNamespace: ecs
      RoleARN: !GetAtt AutoScalingRole.Arn

  ScaleUpPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub '${FargateService}ScaleUpPolicy'
      PolicyType: StepScaling
      ScalingTargetId: !Ref AutoScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - MetricIntervalLowerBound: 0
            ScalingAdjustment: 1
  ScaleDownPolicy:
    Type: AWS::ApplicationAutoScaling::ScalingPolicy
    Properties:
      PolicyName: !Sub '${FargateService}ScaleDownPolicy'
      PolicyType: StepScaling
      ScalingTargetId: !Ref AutoScalingTarget
      StepScalingPolicyConfiguration:
        AdjustmentType: ChangeInCapacity
        Cooldown: 60
        MetricAggregationType: Average
        StepAdjustments:
          - MetricIntervalUpperBound: 0
            ScalingAdjustment: -1

  # this alarm is somewhat nonsense but easy to test out
  # change to something relevant before production
  AlarmHighRequests:
    Type: AWS::CloudWatch::Alarm
    Properties:
      ActionsEnabled: TRUE
      AlarmActions:
        - !Ref ScaleUpPolicy
      AlarmDescription: !Sub
        - 'Scale Up Alarm based on requests for ${FargateServiceName}'
        - FargateServiceName: !GetAtt FargateService.Name
      ComparisonOperator: GreaterThanThreshold
      DatapointsToAlarm: 2
      # these can be found in the console after selecting a namespace to filter by
      Dimensions:
        - Name: TargetGroup
          Value: !GetAtt TargetGroup.TargetGroupFullName
      EvaluationPeriods: 3
      # this can be found in the console on the screen before a metric is graphed
      MetricName: RequestCountPerTarget
      # this can be found in the console on the first screen of filtering metrics
      Namespace: AWS/ApplicationELB
      OKActions:
        - !Ref ScaleDownPolicy
      Period: 60
      Statistic: Sum
      Threshold: 3000
      TreatMissingData: ignore
      Unit: None # comes from the metric

Outputs:
  FargateServiceName:
    Description: A reference to the created Fargate Service
    Value: !GetAtt FargateService.Name
    Export:
      Name: !Sub ${AWS::StackName}-fargate-service-name