Creating An ECS cluster with CloudFormation
Tags: cloud, aws, serverless
Published: 2022-07-06 22:36:09

## What is ECS?
Amazon Elastic Container Service (Amazon ECS) is the AWS container orchestration service that runs and manages Docker containers. With ECS, you can run clusters of virtual machines with either the EC2-backed option or the serverless option with Fargate.
You can read more about ECS in the [AWS documentation](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/Welcome.html)

## Infrastructure
**Disclaimer:** This is not created for a production use case. It is just for illustration purposes.
Things that will be created included in this post:
- VPC
- Internet Gateway
- Public Subnets
- Route tables
- ECS cluster
- ECS Service 
- Task definition
- IAM roles
- Application Load balancer
- Target group
- Security groups
- CloudWatch Log groups
 
That's a long list, but we will accomplish this with CloudFormation. To use HTTPS with the load balancer, you would need to create an SSL certificate in AWS Certificate Manager, but this post will make a load balancer with HTTP, and the cluster will be in public subnets instead of private.
The application used is a simple [rest api](https://hub.docker.com/r/femilawal/python-api).
You can create an ECR repository by following [this post](https://dev.to/femilawal/creating-an-elastic-container-registry-repo-with-cloudformation-f5d) and push the docker image to the repo following [this post](https://dev.to/femilawal/creating-your-private-versions-of-public-docker-images-in-ecr-11f6).

I could divide the CloudFormation template into multiple templates, then export and import values when needed, and give detailed explanations for what each template does, but that would make this post very long... 
![Apology](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/d5crtyhaobdcnti6hm6w.png)
 

I'm going to post everything in a single template, you can consult the [CloudFormation documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) for explanations 

```yaml
AWSTemplateFormatVersion: "2010-09-09"

Description: >
  This template creates ECS Fargate Services.

############################################################
#              PARAMETERS RECORDS BLOCK
############################################################

Parameters:
  ImageTag:
    Type: String
    Default: latest
  ServiceName:
    Type: String
    Default: rest-api
  ContainerPort:
    Type: Number
    Default: 80
  ContainerCpu:
    Type: String
    Default: 256
  ContainerMemory:
    Type: String
    Default: 1GB
  HealthCheckPath:
    Type: String
    Default: /
  MinContainers:
    Type: Number
    Default: 1
  VpcCIDR:
    Description: "IP range for this VPC"
    Type: "String"
    Default: "10.0.0.0/16"
  PublicSubnetOneCIDR:
    Description: "IP range for the public subnet in the first Availability Zone"
    Type: "String"
    Default: "10.0.1.0/24"
  PublicSubnetTwoCIDR:
    Description: "IP range for the public subnet in the second Availability Zone"
    Type: "String"
    Default: "10.0.2.0/24"

Resources:

  ############################################################
  #            VPC & PUBLIC SUBNETS
  ############################################################

  RestVPC:
    Type: "AWS::EC2::VPC"
    Properties:
      CidrBlock: !Ref "VpcCIDR"
      Tags:
        - Key: "Name"
          Value: "Rest-API-VPC"
  ####### Create Public Subnet #######
  PublicSubnetOne:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref RestVPC
      CidrBlock: !Ref "PublicSubnetOneCIDR"
      AvailabilityZone: !Select ["0", !GetAZs ]
      MapPublicIpOnLaunch: "True"
      Tags:
        - Key: "Name"
          Value: !Sub "${PublicSubnetOneCIDR}-PublicSubnetOne"

  PublicSubnetTwo:
    Type: "AWS::EC2::Subnet"
    Properties:
      VpcId: !Ref RestVPC
      CidrBlock: !Ref "PublicSubnetTwoCIDR"
      AvailabilityZone: !Select ["1", !GetAZs ]
      MapPublicIpOnLaunch: "True"
      Tags:
        - Key: "Name"
          Value: !Sub "${PublicSubnetTwoCIDR}-PublicSubnetTwo"

  ######## Create Public Route Table #######
  PublicRouteTable1:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref RestVPC
      Tags:
        - Key: "Name"
          Value: "PublicRoute1"

  PublicRouteTable2:
    Type: "AWS::EC2::RouteTable"
    Properties:
      VpcId: !Ref RestVPC
      Tags:
        - Key: "Name"
          Value: "PublicRoute2"

  ######## Create Internet Gateway #######
  InternetGateway:
    Type: "AWS::EC2::InternetGateway"
    Properties:
      Tags:
        - Key: "Name"
          Value: "InternetGateway"

  ######## Attach Internet Gateway to VPC #######
  GatewayToInternet:
    Type: "AWS::EC2::VPCGatewayAttachment"
    Properties:
      VpcId: !Ref RestVPC
      InternetGatewayId: !Ref "InternetGateway"

  ######## Route-out Public Route Table to Internet Gateway (Internet connection)  #######
  PublicRouteIGW1:
    Type: "AWS::EC2::Route"
    DependsOn: "GatewayToInternet"
    Properties:
      RouteTableId: !Ref "PublicRouteTable1"
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref "InternetGateway"

  PublicRouteIGW2:
    Type: "AWS::EC2::Route"
    DependsOn: "GatewayToInternet"
    Properties:
      RouteTableId: !Ref "PublicRouteTable2"
      DestinationCidrBlock: "0.0.0.0/0"
      GatewayId: !Ref "InternetGateway"

  ######## Associate Public Route Table with Public Subnet1 & Subnet2 #######
  PublicSubnetOneRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref "PublicSubnetOne"
      RouteTableId: !Ref "PublicRouteTable1"

  PublicSubnetTwoRouteTableAssociation:
    Type: "AWS::EC2::SubnetRouteTableAssociation"
    Properties:
      SubnetId: !Ref "PublicSubnetTwo"
      RouteTableId: !Ref "PublicRouteTable2"

  ######## Create Custom Network ACL #######
  PublicNetworkACL:
    Type: "AWS::EC2::NetworkAcl"
    Properties:
      VpcId: !Ref RestVPC
      Tags:
        - Key: "Name"
          Value: "PublicNetworkACL"

  PublicInboundPublicACL:
    Type: "AWS::EC2::NetworkAclEntry"
    Properties:
      NetworkAclId: !Ref PublicNetworkACL
      RuleNumber: "100"
      Protocol: "-1"
      RuleAction: "allow"
      Egress: "false"
      CidrBlock: "0.0.0.0/0"

  PublicOutboundPublicACL:
    Type: "AWS::EC2::NetworkAclEntry"
    Properties:
      NetworkAclId: !Ref PublicNetworkACL
      RuleNumber: "100"
      Protocol: "-1"
      RuleAction: "allow"
      Egress: "true"
      CidrBlock: "0.0.0.0/0"

  ######## Associate Public Subnet to Network ACL #######
  PublicSubnetOneNetworkAclAssociation:
    Type: "AWS::EC2::SubnetNetworkAclAssociation"
    Properties:
      SubnetId: !Ref "PublicSubnetOne"
      NetworkAclId: !Ref "PublicNetworkACL"

  PublicSubnetTwoNetworkAclAssociation:
    Type: "AWS::EC2::SubnetNetworkAclAssociation"
    Properties:
      SubnetId: !Ref "PublicSubnetTwo"
      NetworkAclId: !Ref "PublicNetworkACL"

  ############################################################
  #              ECS CLUSTER BLOCK
  ############################################################
  Cluster:
    Type: AWS::ECS::Cluster
    Properties:
      ClusterName: !Join ["-", [!Ref ServiceName, cluster]]

  ############################################################
  #              ECS TASK DEFINITION
  ############################################################
  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    DependsOn: LogGroup
    Properties:
      # Name of the task definition. Subsequent versions of the task definition are grouped together under this name.
      Family: !Join ["-", [!Ref ServiceName, TaskDefinition]]
      # awsvpc is required for Fargate
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      Cpu: !Ref ContainerCpu
      Memory: !Ref ContainerMemory
      ExecutionRoleArn: !Ref ExecutionRole
      TaskRoleArn: !Ref TaskRole
      ContainerDefinitions:
        - Name: !Ref ServiceName
          Image:
            !Join [
              ".",
              [
                !Ref AWS::AccountId,
                "dkr.ecr",
                !Ref AWS::Region,
                !Sub "amazonaws.com/ecr-repository:${ImageTag}",
              ],
            ]
          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

  ############################################################
  #              IAM ROLE
  ############################################################

  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ["-", [!Ref ServiceName, ExecutionRole]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: "sts:AssumeRole"
      ManagedPolicyArns:
        - "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Join ["-", [!Ref ServiceName, TaskRole]]
      AssumeRolePolicyDocument:
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: "sts:AssumeRole"

  ############################################################
  #              SECURITY GROUP
  ############################################################
  ContainerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: !Join ["-", [!Ref ServiceName, ContainerSecurityGroup]]
      VpcId: !Ref RestVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: !Ref ContainerPort
          ToPort: !Ref ContainerPort
          SourceSecurityGroupId: !Ref LoadBalancerSecurityGroup
  LoadBalancerSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription:
        !Join ["-", [!Ref ServiceName, LoadBalancerSecurityGroup]]
      VpcId: !Ref RestVPC
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0

  ############################################################
  #              ECS SERVICE
  ############################################################
  Service:
    Type: AWS::ECS::Service
    # This dependency is needed so that the load balancer is setup correctly in time
    DependsOn:
      - ApplicationLoadBalancer
      - ALBHTTPListener
      - ListenerRule
      - TargetGroup
    Properties:
      ServiceName: !Ref ServiceName
      Cluster: !Ref Cluster
      TaskDefinition: !Ref TaskDefinition
      DeploymentConfiguration:
        MinimumHealthyPercent: 100
        MaximumPercent: 200
      DesiredCount: !Ref MinContainers
      HealthCheckGracePeriodSeconds: 300
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: ENABLED
          Subnets:
            - !Ref PublicSubnetOne
            - !Ref PublicSubnetTwo
          SecurityGroups:
            - !Ref ContainerSecurityGroup
      LoadBalancers:
        - ContainerName: !Ref ServiceName
          ContainerPort: !Ref ContainerPort
          TargetGroupArn: !Ref TargetGroup

  ############################################################
  #              ECS TARGET GROUP
  ############################################################
  TargetGroup:
    Type: AWS::ElasticLoadBalancingV2::TargetGroup
    Properties:
      HealthCheckIntervalSeconds: 10
      HealthCheckPath: !Ref HealthCheckPath
      HealthCheckTimeoutSeconds: 5
      UnhealthyThresholdCount: 2
      HealthyThresholdCount: 2
      Name: !Join ["-", [!Ref ServiceName, TargetGroup]]
      Port: !Ref ContainerPort
      Protocol: HTTP
      TargetGroupAttributes:
        - Key: deregistration_delay.timeout_seconds
          Value: 60
      TargetType: ip
      VpcId: !Ref RestVPC
      Matcher:
        HttpCode: 200

  ############################################################
  #              APPLICATION LOAD BALANCER
  ############################################################
  ApplicationLoadBalancer:
    Type: AWS::ElasticLoadBalancingV2::LoadBalancer
    Properties:
      LoadBalancerAttributes:
        - Key: idle_timeout.timeout_seconds
          Value: 60
      Name: !Join ["-", [!Ref ServiceName, ApplicationLoadBalancer]]
      Scheme: internet-facing
      SecurityGroups:
        - !Ref LoadBalancerSecurityGroup
      Subnets:
        - !Ref PublicSubnetOne
        - !Ref PublicSubnetTwo

  ALBHTTPListener:
    Type: "AWS::ElasticLoadBalancingV2::Listener"
    Properties:
      LoadBalancerArn: !Ref ApplicationLoadBalancer
      DefaultActions:
        - FixedResponseConfig:
            StatusCode: 200
          Type: fixed-response
      Port: "80"
      Protocol: HTTP

  # ---- Applcation Load Balancer Listener Rule ---- #
  ListenerRule:
    Type: "AWS::ElasticLoadBalancingV2::ListenerRule"
    Properties:
      Actions:
        - Type: forward
          TargetGroupArn: !Ref TargetGroup
      Conditions:
        - Field: path-pattern
          Values:
            - "*"
      ListenerArn: !Ref ALBHTTPListener
      Priority: 1

  ############################################################
  #              CLOUDWATCH LOG GROUPS
  ############################################################
  LogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Join ["", [/ecs/, !Ref ServiceName, TaskDefinition]]
```

I named the template `ecs.yaml`. You can deploy it by running:
```bash
aws cloudformation deploy --template-file ecs.yml --stack-name ecs-infrastructure --capabilities CAPABILITY_NAMED_IAM --profile default
```
![Deploy output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3wume9ww9yonnh6ciu6.png)

You can navigate to the [ECS console](https://us-east-1.console.aws.amazon.com/ecs/v2/clusters) and confirm your cluster was created successfully.
![ECS Console](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/boi3gautjyn1efjrhimz.png)
You can also open the load balancer address to see if the container works as expected.
![LB Address](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xadumj4mqge43c5j53v4.png)

## Cleanup
Always remember to delete resources you do not need, run:
```bash
aws cloudformation delete-stack --stack-name ecs-infrastructure
```
You can check the status of the stack to confirm if it was deleted successfully:
```bash 
aws cloudformation delete-stack --stack-name ecs-infrastructure
```
![Sample output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uv6ppmns2r6jvbf33wiw.png)
Confirm the stack was deleted successfully; remember to clean up the ECR repository if you implemented that.