☁️ 🚀 Cheaper than API Gateway — ALB with Lambda using CloudFormation
t3chflicks
Posted on March 15, 2021
An alternative to API gateway is Application Load Balancer. ALB can be connected with Lambda to produce a highly performant, cost effective API. In this article, I demonstrate how to create an API using CloudFormation and Python.
API Gateway vs. Application Load Balancer
API Gateway and ALB are two different AWS services, however they can both be used to achieve the same thing: send network requests for a service to the service.
API Gateway is pay per request whereas ALBs have an hourly rate, therefore deciding which to use depends on traffic volume. For an in-depth price and feature comparison, I recommend this article, which shares this shocking statistic:
I expect to pay around $166 per month for ALB, whereas I’m paying $4,163 per month for the exact same service from API Gateway.
This is a massive saving! That said, the cost difference isn’t unmerited and you do get extra features from API gateway. Dougal Ballantyne, the Head of Product for Amazon API Gateway tweeted:
If you are building an API and want to leverage AuthN/Z, request validation, rate limiting, SDK generation, direct AWS service backend, use #APIGateway. If you want to add Lambda to an existing web app behind ALB you can now just add it to the needed route
Well, that’s exactly what we’re gonna do!
API with ALB and Lambda
I am going to build the following the system:
Architecture Diagram*
The complete CloudFormation templates can be found here, split into two templates:
vpc.yml
— Configures the VPC. For simplicity, the VPC only contains two public subnets. You can read about a more complex VPC in our previous article.service.yml
— Configures both the ALB and Lambda function.
Lambda
To create a Lambda using CloudFormation, it is necessary to define a Lambda Function, a Role to run the Lambda and a Permission for the ALB to invoke the Lambda:
Lambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
def handler(event, context):
response = {
'isBase64Encoded': False,
'statusCode': 200,
'body': 'HELLO WORLD!'
}
return response
Handler: lambda_function.handler
Role: !GetAtt LambdaRole.Arn
Runtime: python3.7
LambdaRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action: ['sts:AssumeRole']
Effect: Allow
Principal:
Service: ['lambda.amazonaws.com']
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
LambdaFunctionPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt Lambda.Arn
Action: 'lambda:InvokeFunction'
Principal: elasticloadbalancing.amazonaws.com
Application Load Balancer
The Load Balancer is placed across public subnets as it needs to be accessible from the internet. The ALB is configured to listen to HTTP traffic on port 80 and forward it to the Lambda:
LoadBalancerSecGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Load balance allow port 80 traffic
VpcId: !ImportValue VPCID
SecurityGroupIngress:
CidrIp: 0.0.0.0/0
FromPort: 80
IpProtocol: TCP
ToPort: 80
LoadBalancer:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
SecurityGroups:
- !Ref LoadBalancerSecGroup
Subnets:
- !ImportValue PublicSubnetA
- !ImportValue PublicSubnetB
LoadBalancerListener:
Type: AWS::ElasticLoadBalancingV2::Listener
Properties:
Protocol: HTTP
LoadBalancerArn: !Ref LoadBalancer
DefaultActions:
- Type: forward
TargetGroupArn: !Ref LoadBalancerTargetGroup
Port: 80
LoadBalancerTargetGroup:
Type: AWS::ElasticLoadBalancingV2::TargetGroup
Properties:
TargetType: lambda
Targets:
- AvailabilityZone: all
Id: !GetAtt Lambda.Arn
The complete code can be accessed here and can be deployed using the AWS CLI:
aws cloudformation create-stack --stack-name service --template-body file://template.yml --capabilities CAPABILITY_NAMED_IAM
After a successful deployment, the DNS name of the ALB can be found in the EC2 section of the AWS console. It should look something like:
loadb-LoadB-R7RVQD09YC9O-1401336014.eu-west-1.elb.amazonaws.com
Now it is possible to make a request to this URL and get a response:
$ curl loadb-LoadB-R7RVQD09YC9O-1401336014.eu-west-1.elb.amazonaws.com'
HELLO WORLD!
Photo by Nghia Le on Unsplash*
Cross Origin Response Sharing
In order to access this service from a browser webpage on a different domain, CORS must be enabled. This is done by setting headers:
Lambda:
Type: AWS::Lambda::Function
Properties:
Code:
ZipFile: |
def handler(event, context):
response = {
'isBase64Encoded': False,
'statusCode': 200,
'body': 'HELLO WORLD!',
'headers': {
'access-control-allow-methods': 'GET',
'access-control-allow-origin': '*',
'access-control-allow-headers': 'Content-Type, Access-Control-Allow-Headers'
}
}
return response
Handler: lambda_function.handler
Role: !GetAtt LambdaRole.Arn
Runtime: python3.7
Use a Domain Name
AWS provides an ugly Load Balancer address such as:
loadb-LoadB-R7RVQD09YC9O-1401336014.eu-west-1.elb.amazonaws.com
But it’s quite simple to use a custom domain using AWS. Firstly, transfer your DNS management to Route 53 and then create a new record set aliased to the load balancer.
Thanks For Reading
I hope you have enjoyed this article. If you like the style, check out T3chFlicks.org for more tech focused educational content (YouTube, Instagram, Facebook, Twitter).
Resources:
Posted on March 15, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.