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:
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:
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.