AWS Lambda functions have completely revolutionized the way I work with (and think about) compute. It's just amazingly convenient to implement a quick function, push it to Lambda, schedule it with EventBridge and let it run for free or for almost free.
However, if you're like me, you want to keep pace with the latest and greatest in the Python world, which, at the time of this writing, is Python 3.12. I find the new error messages unbelievably useful and I could hardly wait for the ability to use the same quotation mark inside the curly braces in f-strings.
And, this passion to keep up with the latest and greatest interferes with using AWS Lambda, which only offers Python 3.7, 3., 3.9, 3.10 and 3.11.
Luckily, there's a way around this, and it's fairly easy to implement: create your own Lambda runtime.
There's ample documentation and a wide array of tutorials that show you how to do this (e.g. https://docs.aws.amazon.com/lambda/latest/dg/runtimes-walkthrough.html). However, I also like to follow the D.R.Y. (Don't Repeat Yourself) principle, so why not package it as a reusable Docker image and use it in all my Lambda apps?
Step 1. Setting things up
Let's start by creating a new directory for our entire build setup:
mkdir -p python-lambda-runtimes
cd python-lambda-runtimes
Let's create a Dockerfile
in the directory with the following content (we're going to look at what each line does after the code).
# Define custom function directory
ARG FUNCTION_DIR="/var/task/"
FROM python:3.12-slim-bookworm as build-image
# Include global arg in this stage of the build
ARG FUNCTION_DIR
RUN mkdir -p ${FUNCTION_DIR}
# Install aws-lambda-cpp build dependencies
RUN apt-get update && \
apt-get install -y \
g++ \
make \
cmake \
unzip \
libcurl4-openssl-dev
# Install the function's dependencies
RUN pip install --target ${FUNCTION_DIR} awslambdaric
FROM python:3.12-slim-bookworm
# Include global arg in this stage of the build
ARG FUNCTION_DIR
# Set working directory to function root directory
WORKDIR ${FUNCTION_DIR}
# Copy in the built dependencies
COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
Let's see what's happening here. We initiate a multi-phase build to reduce the size of our final image. On the FROM
lines we choose which Python version we want to use for our runtime. If you wanted to work with Python 3.11 then you could simply replace the 3.12
part with 3.11
.
As a last step of the first build phase we install awslambdaric
, which is the AWS Lambda Runtime Interface Client which makes sure that the Lambda environment is able to communicate with our own code.
In the 2nd stage we simply copy the runtime interface client into our final image.
Step 2: Create a repository in your favorite Docker Container Registry
Since Docker has changed pricing model some time ago I started using AWS Elastic Container Registry (ECR) for my custom, private images. So, let's create a new repo. You can either use the AWS Console if you prefer the GUI, or if you like the CLI, you can simply run this command:
aws ecr create-repository --repository-name python-lambda-runtime
This will create a repository called python-lambda-runtimes
. Feel free to replace the name with anything you prefer.
Step 3: Log in to your preferred repo
For example, if you are using ECR you can use this to log in:
aws ecr get-login-password --region eu-west-1 | docker login --username AWS --password-stdin <AWS_ACCOUNT_ID>.dkr.ecr.eu-west-1.amazonaws.com
Please, replace with the ID of your AWS account.
Step 4: Build and push your image
To build your image and push it to ECR, you can do the following:
docker build -t python-lambda-runtime . && \
docker tag python-lambda-runtime:latest <AWS_ACCOUNT_ID>.dkr.ecr.eu-west-1.amazonaws.com/python-lambda-runtime:latest && \
docker push <AWS_ACCOUNT_ID>.dkr.ecr.eu-west-1.amazonaws.com/python-lambda-runtime:3.12
After this, you have everything in place to start using your pre-built image for your Lambda functions. In the next section, we'll do through all the steps you need to take to use it in your SAM template.
Step 5: Use the image in your SAM template
- Create a
Dockerfile
in the directory of your Lambda function. Add the following content:
FROM <AWS_ACCOUNT_ID>.dkr.ecr.eu-west-1.amazonaws.com/python-lambda-runtime:3.12
COPY . /var/task/
RUN chmod -R 0755 .
RUN pip install -r requirements.txt
CMD ["app.lambda_handler"]
- Look for the
Type: AWS::Serverless::Function
part of yourtemplate.yaml
file. Remove theCodeURI
,Handler
, and theRuntime
lines, and addPackageType: Image
instead. - Add a new section with the same indentation as the
Type: AWS::Serverless::Function
with the following content:
Metadata:
DockerTag: <NAME OF YOUR FUNCTION>
DockerContext: ./<DIRECTORY OF YOUR FUNCTION>
Dockerfile: Dockerfile
For example, if your function was called TestFunction
and lived in the directory test_function
of your project root, your Metadata would look like this:
Metadata:
DockerTag: TestFunction
DockerContext: ./test_function
Dockerfile: Dockerfile
If you run sam build
, it will create the new container image for you.
If you've already deployed your Lambda function before, you'll have to delete it or deploy the containerized one under a new name. Also, because your samconfig.toml
file already contains settings for your previous deployment, it makes sense to rename it to something else, and run sam deploy --guided
to re-populate it with your new settings.
Top comments (5)
Any thoughts on what could be causing sam build to be glacially slow with containers? I try hello-world both with and without docker images. Also x86_64 and arm64. Makes it impossible to use practically. sam --version 1.110.0 on ubuntu 23.10
Great post!
Any idea if there is any additional steps to get this to work with API Gateway / Proxy?
Thanks. This works with API Gateway, I have it in use with it.
Hey, this post is great.
Can you please expain better the step number 6 (SAM template)? I can not understand properly.
Best, Raffaele
Hi Raffaele,
Sure. About which part would you like more details?