Lambda Layer: Pillow
Add Pillow using a Lambda Layer so the ingestion Lambda can crop face thumbnails.
Goal
Add the Pillow library (Python image processing) to beetroot-ingest using a Lambda Layer, so we can crop face thumbnails in the next phase.
What is a Lambda Layer?
A Lambda Layer is a separate zip package that contains dependencies (such as Pillow).
You attach it to a Lambda function, and Lambda makes those dependencies available at runtime.
Important: A Layer is not another Lambda
A Layer is not a new Lambda function. It’s just a dependency bundle that your Lambda can “plug in”.
Why we need a layer here?
Cropping an image requires an image library. The default Python Lambda runtime does not include a full image processing library, so we add Pillow.
Where the layer runs?
When you attach a layer:
- Lambda mounts it under
/opt - For Python, Lambda automatically loads packages placed under
/opt/python
Note
That’s why our zip must contain a top-level folder named python/.
What we are building (layer structure)
Your final zip must look like this:
pillow-layer.zip
└─ python/
├─ PIL/
├─ Pillow-*.dist-info/
└─ ...Note
If python/ is missing at the root of the zip, the import will fail.
Prerequisites
You’ll need:
- AWS CLI configured
- Your Lambda runtime confirmed (example:
python3.14) - Docker installed (recommended for all OS)
Why Docker is recommended
Lambda runs on Amazon Linux. Some Python libraries (including Pillow) contain native binaries, so building inside a Linux-compatible environment avoids “works locally but fails on Lambda”.
Steps
Step 1: Create the layer folder
Create a folder for the layer and a python/ directory inside it.
mkdir pillow-layer
cd pillow-layer
mkdir pythonmkdir -p pillow-layer/python
cd pillow-layermkdir -p pillow-layer/python
cd pillow-layerStep 2: Install Pillow into python/ (Linux-compatible build)
This installs Pillow into the folder that will become the layer content.
docker run --rm `
-v "${PWD}:/var/task" `
-w /var/task `
public.ecr.aws/sam/build-python3.14:latest `
/bin/sh -c "python -m pip install --upgrade pip && pip install pillow -t python"docker run --rm \
-v "$(pwd):/var/task" \
-w /var/task \
public.ecr.aws/sam/build-python3.14:latest \
/bin/sh -c "python -m pip install --upgrade pip && pip install pillow -t python"docker run --rm \
-v "$(pwd):/var/task" \
-w /var/task \
public.ecr.aws/sam/build-python3.14:latest \
/bin/sh -c "python -m pip install --upgrade pip && pip install pillow -t python"You should see a folder with name python/PIL/
Step 3: Zip the layer (zip must contain python/ at root)
Compress-Archive -Path .\python -DestinationPath .\pillow-layer.zip -Forcezip -r pillow-layer.zip pythonzip -r pillow-layer.zip pythonYou should see pillow-layer.zip and inside it, the first folder should be python/.
Step 4: Publish the layer
This uploads the zip as a new Layer Version in your region.
Match your Lambda runtime
If your Lambda runtime is not python3.14, change the flag --compatible-runtimes accordingly.
aws lambda publish-layer-version `
--layer-name beetroot-pillow-py314 `
--zip-file fileb://pillow-layer.zip `
--compatible-runtimes python3.14 `
--region us-east-1aws lambda publish-layer-version \
--layer-name beetroot-pillow-py314 \
--zip-file fileb://pillow-layer.zip \
--compatible-runtimes python3.14 \
--region us-east-1aws lambda publish-layer-version \
--layer-name beetroot-pillow-py314 \
--zip-file fileb://pillow-layer.zip \
--compatible-runtimes python3.14 \
--region us-east-1You'll get a response containing a LayerVersionArn.
Step 5: Attach the layer to beetroot-ingest
Go to:
- Lambda → Functions →
beetroot-ingest - Scroll to Layers
- Click Add a layer
- Choose Custom layers
- Select
beetroot-pillow-py314(latest version) - Click Add
The function now shows the layer in its Layers section.
Quick Test
Add these two lines temporarily near the top of your Lambda code:
from PIL import Image
print("PIL OK", Image.__version__)Go to Test Tab --> Click on Test button --> Check the latest CloudWatch logs
Expected: A log line such as PIL OK 12.x.x
Common issues
If you see No module named 'PIL':
- the layer is not attached, or
- the zip does not contain
python/at the root.
The most common mistake is zipping the folder itself instead of its contents.
Your zip must start with: python/
If your Lambda runtime is python3.11 but the layer is published
for python3.14, you may hit compatibility issues. Publish the
layer using the same runtime as your function.
If you see an error like:
docker: error during connect ... dockerDesktopLinuxEngine ... The system cannot find the file specified,
it usually means Docker Desktop isn’t running or Linux containers/WSL2 engine isn’t started.
Start Docker Desktop, ensure Linux containers mode is enabled, then retry the command.