In this tutorial we will create a AWS ECS Cluster using the AWS CLI Tools.

Create ECS Cluster


I assume that you have a AWS IAM User with Full Access to EC2, ECS and VPC and that your AWS CLI tools has been configured.

If not, you can consult the following documentation:

We will create the ECS Cluster called tools-cluster :

$ $ aws --profile default ecs create-cluster --cluster-name tools-cluster
{
    "cluster": {
        "clusterArn": "arn:aws:ecs:eu-west-1:012345678901:cluster/tools-cluster",
        "clusterName": "tools-cluster",
        "status": "ACTIVE",
        "registeredContainerInstancesCount": 0,
        "runningTasksCount": 0,
        "pendingTasksCount": 0,
        "activeServicesCount": 0,
        "statistics": [],
        "tags": [],
        "settings": [
            {
                "name": "containerInsights",
                "value": "disabled"
            }
        ],
        "capacityProviders": [],
        "defaultCapacityProviderStrategy": []
    }
}

Get the Subnet IDs from your VPC, in my case my subnets are tagged with private in their naming convention:

$ aws --profile default ec2 describe-subnets --filters "Name=vpc-id,Values=vpc-01234567" | jq -r '.Subnets[] | select(.Tags[].Value | contains("private")) .SubnetId'
subnet-0123456789aaaaaaa
subnet-0123456789bbbbbbb
subnet-0123456789ccccccc

Now we will create the ECS Instance Role Policy:

$ cat ecs_instance_role_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "ec2.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Then create the IAM Role named ecs-tools-instance-role:

$ aws --profile default iam create-role --role-name ecs-tools-instance-role --assume-role-policy-document file://ecs_instance_role_policy.json

