11.3 Lambda Layers: Sharing Code and Dependencies Across Functions
Right, let’s talk about Lambda Layers. You know that feeling when you’ve copied the same utils.py file into your fifth Lambda function this week? Your IDE is judging you. You’re violating every principle of DRY (Don’t Repeat Yourself) you hold dear. Layers are AWS’s answer to that shame. They’re essentially a .zip file archive that can contain libraries, custom runtimes, or other dependencies, which you can attach to your functions. Think of them as a shared, read-only /opt directory in the sky.
The beauty here is separation of concerns. Your function code becomes just your business logic. The heavy lifting—your fancy custom logging library, that pesky cryptography package, even a massive machine learning model—gets neatly packaged into a layer. This keeps your deployment package for the function itself lean, which is a godsend for iteration speed.
How to Build a Layer (Without Losing Your Mind)
Let’s say you have a set of utilities you use everywhere. Here’s how you’d package them.
First, create the required directory structure. The magic is in the folder names. For Python, your libraries need to live in a python directory. Node.js? nodejs. This isn’t a suggestion; it’s a requirement because AWS mounts your layer contents based on these specific paths.
# Create the project structure
mkdir -p my_awesome_layer/python
cd my_awesome_layer
# Let's create a simple utility module
echo 'def get_clever_quote():
return "This seemed like a good idea at the time."' > python/utils.py
# Now, package it up!
zip -r ../my_awesome_layer.zip .
You upload that .zip file to create a layer in the AWS Console, CLI, or via your Infrastructure-as-Code tool of choice. Then, you attach the shiny new layer to your function. Now, inside your Lambda handler, you can just import utils as if it lived right next to your code. Because, from the function’s perspective, it now does.
The Gotchas: Where This All Goes Sideways
This seems simple, right? Of course it does. That’s how they get you. Here are the landmines:
The Path Problem: This is the big one. The layer’s contents are extracted to
/opt. The runtimes are pre-configured to look in/opt/python(for Python) and/opt/nodejs(for Node) for libraries. If you mess up the directory structure inside your .zip file—like putting your code inlibinstead ofpython—it’s utterly invisible to your function. It’s there, but it might as well be on Mars.Versioning and Aliasing: Layers are versioned. When you update a layer, you publish a new version (e.g., from version 1 to version 2). This is great for rollbacks and safety. However, your function configuration points to a specific version (e.g.,
my-layer:1). If you update your layer but forget to update your function to point tomy-layer:2, your function won’t see the change. This is a feature, not a bug, but it’s a common source of “I swear I deployed that fix!” confusion.The Bloatware Special: It’s incredibly tempting to create one monolithic “common” layer with every dependency you’ve ever used. Don’t. You’ll end up attaching a 50MB layer to a function that needs one 5KB library from it, drastically increasing your cold start times for no reason. Be surgical. Create layers around logical groupings: a “data-processing-utils” layer, a “security-helpers” layer, etc.
A Real-World Example: Packaging a Dependency
Let’s package the popular requests library instead of our own code. You can’t just pip install requests onto your local machine and zip it up; you have to install it targeting the specific architecture and Python version of the Lambda execution environment, which is Amazon Linux.
The easiest way? Use a Docker container that mimics the Lambda environment. It feels like overkill until it saves you three hours of debugging.
# Create the directory
mkdir -p requests_layer/python
cd requests_layer
# Use Docker to install the library to the target directory
docker run -v "$PWD":/var/task "public.ecr.aws/sam/build-python3.9" /bin/sh -c "pip install -t python requests"
# Package it (the __pycache__ directories are harmless but you can exclude them if you're obsessive)
zip -r ../requests_layer.zip .
Now you have a requests_layer.zip that you can turn into a layer. Attach it to a function, and boom, import requests just works. No more including it in every single deployment package.
The Cold, Hard Truth About Cold Starts
Ah, the question everyone asks: “Do layers affect cold starts?” The answer is nuanced. The layer .zip is fetched and extracted at the same time as your function’s deployment package. So, if you have a massive layer, yes, it can add milliseconds to the cold start initialization phase as AWS sets up the execution environment. However, a well-structured layer can improve cold starts for subsequent functions. How? If multiple functions use the same layer, AWS is sometimes smart enough to already have that environment ready to go, reducing the overhead. It’s a trade-off, like most things in engineering.
The bottom line? Use layers. They are unequivocally a best practice. They make your code cleaner, your deployments faster, and your life easier. Just respect the rules of the road, keep an eye on their size, and always, always test that the layer is actually accessible from your function before you declare victory.