Automating AWS Lambda Deployments with GitHub Actions and Shared Layers
A mini‑case study on automating Lambda deployments and centralizing helper functions in a shared layer.
Published on Apr 20, 2025
Table of Contents
Introduction to Lambda Automation
Goals of Automating Lambda Deployments
- Central helpers: Keep all reusable code in one common package so each of the 20 Lambda functions uses the same helpers.
- Fewer manual steps: Eliminate the need to upload a new layer and update each Lambda by hand, saving time and reducing errors.
- Secure CI/CD: Use GitHub Actions with AWS OIDC and repository secrets for safe, automatic deployments whenever I push changes.
Organizing Your Lambda Deployment Project
To keep everything clear and easy to navigate, the project follows this top-level structure:
bookkeepingsupportpro-lambda/
├── .github/
│ └── workflows/
│ └── main.yml
├── zip_lambdas.sh
├── deploy_lambdas.sh
├── common-helpers/
│ └── nodejs/
│ └── node_modules/
│ └── common/
│ ├── index.mjs
│ ├── appointmentHelpers.mjs
│ ├── cognitoHelpers.mjs
│ ├── dateTimeHelpers.mjs
│ ├── ...
│ ├── stripeHelpers.mjs
│ ├── node_modules/
│ │ └── stripe/
│ └── package.json
├── package.json
├── README.md
├── .gitignore
├── .gitattributes
└── <Lambda function folders> (20 total)
├── CreateAppointment/
├── CreateStripeCheckoutSession/
├── DeleteProviderAvailability/
├── GetAppointments/
├── GetAvailableDates/
└── ...
Here’s what each part does:
.github/workflows/main.yml
: Defines the GitHub Actions workflow that triggers on pushes tomain
.zip_lambdas.sh
&deploy_lambdas.sh
: Scripts that package changed Lambdas and deploy both my functions and the shared helpers layer.common-helpers/
: Contains the shared helper code undernodejs/node_modules/common
, including any third‑party modules like Stripe.package.json
,README.md
,.gitignore
,.gitattributes
: Standard files for project metadata, documentation, and Git configuration.- <Lambda function folders>: Each folder (20 total) holds one Lambda, named by its action and purpose, e.g.
CreateAppointment/
orGetAvailableDates/
.
Sharing Code Using Lambda Layers
To share helpers across all functions, I bundle the common-helpers
folder into a Lambda layer:
- Install dependencies: In
common-helpers/nodejs/node_modules/common
, runnpm install
so modules like Stripe live undernode_modules/common/node_modules
. - Automatic zipping: The
zip_lambdas.sh
script zips every directory—includingcommon-helpers
—into separate.zip
files, so no manual zipping is needed. - Publish layer: The deploy script calls
aws lambda publish-layer-version --compatible-runtimes nodejs22.x
, capturing the new ARN automatically. - Import in functions: The root
index.mjs
file re-exports all helper modules (e.g., appointmentHelpers, dynamoHelpers, stripeHelpers), and with"type": "module"
incommon/package.json
, Lambdas can simply useimport { helper } from 'common';
. - Version management: AWS assigns incremental version numbers. I manually remove older versions, keeping only the latest and previous for rollback safety.
Since this is a new project not yet in production, I push changes to GitHub and let the GitHub Actions workflow deploy the layer and functions to AWS; then I test my updates from my website running on localhost.
Example Lambda: Creating Appointments
This Lambda function handles booking appointments by using shared helpers for core tasks:
import {
verifyUserAccess,
getBookkeeperAppointments,
findAppointmentOverlap,
storeEntity
} from 'common';
1. Access Control: Ensure the user is allowed to book:
await verifyUserAccess(event.requestContext.authorizer);
2. Fetch Existing Appointments: Get current bookings for the provider:
const existing = await getBookkeeperAppointments({
providerId,
date: appointmentDate
});
3. Overlap Detection: Prevent double-booking by checking:
if (await findAppointmentOverlap({
providerId,
appointmentDate,
appointmentTime
})) {
return {
statusCode: 409,
body: 'Time slot unavailable'
};
}
4. Store the Appointment: Save the new booking record:
const appointmentRecord = { appointmentId, providerId, appointmentDate, appointmentTime };
await storeEntity('AppointmentsTable', appointmentRecord);
I felt relieved replacing inline logic with these helper calls. The handler now clearly shows the booking steps without clutter.
Efficiently Zipping Lambdas
Before pushing changes, I run ./zip_lambdas.sh
from the project root. This script:
- Loops through each directory and removes any old
.zip
file. - Checks for Git-tracked changes using
git ls-files
andgit diff
to skip unchanged directories. - Zips only when needed by entering each folder, zipping its contents, and creating the corresponding
.zip
at the root.
#!/bin/bash
for dir in */ ; do
dirname=$(basename "$dir")
if [ -f "${dirname}.zip" ]; then
rm -f "${dirname}.zip"
fi
if git ls-files --error-unmatch "$dir" > /dev/null 2>&1; then
if git diff --quiet HEAD -- "$dir"; then
echo "No changes in $dirname, skipping."
continue
fi
fi
echo "Changes detected in $dirname, creating zip..."
pushd "$dir" > /dev/null
zip -r "../${dirname}.zip" .
popd > /dev/null
done
Running this locally after edits ensures only changed Lambdas and the common package are packaged, keeping deployments fast and focused.
Automating Lambda Deployment with GitHub Actions
To make deployments hands‑off, I set up a GitHub Actions workflow that runs on every push to the main
branch, using these steps:
name: Deploy Lambdas and Layer
on:
push:
branches:
- main
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v3
with:
role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y zip jq
- name: Deploy Lambdas and Layer
env:
AWS_REGION: ${{ secrets.AWS_REGION }}
run: |
chmod +x ./deploy_lambdas.sh
./deploy_lambdas.sh
Deploying Lambdas and AWS Lambda Layers
With zip files in place, ./deploy_lambdas.sh
takes over to publish the shared helpers layer and update each Lambda:
- Publish new layer version: Checks for
common-helpers.zip
and runsaws lambda publish-layer-version
, capturingLAYER_VERSION_ARN
. - Update function code: Loops through all
*.zip
files (excluding the common layer), updates each function usingaws lambda update-function-code
, and waits forLastUpdateStatus
to be Successful. - Update tagged functions: If
LAYER_VERSION_ARN
is set, finds all Lambdas taggedproject-name=bookkeepingsupportpro
, then updates their configuration with the new layer viaaws lambda update-function-configuration --layers
, again waiting for success.
#!/bin/bash
# Publish layer if common-helpers.zip exists
if [ -f "$COMMON_HELPERS" ]; then
layer_response=$(aws lambda publish-layer-version \
--layer-name "$LAYER_NAME" \
--zip-file fileb://"$COMMON_HELPERS" \
--compatible-runtimes nodejs22.x \
--region "$AWS_REGION")
LAYER_VERSION_ARN=$(echo "$layer_response" | jq -r '.LayerVersionArn')
fi
# Update each function's code and wait for completion
for zip_file in *.zip; do
[ "$zip_file" == "$COMMON_HELPERS" ] && continue
fn="${zip_file%.zip}"
aws lambda update-function-code --function-name "$fn" --zip-file fileb://"$zip_file" --region "$AWS_REGION"
wait_for_update "$fn"
done
# Update layer for tagged Lambdas
if [ -n "$LAYER_VERSION_ARN" ]; then
get_lambdas_by_tag
for arn in "${lambda_arns[@]}"; do
aws lambda update-function-configuration \
--function-name "$arn" \
--layers "$LAYER_VERSION_ARN" \
--region "$AWS_REGION"
wait_for_update "$arn"
done
fi
This script ensures every Lambda always uses the newest shared code, with retries and status checks to catch failures early.
Results and Benefits
Updating 20 Lambda functions and publishing a new layer manually took over 20 minutes. With automation, deployments now finish in 2–3 minutes.
Previously, I sometimes missed updating a layer reference and only discovered it during testing. Now, every function reliably uses the latest helpers without manual checks.
There’s no more dread or late-night anxiety about pushing changes. I felt relieved when the pipeline first ran flawlessly and slept better that night, knowing I had regained headspace for real development.
Key Takeaways & Best Practices
Here are some lessons and tips I gathered:
- Naming & Tagging Conventions: Name Lambdas by HTTP action and path, zip files match directory names, and consistently tag all resources with
project-name=bookkeepingsupportpro
. - Secure OIDC Setup: Use
aws-actions/configure-aws-credentials@v3
with a minimal IAM role. In my case, I need S3 full access and only Lambda permissions to update code, publish layers, and list functions/tags. - Layer Version Management: AWS auto‑increments version numbers; manually prune old versions so only the latest and previous remain for quick rollback.
- Error Handling & Monitoring: Use the
wait_for_update
function to poll forLastUpdateStatus
, monitor each Lambda’s CloudWatch logs, and review GitHub Actions logs for deployment errors. - Local Testing & CI/CD Hygiene: Run
./zip_lambdas.sh
after edits, commit and push tomain
, let the Actions workflow deploy, then test on localhost. - Future Improvements: Move zipping into the CI workflow and automate pruning of old layer versions to streamline maintenance even further.
Following these practices keeps deployments secure, efficient, and easy to manage as the project grows.