In first post of this series, I introduced my Cloud Native Buildpack for AWS Lambda PHP (Bref).

Here I want to provide some technical details on how it works so far.

Lambda's /var/task problem

CNB places the application at /workspace. Lambda requires it at /var/task. And you can't override LAMBDA_TASK_ROOT, as it's a reserved environment variable.

The buildpack runs as a non-root user during certain CNB phases, so it can't write to /var/task either. The solution is a thin flatten step after the CNB build that copies /workspace to /var/task and restores the Bref entrypoint. This is handled automatically by make lambda.

Is it elegant? Not perfectly. But it works, and the developer never sees it.

Extension installation: three fallback strategies

Bref's extra PHP extensions are published as Docker images (e.g., bref/extra-gd-php-84:3). Each image is a FROM scratch layer containing just the .so file and its dependencies under /opt.

The buildpack uses a three-tier strategy to install extensions:

Method 1: crane. It uses go-containerregistry to pull and extract the extension image directly from Docker Hub, no Docker daemon needed. Works great in CI. However, bref-extra only publishes x86_64 images (ARM64 is not supported by the bref-extra project).

Method 2: docker create + cp. It falls back to the Docker daemon if crane fails. Same limitation: x86 only.

Method 3: Compile from source. When neither method works (ARM deployments), the buildpack compiles extensions from source using the Bref build image's toolchain. This is the same approach used in multi-stage Dockerfiles but automated via build recipes.

The recipes file (lib/extension-recipes.sh) defines how to build each extension: what system packages to install, what configure flags to pass, where to download external source. Currently supports gd, redis, imagick, amqp, soap, ftp, gmp, pgsql, uuid, yaml, mongodb, calendar, and exif.

How bref-extra extensions are built

Looking at the bref-extra repo, each extension uses a two-stage Dockerfile:

FROM bref/build-php-$PHP_VERSION:$BREF_VERSION AS ext
# compile the extension...
RUN cp "$(php-config --extension-dir)/redis.so" /tmp/redis.so
RUN php /bref/lib-copy/copy-dependencies.php /tmp/redis.so /tmp/extension-libs

FROM scratch
COPY --from=ext /tmp/redis.so /opt/bref/extensions/redis.so
COPY --from=ext /tmp/ext.ini /opt/bref/etc/php/conf.d/ext-redis.ini
COPY --from=ext /tmp/extension-libs /opt/lib

Enter fullscreen mode Exit fullscreen mode

The final published image is literally just /opt with the compiled artifacts. That's what crane extracts during our build. And for ARM, our recipes replicate that first stage at build time.