How to Plug a Lambda Function into a Legacy Application Without Changing a Single Line of Code
DEV Community

How to Plug a Lambda Function into a Legacy Application Without Changing a Single Line of Code

Introduction Every development team has one, that critical application that runs the business, that nobody dares to touch, and that a brave team somewhere is slowly rewriting. In the meantime, the business doesn't stop. New requirements keep coming, and someone has to handle them. The classic dilemma : do you ask the team rewriting the application to squeeze in this new feature, slowing down their already complex work ? Or do you modify the legacy codebase yourself, knowing that one wrong move could break something nobody fully understands anymore ? There is a third option that most developers overlook : intercept the request at the load balancer level and handle it with an AWS Lambda function, without touching the legacy application at all. Not a single line of code changed. Not a single deployment risk. In this article, you will learn how to use an Application Load Balancer (ALB) listener rule to route a specific URL path to a Lambda function, while everything else continues flowing normally to your existing EC2 application. The Building Blocks Before diving into the implementation, let's quickly align on the AWS building blocks involved in this solution. Application Load Balancer (ALB) An ALB is a managed load balancer that distributes incoming HTTP/HTTPS traffic across multiple targets. Beyond simple load balancing, it supports content-based routing, meaning it can make intelligent forwarding decisions based on the URL path, hostname, HTTP headers, or query parameters of each incoming request. Listeners and Rules A Listener is a process that checks for incoming connection requests on a specific protocol and port (in our case, HTTP on port 80). Each Listener has a set of Rules that define how to route traffic. Rules are evaluated in priority order, the first matching rule wins. A Default Rule handles any traffic that doesn't match any specific rule. Target Groups A Target Group is a logical grouping of destinations that the ALB forwards traffic to. AWS supports two target types relevant to our use case : Instance (your EC2 application) and Lambda (your function). Each Target Group handles health checking independently; note that Lambda Target Groups do not require health checks, since Lambda manages its own availability. AWS Lambda Lambda is a serverless compute service that runs your code in response to events, including HTTP requests forwarded by an ALB. You only pay for the actual execution time, making it a cost-effective choice for handling specific URL paths that don't receive constant traffic. The Architecture Now that we have a clear picture of each component, let's see how they work together. The solution relies on a single Application Load Balancer serving as the single entry point for all incoming traffic. Behind it, two distinct Target Groups handle requests based on the URL path : - The Instance Target Group receives all general traffic and forwards it to the legacy EC2 application, business as usual. - The Lambda Target Group receives only requests matching a specific URL path (in our case /lambda ) and forwards them to the new Lambda function. Here is the complete request flow : Client Request โ”‚ โ–ผ Application Load Balancer (port 80) โ”‚ โ–ผ Listener - evaluates rules in priority order โ”‚ โ”œโ”€โ”€ Rule: path is /lambda โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Lambda Target Group โ”€โ”€โ–บ Lambda Function โ”‚ โ”‚ โ””โ”€โ”€ Default Rule โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บ Instance Target Group โ”€โ”€โ–บ EC2 Application The key insight here is that the ALB makes the routing decision before the request ever reaches any backend. Neither the Lambda function nor the EC2 application knows about each other, they simply receive requests and return responses. The ALB handles all the traffic orchestration. Why This Matters This architecture gives you surgical precision : you can intercept exactly one URL path and handle it differently, while leaving 100% of the existing application behavior untouched. No redeployment of the legacy app. No coordination with the team rewriting it. No regression risk. Step-by-Step Implementation Let's walk through the exact steps to implement this architecture. We assume the following resources already exist in your AWS account : an Application Load Balancer, an EC2 instance running your legacy application, and a Lambda function containing your new feature's logic. Step 1: Create the Lambda Target Group In the EC2 console, navigate to Target Groups and create a new Target Group with the following settings : - Target type : Lambda function - Target Group name : something containing "Lambda" for clarity (e.g. tg-lambda-new-feature ) - Health checks : disabled, Lambda manages its own availability, no health check endpoint is needed Once created, register your Lambda function as the target. Step 2: Create the Instance Target Group Create a second Target Group for your EC2 application : - Target type : Instances - Target Group name : something containing "Instance" (e.g. tg-instance-legacy-app ) - Protocol : HTTP, Port : 80 - Health check path : /health, make sure your legacy application exposes this endpoint and returns a 200 response Register your EC2 instance as the target and wait for its status to become Healthy before proceeding. Step 3: Add a Listener to the ALB In the EC2 console, navigate to Load Balancers, select your ALB, and add a Listener : - Protocol : HTTP, Port : 80 - Default action : Forward to the Instance Target Group This ensures that all traffic goes to your legacy EC2 application by default, exactly as before. Step 4: Add a Routing Rule for Lambda In your Listener, add a new Rule with the following configuration : - Condition : Path is /lambda - Action : Forward to the Lambda Target Group - Priority : set it higher than the Default Rule (a lower number means higher priority) From this point on, any request to /lambda is intercepted by the ALB and forwarded to your Lambda function. Everything else continues flowing to EC2. Step 5: Ensure your Lambda Returns the Correct Response Format This is a critical detail that catches many developers off guard. When invoked by an ALB, Lambda must return a response in a specific format, different from the API Gateway format : { "statusCode": 200, "statusDescription": "200 OK", "isBase64Encoded": false, "headers": { "Content-Type": "application/json" }, "body": "{"message": "Hello from Lambda"}" } If your Lambda returns a response in the API Gateway format (without statusDescription ), the ALB will return a 502 Bad Gateway error to the client. Always validate your response format when switching between ALB and API Gateway integrations. Common Pitfalls to Avoid Even with a solid understanding of the architecture, a few subtle mistakes can cost you hours of debugging. Here are the most common ones. Pitfall 1: Forgetting to Disable Health Checks on the Lambda Target Group Unlike EC2 instances, Lambda functions do not listen on a port and cannot respond to traditional HTTP health check probes. If you enable health checks on a Lambda Target Group, the ALB will continuously mark your Lambda as unhealthy and stop routing traffic to it. Always disable health checks when using Lambda as a target. Pitfall 2: Wrong Lambda Response Format As mentioned in the implementation steps, the ALB expects a very specific response structure from Lambda, including the statusDescription field. A response that works perfectly with API Gateway will silently break when used behind an ALB, returning a 502 to the client with no obvious error message in the Lambda logs. Always test your Lambda response format specifically in the ALB context. Pitfall 3: Rule Priority Order ALB Listener Rules are evaluated in priority order, lowest number wins. If your Lambda rule has a lower priority than another rule that matches the same path, your Lambda will never receive traffic. Always double-check the priority of your rules, especially when adding new ones to an existing Listener that already has multiple rules. Pitfall 4: Missing Lambda Permissions The ALB needs explicit permission to invoke your Lambda function. When you register a Lambda function via the AWS console, this permission is added automatically as a resource-based policy on the Lambda. However, if you use the AWS CLI or infrastructure-as-code tools like Terraform or CloudFormation, you must add this permission manually using lambda:AddPermission with the ALB as the principal. Without it, the ALB will receive a permission denied error and return a 502 to the client. Pitfall 5: One Lambda per Target Group Unlike Instance Target Groups, a Lambda Target Group can only contain a single Lambda function. If you need to handle multiple distinct URL paths with different Lambda functions, you must create a separate Target Group for each one and add a corresponding Listener Rule for each path. Infrastructure as Code: Terraform Implementation If you prefer infrastructure-as-code over manual console steps, and you should, here is how to implement the key pieces of this architecture using Terraform. The code follows a modular structure for reusability and clarity. The full Terraform code, including module structure, variables, and README, is available on GitHub : alb-lambda-legacy-pattern Here are the two most critical resources to understand. The Listener Rule, routing /lambda to the Lambda Target Group resource "aws_lb_listener_rule" "lambda_rule" { listener_arn = aws_lb_listener.http.arn priority = 10 # Lower number = higher priority condition { path_pattern { values = ["/lambda"] } } action { type = "forward" target_group_arn = aws_lb_target_group.lambda_tg.arn } } The Lambda Permission, the step the console handles automatically but Terraform requires explicitly resource "aws_lambda_permission" "alb_invoke" { statement_id = "AllowALBInvoke" action = "lambda:InvokeFunction" function_name = var.lambda_function_arn principal = "elasticloadbalancing.amazonaws.com" source_arn = var.alb_arn } This second block is the most commonly forgotten piece when implementing this pattern

Read on DEV Community ↗ ← Back to News

Comments

No comments yet. Start the discussion.