{
    "Role": {
        "Path": "/",
        "RoleName": "ecs-tools-instance-role",
        "RoleId": "XX",
        "Arn": "arn:aws:iam::xxxxxxxxxxxx:role/ecs-tools-instance-role",
        "CreateDate": "2020-05-19T10:24:18+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ec2.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

Attach the AmazonEC2ContainerServiceforEC2Role to the Role ecs-tools-instance-role:

$ aws --profile default iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AmazonEC2ContainerServiceforEC2Role --role-name ecs-tools-instance-role

Create the Instance Profile Role:

$ aws --profile default iam create-instance-profile --instance-profile-name ecs-tools-instance-role
{
    "InstanceProfile": {
        "Path": "/",
        "InstanceProfileName": "ecs-tools-instance-role",
        "InstanceProfileId": "XX",
        "Arn": "arn:aws:iam::xxxxxxxxxxxx:instance-profile/ecs-tools-instance-role",
        "CreateDate": "2020-05-19T10:30:36+00:00",
        "Roles": []
    }
}

Add the IAM Role to Instance Profile:

$ aws --profile default iam add-role-to-instance-profile --instance-profile-name ecs-tools-instance-role --role-name ecs-tools-instance-role

Create the ECS Task Role Policy ecs_tasks_role_policy.json:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Create the Task IAM Role:

$ aws --profile default iam create-role --role-name ecs-tools-task-role --assume-role-policy-document file://ecs_tasks_role_policy.json
{
    "Role": {
        "Path": "/",
        "RoleName": "ecs-tools-task-role",
        "RoleId": "X",
        "Arn": "arn:aws:iam::xxxxxxxxxxxx:role/ecs-tools-task-role",
        "CreateDate": "2020-05-19T15:43:58+00:00",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Sid": "",
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "ecs-tasks.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

Attach any policies to the IAM Role that you prefer, in my case SSMReadOnly Policy:

$ aws --profile default iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/CloudWatchFullAccess  --role-name ecs-tools-task-role

$ aws --profile default iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy  --role-name ecs-tools-task-role

$ aws --profile default iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess --role-name ecs-tools-task-role

Get the Instance Profile ARN:

$ aws --profile default iam list-instance-profiles-for-role --role-name ecs-tools-instance-role --query 'InstanceProfiles[0].Arn' | jq -r ''
arn:aws:iam::xxxxxxxxxxxx:instance-profile/ecs-tools-instance-role

Create your first Task Definition, taskdef.json:

{
  "family": "mytaskdefinition",
  "containerDefinitions": [{
    "name": "myapp",
    "image": "httpd:2.4",
    "cpu": 68,
    "portMappings": [{
      "containerPort": 80,
      "hostPort": 80,
      "protocol": "tcp"
    }],
    "memory": 50,
    "essential": true,
    "entryPoint": [
      "sh",
      "-c"
    ],
    "command": [
      "/bin/sh -c \"echo 'hello from ecs container instance' >  /usr/local/apache2/htdocs/index.html && httpd-foreground\""
    ]
  }]
}

Register your ECS Task Definition:

$ aws --profile default ecs register-task-definition --cli-input-json file://taskdef.json

Create ECS Container Instance

Get the latest ECS optimized AMI:

$ aws --profile default ssm get-parameter --name '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id' | jq -r '.Parameter.Value'
ami-0a74b180a0c97ecd1

Create a SSH Keypair (optional):

$ aws --profile default ec2 create-key-pair --key-name ecs --query 'KeyMaterial' --output text > ecs.pem
$ chmod 400 ecs.pem

Create the userdata for your ECS Container Instance:

$ cat userdata.txt
#!/bin/bash
echo "ECS_CLUSTER=tools-cluster" >> /etc/ecs/ecs.config
echo ECS_AVAILABLE_LOGGING_DRIVERS='["json-file","awslogs"]' >> /etc/ecs/ecs.config
echo "ECS_CONTAINER_INSTANCE_TAGS={\"ECS_CLUSTER\": \"tools-cluster\"}" >> /etc/ecs/ecs.config

Create the Security Group for the ECS Container Instance:

$ aws --profile default ec2 create-security-group --group-name tools-ecs-sg --description "tools-ecs-sg"
{
    "GroupId": "sg-00000000000000000"
}

Create the Security Group for the Application Load Balancer:

$ aws --profile default ec2 create-security-group --group-name tools-alb-sg --description "tools-alb-sg"
{
    "GroupId": "sg-11111111111111111"
}

Authorize Ingress Ports and Source Addresses of choice:

$ aws --profile default --region eu-west-1 ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 22 --cidr 172.31.0.0/16

$ aws --profile default --region eu-west-1 ec2 authorize-security-group-ingress --group-id sg-00000000000000000 --protocol tcp --port 2049 --cidr 172.31.0.0/16

Create the EBS Mapping for the ECS Container Instance:

$ cat ebs_mapping.json
[
    {
        "DeviceName": "/dev/sda1",
        "Ebs": {
            "DeleteOnTermination": true,
            "VolumeSize": 50,
            "VolumeType": "standard"
        }
    }
]

Deploy the EC2 Instance Deploy Script:

$ cat boot_instance.sh
#!/bin/bash
AWS_PROFILE="default"
AWS_AMI_ID="$(aws --profile default ssm get-parameter --name '/aws/service/ecs/optimized-ami/amazon-linux-2/recommended/image_id' | jq -r '.Parameter.Value')"

AWS_INSTANCE_TYPE="t3a.medium"
AWS_SUBNET_ID="subnet-00000000000000000"
AWS_SSH_KEY="ecs"
AWS_EC2_INSTANCE_COUNT="1"
AWS_SG_ID="sg-00000000000000000"
AWS_INSTANCE_PROFILE_ROLE="arn:aws:iam::xxxxxxxxxxxx:instance-profile/ecs-tools-instance-role"
aws --profile default ec2 run-instances --image-id ${AWS_AMI_ID} --count ${AWS_EC2_INSTANCE_COUNT} \
     --instance-type ${AWS_INSTANCE_TYPE} --key-name ${AWS_SSH_KEY} \
     --subnet-id ${AWS_SUBNET_ID} --security-group-ids ${AWS_SG_ID} \
     --block-device-mappings file://ebs_mapping.json \
     --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ecs-tools-ec2-instance}]' 'ResourceType=volume,Tags=[{Key=Name,Value=ecs-tools-ec2-instance}]' \
     --iam-instance-profile Arn=${AWS_INSTANCE_PROFILE_ROLE} \
     --user-data file://userdata.txt

# to catch ec2_instance_id on run-instances pipe to: | jq -r '.Instances[].InstanceId'

Run the script and create the ECS EC2 Container Instance:

$ $ bash boot_instance.sh
{
    "Groups": [],
    "Instances": [
        {
            "AmiLaunchIndex": 0,
            "ImageId": "ami-0a74b180a0c97ecd1",
            "InstanceId": "i-00000000000000000",
            "InstanceType": "t3a.medium",
            "KeyName": "ecs",
            "LaunchTime": "2020-05-19T11:00:16+00:00",
            "Monitoring": {
                "State": "disabled"
            },
            "Placement": {
                "AvailabilityZone": "eu-west-1a",
                "GroupName": "",
                "Tenancy": "default"
            },
            "PrivateDnsName": "ip-172-31-62-6.eu-west-1.compute.internal",
            "PrivateIpAddress": "172.31.62.6",
            "ProductCodes": [],
            "PublicDnsName": "",
            "State": {
                "Code": 0,
                "Name": "pending"
            },
            "StateTransitionReason": "",
            "SubnetId": "subnet-00000000000000000",
            "VpcId": "vpc-00000000",
            "Architecture": "x86_64",
            "BlockDeviceMappings": [],
            "ClientToken": "",
            "EbsOptimized": false,
            "Hypervisor": "xen",
            "IamInstanceProfile": {
                "Arn": "arn:aws:iam::xxxxxxxxxxxxx:instance-profile/ecs-tools-instance-role",
                "Id": "X"
            },
            "NetworkInterfaces": [
                {
                    "Attachment": {
                        "AttachTime": "2020-05-19T11:00:16+00:00",
                        "AttachmentId": "eni-attach-xxxxxxxxxxxx",
                        "DeleteOnTermination": true,
                        "DeviceIndex": 0,
                        "Status": "attaching"
                    },
                    "Description": "",
                    "Groups": [
                        {
                            "GroupName": "tools-ecs-sg",
                            "GroupId": "sg-111111111111111111"
                        }
                    ],
                    "Ipv6Addresses": [],
                    "MacAddress": "02:3f:5c:24:05:8a",
                    "NetworkInterfaceId": "eni-xxxxxxxxxxxxx",
                    "OwnerId": "xxxxxxxxxxxxx",
                    "PrivateDnsName": "ip-172-31-62-6.eu-west-1.compute.internal",
                    "PrivateIpAddress": "172.31.62.6",
                    "PrivateIpAddresses": [
                        {
                            "Primary": true,
                            "PrivateDnsName": "ip-172-31-62-6.eu-west-1.compute.internal",
                            "PrivateIpAddress": "172.31.62.6"
                        }
                    ],
                    "SourceDestCheck": true,
                    "Status": "in-use",
                    "SubnetId": "subnet-xxxxxxxxxxxxxxxx",
                    "VpcId": "vpc-xxxxxxxxx",
                    "InterfaceType": "interface"
                }
            ],
            "RootDeviceName": "/dev/xvda",
            "RootDeviceType": "ebs",
            "SecurityGroups": [
                {
                    "GroupName": "tools-ecs-sg",
                    "GroupId": "sg-000000000000000000"
                }
            ],
            "SourceDestCheck": true,
            "StateReason": {
                "Code": "pending",
                "Message": "pending"
            },
            "Tags": [
                {
                    "Key": "Name",
                    "Value": "tools-ec2-instance"
                }
            ],
            "VirtualizationType": "hvm",
            "CpuOptions": {
                "CoreCount": 1,
                "ThreadsPerCore": 2
            },
            "CapacityReservationSpecification": {
                "CapacityReservationPreference": "open"
            },
            "MetadataOptions": {
                "State": "pending",
                "HttpTokens": "optional",
                "HttpPutResponseHopLimit": 1,
                "HttpEndpoint": "enabled"
            }
        }
    ],
    "OwnerId": "xxxxxxxxxxxxx",
    "ReservationId": "r-xxxxxxxxxxxxx"
}

Check if instance is running:

$ aws --profile default ec2 describe-instance-status --instance-ids i-0000000000000000
{
    "InstanceStatuses": [
        {
            "AvailabilityZone": "eu-west-1a",
            "InstanceId": "i-0000000000000000",
            "InstanceState": {
                "Code": 16,
                "Name": "running"
            },
            "InstanceStatus": {
                "Details": [
                    {
                        "Name": "reachability",
                        "Status": "passed"
                    }
                ],
                "Status": "ok"
            },
            "SystemStatus": {
                "Details": [
                    {
                        "Name": "reachability",
                        "Status": "passed"
                    }
                ],
                "Status": "ok"
            }
        }
    ]
}

Or, you can just get the status:

$ aws --profile default ec2 describe-instance-status --instance-ids i-0000000000000000 | jq -r '.InstanceStatuses[].InstanceState.Name'
running

ECS Container Instance Checked In

Check that ECS container instance has checked in to the ECS Cluster:

$ aws --profile default ecs list-container-instances --cluster tools-ecs
{
    "containerInstanceArns": [
        "arn:aws:ecs:eu-west-1:xxxxxxxxxxxx:container-instance/xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
    ]
}

List your ECS Clusters:

$ aws --profile default ecs list-clusters
{
    "clusterArns": [
        "arn:aws:ecs:eu-west-1:xxxxxxxxxxxx:cluster/tools-ecs"
    ]
}

Describe your ECS Cluster:

$ aws --profile default ecs describe-clusters --cluster arn:aws:ecs:eu-west-1:xxxxxxxxxxxx:cluster/tools-ecs
{
    "clusters": [
        {
            "clusterArn": "arn:aws:ecs:eu-west-1:xxxxxxxxxxxx:cluster/tools-ecs",
            "clusterName": "tools-ecs",
            "status": "ACTIVE",
            "registeredContainerInstancesCount": 1,
            "runningTasksCount": 0,
            "pendingTasksCount": 0,
            "activeServicesCount": 0,
            "statistics": [],
            "tags": [],
            "settings": [
                {
                    "name": "containerInsights",
                    "value": "disabled"
                }
            ],
            "capacityProviders": [],
            "defaultCapacityProviderStrategy": []
        }
    ],
    "failures": []
}

To list your Task Definitions:

$ aws --profile default ecs list-task-definitions

Describe a Task Definition:

$ aws --profile default ecs describe-task-definition --task-definition mytaskdefinition

To list the running Tasks in your ECS Cluster:

$ aws --profile prod ecs list-tasks --cluster tools-ecs
{
    "taskArns": []
}

Further Steps: Load Balancers

  • Allow the ALB’s SG on the ECS SG (All Traffic) so that the ALB can talk to ECS Container Instances
  • Ensure the Roles are included in the KMS policy (usage)
  • Ensure the Roles are included in the ECR policy (only for build so this can be scratched)
  • Create a Target Group, specify the HTTP port and health checks, dont select instances
  • Go to ALB, edit listener, insert rule, host header, forward to, select created target group
  • Go to Route53, create new entry, the host that you specified, point the record to the ALB CNAME
  • Create service, specify the task definition, add to alb, select existing https listener to point to existing target group

Resources: