Amazon ECS and Application Load Balancer

Amazon ECS can optionally be configured to load balance traffic evenly across the tasks in your service. You can either use the classic Elastic Load Balancer or the newer Application Load Balancer.

One cool thing with the Amazon Application Load Balancer, in combination with ECS, is that it supports dynamic port mapping. This means that you don’t need to define a mapping from the container port to the host port in your tasks container definitions. Instead a port will be automatically chosen for the instance’s ephemeral port range. This allows you to run multiple tasks from a single service on the same instance.

The Application Load Balancer also supports path-based routing allowing you to route traffic to different targets depending on a specific pattern in the URL path.

In this post we use Cloudformation to setup an Application Load Balancer and then use it to distribute traffic to two different ECS services.

The load balancer will distribute the incoming requests across the different ECS tasks that are running for each service.

Prerequisites

Create the stack

Download the cloudformation script from here.

Create the stack using AWS console or AWS CLI.

The stack requires the following input parmeters:

  • ClusterName
    The name of the existing ECS cluster
  • ECSSecurityGroup
    An existing security group associated with the EC2 instances used to run ECS tasks. The cloudformation script will add permission for tcp port 32768 to 65535 to this security group.
  • Vpc
    An existing VPC where the EC2 instances resides.
  • Subnets
    Two existing subnets within the above VPC

The stack will output the two URLs that can be used to access the two services.

Details

