Blog -
Attaching Lambdas to an Existing ALB With Terraform
At FloQast, we use AWS Lambda Functions behind an Application Load Balancer as our primary API platform. Each of our Lambda Functions are in a separate Git repository, and the supporting Terraform is in the repo along with the lambda source code. This is great because it gives engineers complete control over their slice of the application.
Scattering the source code across multiple repos solves some problems, but also adds a bit of complexity. We can’t simply attach the lambda function to the ALB by referencing the resource directly, because we don’t have the ALB resource in our local repository. Here is an example of what an ALB system might look like.
ALB Data Sources
This is where we utilize Terraform Data Sources to look up the remote ALB that we will attach our lambda function to.
We actually need to perform two data source lookups here. First we need to get the ALB, so we can use that Resource ARN to look up the Listener. The Listener is what we are actually attaching our Target Group and Lambda function to.
data aws_lb alb { name = var.alb_name } data aws_lb_listener alb443 { load_balancer_arn = data.aws_lb.alb.arn port = 443 }
Lambda Function Resources
Now that we have the Application Load Balancer, we need to create our resources for the Lambda Function.
- Lambda Function
- Lambda Alias We create a
live
alias for all of our lambda functions. (optional) - Target Group
- Lambda Permission to allow the Target Group to invoke the Lambda Function.
- Target Group Attachment to bind the Target Group to the Lambda Function.
resource aws_lambda_function main { function_name = var.lambda_name ... } resource aws_lambda_alias live { name = "live" description = "Live alias" function_name = aws_lambda_function.main.arn function_version = aws_lambda_function.main.version } resource aws_lb_target_group main { name = "${substr(var.lambda_name, 0, 29)}-tg" target_type = "lambda" ... } resource aws_lambda_permission alb { statement_id = "AllowExecutionFromALB" action = "lambda:InvokeFunction" function_name = aws_lambda_function.main.function_name principal = "elasticloadbalancing.amazonaws.com" qualifier = aws_lambda_alias.live.name source_arn = aws_lb_target_group.main.arn } resource aws_lb_target_group_attachment main { target_group_arn = aws_lb_target_group.main.arn target_id = aws_lambda_alias.live.arn depends_on = [ aws_lambda_permission.alb ] }
The substr is used to truncate the name of the Target Group due to the 32 character limit.
Creating the Connection
We have our ALB Data Sources, and we have our Lambda Function, Target Group, and other configuration. Next we need to connect it all together. We will do this using two final resources.
- Target Group Attachment will connect the Target Group to the Lambda Function.
- ALB Listener Rule will connect the ALB to the Target Group.
resource aws_lb_target_group_attachment main { target_group_arn = aws_lb_target_group.main.arn target_id = aws_lambda_alias.live.arn depends_on = [ aws_lambda_permission.alb, aws_lambda_alias.live ] } resource aws_lb_listener_rule main { listener_arn = data.aws_lb_listener.alb443.arn action { type = "forward" target_group_arn = aws_lb_target_group.main.arn } condition { path_pattern { values = local.alb_path } } condition { http_request_method { values = var.alb_request_methods } } }
Note our use of local.alb_path, this will be covered in the next section.
Inputs
We try to abstract as much as possible so the Engineer developing the Lambda Function needs to input as few values as possible. Here are the input variables required for this example to function, but internally we make use of our deployment pipeline and Terraform Local Values to abstract even further.
variable lambda_name { description = "A unique name for your Lambda Function." type = string } variable alb_name { description = "The name of the Application Load Balancer." type = string } variable alb_path { description = "The path to be created on the ALB." type = string default = null } variable alb_request_methods { description = "The request methods to be created on the ALB." type = list(string) default = ["OPTIONS", "GET"] } locals { alb_path = var.alb_path != null ? var.alb_path ? var.lambda_name }
Note that we use a local to default the alb_path to the Lambda Function name. This reduces the inputs required.
A New Problem – Modules to the rescue!
We’ve solved the original problem of attaching a new lambda function to an existing ALB that we don’t have direct resource access to via Terraform, by utilizing Data Sources.
Now we have a new problem of Terraform code duplication across many application repositories. We solved this problem at FloQast by turning the terraform code above into a Terraform Module
By using a module, we are able to maintain the same set of Terraform code for all of our lambda functions, in a single repository. The only Terraform code that now sits in the application repository, is a single module.tf
file that looks something this.
provider aws { region = "us-west-2" } module lambda-platform { source = "[email protected]:FloQast/terraform-module-lambda-platform.git?ref=v1.4.0" lambda_name = "sample-lambda" alb_path = ["/my-sample-path"] }
Within our “Lambda Platform” Terraform Module, we expose a whole suite of customization options, but only a few inputs are actually required. This allows our engineers to quickly stand up new API endpoints without having to spend much time thinking about the underlying infrastructure, while still giving them the ability to customize as much as they need.
Back to Blog