May 3, 2021

ALB Consolidation

This post isn’t so much about how I consolidated a lot of ALBs, but more about how I saved ~USD$3,300 a year by doing this consolidation.

The particular application in question has been running on AWS for a number of years and has 3 environments, test, staging and production. The application is made up of 6 services. (no, I’m not going to call them micro). Each service is an autoscaling group and used to have an ELB attached.

In the last year or so the ELBs were replaced for ALBs, but were replaced 1:1. With 3 environments and 6 services per environment, that’s nearly USD$4000 per year.

One benefit of ALBs of course, is the ability to route traffic to different target groups based on Listener rules.

With this in mind I set about finding a way to simplify the environment and save some money along the way.

This is a generalised view of an environment before the ALB consolidation process:

ALB After

First off I had to establish if all the services were referred to by a unique hostname and not unusual port numbers and that they used the same domain (or at least hostnames that we could easily add on to an ACM certificate as extra SANs).

Thankfully all the services were on the same domain so a * certificate could be used. Staging and test had odd names but it wasn’t going to impact our consolidation effort.

Domain: foobar.com (not the real domain nor are the host names)

Service Prod Staging Test
Service 1 app app-staging app-test
Service 2 files files-staging files-test
Service 3 web web-staging web-test
Service 4 admin admin-staging admin-test
Service 5 sites sites-staging sites-test
Service 6 blog blog-staging blog-test

This allowed me to create a Mapping in the CloudFormation like this:

  "Mappings": {
    "EnvMapping": {
      "test": {
        "Service1": "app-test.foobar.com",
        "Service2": "files-test.foobar.com",
        "Service3": "web-test.foobar.com",
        "Service4": "admin-test.foobar.com",
        "Service5": "sites-test.foobar.com",
        "Service6": "blog-test.foobar.com"
      },
      "staging": {
        "Service1": "app-staging.foobar.com",
        "Service2": "files-staging.foobar.com",
        "Service3": "web-staging.foobar.com",
        "Service4": "admin-staging.foobar.com",
        "Service5": "sites-staging.foobar.com",
        "Service6": "blog-staging.foobar.com"
      },
      "production": {
        "Service1": "app.foobar.com",
        "Service2": "files.foobar.com",
        "Service3": "web.foobar.com",
        "Service4": "admin.foobar.com",
        "Service5": "sites.foobar.com",
        "Service6": "blog.foobar.com",
      }
    }
  },

Now I needed to setup a new ALB in the Network Stack and export values about the ALB so they could be consumed by the services.

Each service would need:

  • a target group
  • a listener rule
  • a cloudwatch alarm

We also wanted a catchall rule to go through to one service.

This is what the final solution looked like:

ALB After

This now has one ALB in the network stack and a default target group. We export values that we can use in the service stacks:

    "ALBName" : {
      "Description" : "ALBName",
      "Value" : { "Fn::GetAtt": [ "ALB", "LoadBalancerFullName" ]}
    },
    "DefaultTargetGroupName" : {
      "Description" : "ALB Default Target Group Name",
      "Value" : { "Fn::GetAtt": [ "DefaultTargetGroup", "TargetGroupFullName" ]}
    },
    "HTTPSListener" : {
      "Description" : "ALB HTTPS Listener",
      "Value" :  { "Ref" : "HTTPSListener" }
    },
    "DefaultTargetGroup" : {
      "Description" : "ALB Default Target Group",
      "Value" :  { "Ref" : "DefaultTargetGroup" }
    },

In each service you pull in the service mappings for the specific service and environmnet and you can create listener rules like this:

    "ListenerRule": {
      "Type" : "AWS::ElasticLoadBalancingV2::ListenerRule",
      "Properties" : {
          "Actions" : [ { "TargetGroupArn": { "Ref": "Service1TG"}, "Type": "forward", "Order": 1 } ],
          "Conditions" : [ { "Field" : "host-header", "Values": { "Ref" : "Service1Map"} } ],
          "ListenerArn" : { "Ref" : "HTTPSListener"},
          "Priority" : 20 
        }
    },

The service needs a target group created for the autoscaling group to attach to, but the listener rule is what routes any requests to the target group for that service.

To create a cloudwatch alarm for unhealthy instances you can use the local target group and the ALB name in the dimensions like this:

        "Dimensions": [{
          "Name": "LoadBalancer",
          "Value": { "Ref": "ALBName" }
        }, {
          "Name": "TargetGroup",
          "Value": { "Fn::GetAtt": [ "Service1TG", "TargetGroupFullName" ] }
        }],

This is a great example of how to perform a cost optimisation and to simplify an architecture that also reduces the amount of code that needs maintaining.

© Greg Cockburn

Powered by Hugo & Kiss.