The cloud formation scripts does the following:

  1. Creates a SecurityGroup that allows port 80. This is the port the load balancer will listen on.
    "LoadBalancerSecurityGroup": {
          "Type": "AWS::EC2::SecurityGroup",
          "Properties": {
            "GroupDescription": "Loadbalancer Allowed Ports",
            "VpcId": { "Ref" : "Vpc"},
            "SecurityGroupIngress": [
              {
                "IpProtocol": "tcp",
                "FromPort": "80",
                "ToPort": "80",
                "CidrIp": "0.0.0.0/0"
              }
            ]
          }
        }
    
  2. Creates a LoadBalancer. We need to supply the two subnets that our EC2 instances resides in and also the security group that we defined above.
    "LoadBalancer" : {
          "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer",
          "Properties": {
            "Scheme" : "internet-facing",
            "Name" : { "Fn::Join": ["-", [ {"Ref": "AWS::StackName"}, "LoadBalancer"] ] },
            "Subnets" : {"Ref": "Subnets"},
            "SecurityGroups": [{"Ref": "LoadBalancerSecurityGroup"}]
          }
        }
    
  3. Creates two TargetGroups, one for each service. The port number (10 in this example) specified for the target group is not relevant in our case since it will be overridden when ECS registers a task as a target. The ephemeral host port assigned to the container by ECS/Docker will be used when routing traffic to the target.
    "TargetGroupService1" : {
          "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup",
          "Properties" : {
            "Name": {
              "Fn::Join": ["-", [ {"Ref": "AWS::StackName"}, "TargetGroupService1"] ]
            },
            "Port": 10,
            "Protocol": "HTTP",
            "HealthCheckPath": "/service1",
            "VpcId": {"Ref" : "Vpc"}
          }
        },
        "TargetGroupService2" : {
          "Type" : "AWS::ElasticLoadBalancingV2::TargetGroup",
          "Properties" : {
            "Name": {
              "Fn::Join": ["-", [ {"Ref": "AWS::StackName"}, "TargetGroupService2"] ]
            },
            "Port": 10,
            "Protocol": "HTTP",
            "HealthCheckPath": "/service2",
            "VpcId": {"Ref" : "Vpc"}
          }
        }
    
  4. Creates a Listener for the load balancer. The listener will listen to port 80 and we supply one of the target groups as the default action (it will be used if no other rule matches).
    "Listener": {
          "Type": "AWS::ElasticLoadBalancingV2::Listener",
          "Properties": {
            "DefaultActions": [
              {
                                 "Type": "forward",
                                 "TargetGroupArn": { "Ref": "TargetGroupService1" }
                               }
            ],
            "LoadBalancerArn": { "Ref": "LoadBalancer" },
            "Port": "80",
            "Protocol": "HTTP"
          }
        }
    
  5. Creates two Listener Rules, one for each service. A listener rule defines a path pattern (such as /service1) and the action to be taken when an incoming HTTP request URL matches this path.  In our case the action will be to forward the HTTP request to the corresponding target group.
    "ListenerRuleService1": {
          "Type" : "AWS::ElasticLoadBalancingV2::ListenerRule",
          "Properties" : {
            "Actions" : [
              {
                "TargetGroupArn" : {"Ref": "TargetGroupService1"},
                "Type" : "forward"
              }
            ],
            "Conditions" : [
              {
                "Field" : "path-pattern",
                "Values" : [ "/service1" ]
              }
            ],
            "ListenerArn" : {"Ref": "Listener"},
            "Priority" : 1
          }
        },
        "ListenerRuleService2": {
          "Type" : "AWS::ElasticLoadBalancingV2::ListenerRule",
          "Properties" : {
            "Actions" : [
              {
                "TargetGroupArn" : {"Ref": "TargetGroupService2"},
                "Type" : "forward"
              }
            ],
            "Conditions" : [
              {
                "Field" : "path-pattern",
                "Values" : [ "/service2" ]
              }
            ],
            "ListenerArn" : {"Ref": "Listener"},
            "Priority" : 2
          }
        }
    
  6. Allows access to ephemeral port range on the EC2 instances. This is done by creating a security group ingress and associated it with a security group assigned to the EC2 instances.
    "SecurityGroupIngressService1": {
          "Type": "AWS::EC2::SecurityGroupIngress",
          "Properties": {
            "GroupId": {
              "Ref": "ECSSecurityGroup"
            },
            "IpProtocol": "tcp",
            "FromPort": 32768,
            "ToPort": 65535,
            "SourceSecurityGroupId": {
              "Ref": "LoadBalancerSecurityGroup"
            }
          }
        }
    
  7. Creates the two ECS tasks. The tasks will run a docker image that contains a demo HTTP server that listen on port 8080 and serves a configurable URL path. You can find the source code on github (https://github.com/babtist/ecs-elb-demo). The docker image takes two parameters, –path defines the URL path served by the HTTP server and –content defined the response that will be sent when accessing the path. The two tasks are created with different path and content values.
    "Task1": {
          "Type" : "AWS::ECS::TaskDefinition",
          "Properties" : {
            "ContainerDefinitions": [
              {
                "Name" : "Task1",
                "Image" : "babtist/http-server-demo",
                "Memory":200,
                "PortMappings":[
                  {
                    "ContainerPort" : 8080
                  }
                ],
                "Command" : [
                  "--path",
                  "/service1",
                  "--content",
                  "'Welcome to Service #1"
                ],
                "Essential":true
              }
            ],
            "Family":{"Fn::Join": ["-", [ {"Ref": "AWS::StackName"}, "Task1"] ]}
          }
        },
        "Task2": {
          "Type" : "AWS::ECS::TaskDefinition",
          "Properties" : {
            "ContainerDefinitions": [
              {
                "Name" : "Task2",
                "Image" : "babtist/http-server-demo",
                "Memory":200,
                "PortMappings":[
                  {
                    "ContainerPort" : 8080
                  }
                ],
                "Command" : [
                  "--path",
                   "/service2",
                  "--content",
                  "'Welcome to Service #2"
                ],
                "Essential":true
              }
            ],
            "Family":{"Fn::Join": ["-", [ {"Ref": "AWS::StackName"}, "Task2"] ]}
          }
        }
    
  8. Creates the two ECS services. We define that we want to run two tasks for each service. Here is also the place where we link our service to the load balancer target group. Note that the ContainerPort parameter refers to the port inside the container, i.e 8080 in our case.
    "Service1": {
          "Type" : "AWS::ECS::Service",
          "DependsOn": [
            "ListenerRuleService1"
          ],
          "Properties" : {
            "Cluster" : { "Ref" : "ClusterName" },
            "DesiredCount" : 2,
            "Role" : "/ecsServiceRole",
            "TaskDefinition" : {"Ref":"Task1"},
            "LoadBalancers": [
              {
                "ContainerName": "Task1",
                "ContainerPort": "8080",
                "TargetGroupArn" : { "Ref" : "TargetGroupService1" }
              }
            ]
          }
        },
        "Service2": {
          "Type" : "AWS::ECS::Service",
          "DependsOn": [
            "ListenerRuleService2"
          ],
          "Properties" : {
            "Cluster" : { "Ref" : "ClusterName" },
            "DesiredCount" : 2,
            "Role" : "/ecsServiceRole",
            "TaskDefinition" : {"Ref":"Task2"},
            "LoadBalancers": [
              {
                "ContainerName": "Task2",
                "ContainerPort": "8080",
                "TargetGroupArn" : { "Ref" : "TargetGroupService2" }
              }
            ]
          }
        }
    
  9. Finally, the script output the URL:s to our two services.
    "Service1DNSName": {
          "Description": "The DNS name for service #1",
          "Value": {
            "Fn::Join": [
              "",
              [
                {"Fn::GetAtt" : [ "LoadBalancer", "DNSName" ]},
                "/service1"
              ]
            ]
          }
        },
        "Service2DNSName": {
          "Description": "The DNS name for service #2",
          "Value": {
            "Fn::Join": [
              "",
              [
                {"Fn::GetAtt" : [ "LoadBalancer", "DNSName" ]},
                "/service2"
              ]
            ]
          }
        }
    

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s