· admin2282 · Blog  · 5 min read

Building a Scalable and Cost-Effective Backend with AWS Lambda and Docker

Rethinking Backend Architecture with Serverless

You’re developing an application with a backend that has activity spikes during the day and almost no activity at night. There are several solutions to handle this:

  1. Have enough servers for the biggest activity peaks. This is simple to set up, but it will be expensive since these servers are underutilized for a good part of the time.
  2. Implement auto-scaling mechanisms to adjust the number of servers based on activity. This is very relevant but requires significant effort to set up correctly.
  3. The solution we’ll talk about today: Deploy your backend on AWS Lambda.

This approach has quite a few advantages. First, taking AWS as an example, it’s very easy to set up. In broad strokes, you just need to create a load balancer, an AWS Lambda function with your backend code (very easy with the Docker runtime), and connect them together. Cost-wise, it’s generally much cheaper than a classic infrastructure like Kubernetes or ECS, where servers run continuously for variable activity peaks. Finally, it’s super scalable. AWS Lambda can launch thousands of instances per minute, allowing it to adjust to almost any number of requests made to your backend.

The Dockerfile

Creating a Docker container for Lambda might seem complex, but it’s actually very simple. For a Python application with Django as the backend, here’s what it looks like:

dockerfile

# Base image optimized for Lambda
FROM public.ecr.aws/lambda/python:3.12

COPY app.py ${LAMBDA_TASK_ROOT}
COPY requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install -r requirements.txt

# Function entry point
CMD [ "app.lambda_handler" ]

Most of the steps are standard and found in any Dockerfile. However, a few details are important:

  • The base image: It’s crucial to use an image provided by AWS to create a Lambda function with a Docker runtime.
  • LAMBDA_TASK_ROOT is a built-in environment variable in the official AWS Lambda image, and its value is /var/task. This is where Lambda executes all the code, so that’s where the code must be located.
  • We see that the entry point is app.lambda_handler. Here is what the app.py file looks like:

python

import os

# Mangum acts as an adapter between the load balancer and the Django app.
# It transforms Lambda requests into WSGI HTTP requests.
from mangum import Mangum
from django.core.wsgi import get_wsgi_application

# Step 1: Define the environment variables required by Django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")

# Step 2: Create the Django WSGI application
application = get_wsgi_application()

# Step 3: Wrap Django with Mangum to make it compatible with Lambda + API Gateway
lambda_handler = Mangum(application)

For more details, you can check the official AWS documentation.

The Deployment Process on AWS

Once our container is ready, here are the steps to deploy it on AWS Lambda.

Step 1: Build the image and push it to ECR

Amazon Elastic Container Registry (ECR) is a private and secure repository on AWS for your Docker images. Using the docker build and docker push commands, this step is usually very straightforward. Here is an example:

bash

export AWS_ACCESS_KEY_ID=<your_access_key>
export AWS_SECRET_ACCESS_KEY=<your_secret_key>
export AWS_DEFAULT_REGION=<your_region>

aws ecr get-login-password --region <your_region> | docker login --username AWS --password-stdin <account_id>.dkr.ecr.eu-west-3.amazonaws.com

docker build --platform linux/amd64 -t <account_id>.dkr.ecr.<your_region>.amazonaws.com/<ecr_repo_name>:<tag> -f deploy/Dockerfile .
docker push <account_id>.dkr.ecr.<your_region>.amazonaws.com/<ecr_repo_name>:<tag>

Step 2: Create the Lambda Function

In the AWS console, you create a new Lambda function by choosing « Container image » as the source. You then simply need to provide the address (the URI) of the image you just stored in ECR.

Step 3: Configure the Function

The final adjustments involve key parameters like allocated memory, environment variables, and the timeout. These settings ensure the function has enough resources to execute correctly.

Directing Traffic with an Application Load Balancer (ALB)

Our Lambda function is ready, but how can users access it? There are several options, but the Application Load Balancer (ALB) is one of the simplest to set up. It sits on the front line, receives all requests, and efficiently directs them to the correct Lambda function.

The ALB has a dual role: it ensures reliability and performance. During a traffic spike, similar to rush hour, it distributes incoming requests across multiple simultaneous executions of your function. Most importantly, the integration between ALB and Lambda is seamless and easy to configure on AWS.

For the end user, everything is transparent. They send a standard HTTP request, the ALB triggers the Lambda function, waits for its response, and then translates it into a classic HTTP response.

Conclusion: The Limits of This Infrastructure

No solution is perfect; otherwise, everyone would choose this infrastructure 😊

The most important constraint concerns data size. The integration between the ALB and Lambda imposes a 1 MB limit for both requests and responses (see the docs). This means this architecture is ideal for APIs, microservices, and functions that exchange small amounts of data (which, between us, should be the case as much as possible). However, it is not suitable for uploading or downloading large files. An interesting infrastructure to compensate for this is to have two target groups: one on AWS Lambda, and another on, for example, Kubernetes, ECS, or another service. Depending on the requests, traffic is redirected either to Lambda or to the classic target.

Two other shortcomings to consider are: « Cold Starts » (an initial latency during the first invocation after a period of inactivity) and the maximum execution time of 15 minutes. Cold starts can be problematic, and a good solution is to send a request very frequently (e.g., every 5 seconds) to ensure the Lambda functions stay « warm. »

Back to Blog