Beetroot LogoBeetroot

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 python
mkdir -p pillow-layer/python
cd pillow-layer
mkdir -p pillow-layer/python
cd pillow-layer

Step 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 -Force
zip -r pillow-layer.zip python
zip -r pillow-layer.zip python

You 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-1
aws lambda publish-layer-version \
  --layer-name beetroot-pillow-py314 \
  --zip-file fileb://pillow-layer.zip \
  --compatible-runtimes python3.14 \
  --region us-east-1
aws lambda publish-layer-version \
  --layer-name beetroot-pillow-py314 \
  --zip-file fileb://pillow-layer.zip \
  --compatible-runtimes python3.14 \
  --region us-east-1

You'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.

On this page