How to use AWS Lambda with Application Load Balancer
In this article, we will discuss how to use AWS Lambda with Application Load Balancer.
target group
with lambda function in the load balancer and add action to the listener to forward the request to this newly created target group based on the path. Read the article for more details.
Why do we need to use AWS Lambda with Application Load Balancer?
One of the popular integrations for AWS Lambda is API Gateway. With API Gateway, the architecture would be completely serverless- you might wonder why we need to use AWS Lambda with Application Load Balancer.
Let's assume you have an existing fargate-based application or EC2-based application running behind the application load balancer - as shown in the below picture
And, you may want to move the complete application to Lambda gradually or some parts of the application to AWS Lambda. In either case, you'll have Fargate/EC2 service and Lambda together. The architecture would look like below
How to mount AWS Lambda with an application load balancer
We'll use path based routing feature of the application load balancer. If the request comes for a specific path, it will be routed to AWS Lambda and all other paths would be routed to the existing Fargate service
Infrastructure
The infrastructure is pretty simple
- VPC with public and private subnets
- Fargate service is hosted in a private subnet of VPC - as it may communicate with RDS
- Lambda function
- Application Load Balancer
Infrastructure code:
We'll be using AWS CDK
for creating all necessary AWS resources in this article. It's an open-source software development framework that lets you define cloud infrastructure. AWS CDK
supports many languages including TypeScript, Python, C#, Java, and others. You can learn more about AWS CDK from a beginner's guide here. We're going to use TypeScript in this article.
VPC:
We're creating VPC with 2 subnets - a public subnet and a private subnet.
const vpc = new ec2.Vpc(this, "alb-lambda-vpc", {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: "ingress",
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: "application",
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
As mentioned earlier, our fargate app is hosted in private subnets. Private subnets are subnets without any internet connectivity and this poses a problem. When you don't have internet connectivity, how we're going to pull the docker image and additional packages that are required for our application? NAT Gateway
solves this problem. From our private subnet, using the NAT gateway, you can access the internet from the private subnet for outbound traffic, and still, all inbound traffic will be blocked.
Load Balancer:
This would be public facing load balancer as the users would be accessing our application through this load balancer
const loadbalancer = new elbv2.ApplicationLoadBalancer(
this,
"alb-lambda-lb",
{
vpc,
internetFacing: true,
vpcSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PUBLIC,
}),
}
);
Fargate Service
In the below code snippet, we're creating an ECS cluster for hosting the fargate service. And we're creating a role with the necessary permission for task execution.
Please note that for this article, we're going to use the image provided by amazon amazon/amazon-ecs-sample
.
Lambda Function
We're creating a simple lambda function with Node 18 as its runtime environment.
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
"aws-sdk", // Use the 'aws-sdk' available in the Lambda runtime
],
},
runtime: Runtime.NODEJS_18_X,
timeout: cdk.Duration.seconds(30), // Default is 3 seconds
memorySize: 256,
};
const helloFn = new NodejsFunction(this, "hello", {
entry: path.join(__dirname, "../lambdas", "hello.ts"),
...nodeJsFunctionProps,
functionName: "helloFn",
});
Target group
for Lambda function:
It is important to note that the application load balancer will not route to specific lambda. Â We're going to create a new target group for the lambda function. Then, based on the path, we can route the request to this target group which contains the lambda functions
const targetGroup = new elbv2.ApplicationTargetGroup(
this,
"alb-lambda-target-group",
{
vpc,
targetType: elbv2.TargetType.LAMBDA,
targets: [new targets.LambdaTarget(helloFn)],
}
);
Add Action to the existing listener of the load balancer:
Our fargate service and lambda function would listen to the requests from port 80. When creating the fargate service, a listener would have created.
We're going to use the existing listener which was created earlier  and add action
to forward the request to the target group containing the lambda function - if it matches a specific path.
const listener = loadbalancer.listeners[0];
listener.addAction("alb-lambda-action", {
action: elbv2.ListenerAction.forward([targetGroup]),
conditions: [elbv2.ListenerCondition.pathPatterns(["/hello"])],
priority: 1,
});
Deployment
Once you deploy the infrastructure using cdk deploy
command, you'll get the URL of the application load balancer
When you access the URL, you'll be able to see the sample app that we loaded in the fargate service
When you access the /hello
path, the request would be forwarded to the lambda group containing lambda and you'll be able to see the response from the lambda.
Complete Code
Below is the complete code for your reference
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import {
NodejsFunction,
NodejsFunctionProps,
} from "aws-cdk-lib/aws-lambda-nodejs";
import { Runtime } from "aws-cdk-lib/aws-lambda";
import * as path from "path";
import * as targets from "aws-cdk-lib/aws-elasticloadbalancingv2-targets";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ecs_patterns from "aws-cdk-lib/aws-ecs-patterns";
export class AlbLambdaStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpc = new ec2.Vpc(this, "alb-lambda-vpc", {
maxAzs: 2,
natGateways: 1,
subnetConfiguration: [
{
cidrMask: 24,
name: "ingress",
subnetType: ec2.SubnetType.PUBLIC,
},
{
cidrMask: 24,
name: "application",
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
],
});
const loadbalancer = new elbv2.ApplicationLoadBalancer(
this,
"alb-lambda-lb",
{
vpc,
internetFacing: true,
vpcSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PUBLIC,
}),
}
);
const nodeJsFunctionProps: NodejsFunctionProps = {
bundling: {
externalModules: [
"aws-sdk", // Use the 'aws-sdk' available in the Lambda runtime
],
},
runtime: Runtime.NODEJS_18_X,
timeout: cdk.Duration.seconds(30), // Default is 3 seconds
memorySize: 256,
};
const helloFn = new NodejsFunction(this, "hello", {
entry: path.join(__dirname, "../lambdas", "hello.ts"),
...nodeJsFunctionProps,
functionName: "helloFn",
});
const cluster = new ecs.Cluster(this, "Cluster", {
vpc,
clusterName: "fargate-cluster",
});
const executionRole = new iam.Role(this, "ExecutionRole", {
assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AmazonECSTaskExecutionRolePolicy"
),
],
});
const service = new ecs_patterns.ApplicationLoadBalancedFargateService(
this,
"FargateService",
{
cluster,
taskImageOptions: {
image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
containerName: "fargate-app-container",
family: "fargate-task-defn",
containerPort: 80,
executionRole,
},
cpu: 256,
memoryLimitMiB: 512,
desiredCount: 2,
serviceName: "fargate-service",
taskSubnets: vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
}),
loadBalancer: loadbalancer,
}
);
const targetGroup = new elbv2.ApplicationTargetGroup(
this,
"alb-lambda-target-group",
{
vpc,
targetType: elbv2.TargetType.LAMBDA,
targets: [new targets.LambdaTarget(helloFn)],
}
);
const listener = loadbalancer.listeners[0];
listener.addAction("alb-lambda-action", {
action: elbv2.ListenerAction.forward([targetGroup]),
conditions: [elbv2.ListenerCondition.pathPatterns(["/hello"])],
priority: 1,
});
}
}
Please let me know your thoughts in the comments.