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

    After having more than a handful of Lambdas, I noticed common code between them and wanted a way to maintain reusable helper functions in one place. After doing some research, I landed on using a Lambda Layer to maintain a common package with reusable helper functions. In this case study, you’ll see how I automated publishing new layer versions and updating all my Lambda functions with GitHub Actions so every function always uses the latest shared code without manual effort.

    Goals of Automating Lambda Deployments

    In this project, I had three main goals:
    • 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.
    By reaching these goals, I reclaimed developer time, gained peace of mind, and made sure my Lambdas always point to the latest shared code.

    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 to main.
    • 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 under nodejs/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/ or GetAvailableDates/.

    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, run npm install so modules like Stripe live under node_modules/common/node_modules.
    • Automatic zipping: The zip_lambdas.sh script zips every directory—including common-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" in common/package.json, Lambdas can simply use import { 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 and git 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 runs aws lambda publish-layer-version, capturing LAYER_VERSION_ARN.
    • Update function code: Loops through all *.zip files (excluding the common layer), updates each function using aws lambda update-function-code, and waits for LastUpdateStatus to be Successful.
    • Update tagged functions: If LAYER_VERSION_ARN is set, finds all Lambdas tagged project-name=bookkeepingsupportpro, then updates their configuration with the new layer via aws 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 for LastUpdateStatus, 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 to main, 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.

    Share this article: