How to Deploy AWS Lambda Functions Using GitHub Actions:Complete CI/CD Automation Guide.

Table of Contents

Introduction

Deploying AWS Lambda functions manually can be time-consuming and error-prone, especially when managing multiple serverless functions across different environments. GitHub Actions provides a powerful way to automate this process, ensuring consistent deployments and reducing human error in your CI/CD pipeline.
This comprehensive guide shows you how to set up an automated CI/CD pipeline that detects Lambda function folders in your repository and automatically deploys them to AWS. The solution uses a smart folder naming convention and a Python deployment script that handles create, update, and delete operations automatically for serverless application deployment.
At KubeNine Consulting, we specialize in helping organizations implement robust CI/CD pipelines for AWS Lambda functions and serverless applications. Our team of experienced DevOps engineers and cloud architects has successfully automated deployment processes for hundreds of Lambda functions, delivering significant improvements in deployment speed, reliability, and operational efficiency.

Architecture Overview

The deployment flow works as follows:

Prerequisites

Before implementing this solution, ensure you have:

  • AWS CLI configured with appropriate permissions
  • GitHub repository with GitHub Actions enabled
  • AWS IAM role with Lambda deployment permissions
  • Basic understanding of GitHub Actions workflows

Repository Structure

Organize your Lambda functions using this folder structure:

your-repo/
├── .github/
│   └── workflows/
│       └── deploy-lambda.yml
├── aws-lambda-user-service/
│   ├── index.js
│   ├── package.json
│   └── requirements.txt
├── aws-lambda-auth-service/
│   ├── index.js
│   └── package.json
├── lambda.py
└── README.md

The key is using the aws-lambda-<id> naming convention for your Lambda function folders.

GitHub Actions Workflow

Create the workflow file at .github/workflows/deploy-lambda.yml:

name: Deploy Lambda Functions

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ${{ secrets.AWS_REGION }}
        
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
        
    - name: Install AWS CLI
      run: |
        curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
        unzip awscliv2.zip
        sudo ./aws/install
        
    - name: Detect Lambda Function Changes
      id: detect-changes
      run: |
        # Get list of changed files
        CHANGED_FILES=$(git diff --name-only ${{ github.event.before }} ${{ github.sha }})
        
        # Find Lambda function folders that were modified
        LAMBDA_FUNCTIONS=""
        DELETED_FUNCTIONS=""
        
        for file in $CHANGED_FILES; do
          if [[ $file =~ ^aws-lambda-[^/]+/ ]]; then
            function_name=$(echo $file | cut -d'/' -f1)
            if [[ ! " $LAMBDA_FUNCTIONS " =~ " $function_name " ]]; then
              LAMBDA_FUNCTIONS="$LAMBDA_FUNCTIONS $function_name"
            fi
          fi
        done
        
        # Check for deleted Lambda function folders
        # Get list of deleted files
        DELETED_FILES=$(git diff --name-only --diff-filter=D ${{ github.event.before }} ${{ github.sha }})
        for file in $DELETED_FILES; do
          if [[ $file =~ ^aws-lambda-[^/]+/ ]]; then
            function_name=$(echo $file | cut -d'/' -f1)
            if [[ ! " $DELETED_FUNCTIONS " =~ " $function_name " ]]; then
              DELETED_FUNCTIONS="$DELETED_FUNCTIONS $function_name"
            fi
          fi
        done
        
        # Only proceed if there are actual Lambda function changes
        if [ -z "$LAMBDA_FUNCTIONS" ] && [ -z "$DELETED_FUNCTIONS" ]; then
          echo "No Lambda function changes detected, skipping deployment"
          echo "functions=" >> $GITHUB_OUTPUT
          echo "deleted_functions=" >> $GITHUB_OUTPUT
          exit 0
        fi
        
        echo "functions=$LAMBDA_FUNCTIONS" >> $GITHUB_OUTPUT
        echo "deleted_functions=$DELETED_FUNCTIONS" >> $GITHUB_OUTPUT
        echo "Detected functions: $LAMBDA_FUNCTIONS"
        echo "Detected deleted functions: $DELETED_FUNCTIONS"
        
    - name: Deploy Lambda Functions
      run: |
        # Deploy updated/new functions
        if [ -n "${{ steps.detect-changes.outputs.functions }}" ]; then
          for function in ${{ steps.detect-changes.outputs.functions }}; do
            echo "Deploying function: $function"
            python lambda.py "$function"
          done
        fi
        
        # Delete removed functions
        if [ -n "${{ steps.detect-changes.outputs.deleted_functions }}" ]; then
          for function in ${{ steps.detect-changes.outputs.deleted_functions }}; do
            echo "Deleting function: $function"
            python lambda.py "$function" "delete"
          done
        fi
        
        # If no functions to process
        if [ -z "${{ steps.detect-changes.outputs.functions }}" ] && [ -z "${{ steps.detect-changes.outputs.deleted_functions }}" ]; then
          echo "No Lambda functions to deploy or delete"
        fi
      env:
        GITHUB_SHA: ${{ github.sha }}
        GITHUB_REF: ${{ github.ref }}

Lambda Deployment Script

Create the lambda.py script in your repository root:

#!/usr/bin/env python3

"""
Lambda deployment script for GitHub Actions
Automatically creates, updates, or deletes Lambda functions based on arguments
"""

import os
import sys
import json
import subprocess
import zipfile
import tempfile
import shutil
from pathlib import Path
from typing import Optional

# Configuration
AWS_REGION = os.getenv("AWS_REGION", "us-east-1")
LAMBDA_RUNTIME = "nodejs18.x"
LAMBDA_HANDLER = "index.handler"
LAMBDA_TIMEOUT = 30
LAMBDA_MEMORY = 128

def log_info(message: str):
    """Log info message with green color"""
    print(f"\033[0;32m[INFO]\033[0m {message}")

def log_warn(message: str):
    """Log warning message with yellow color"""
    print(f"\033[1;33m[WARN]\033[0m {message}")

def log_error(message: str):
    """Log error message with red color"""
    print(f"\033[0;31m[ERROR]\033[0m {message}")

def run_command(command: list, cwd: Optional[str] = None) -> bool:
    """Run shell command and return success status"""
    try:
        result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, check=True)
        return True
    except subprocess.CalledProcessError as e:
        log_error(f"Command failed: {' '.join(command)}")
        log_error(f"Error: {e.stderr}")
        return False

def lambda_exists(function_name: str) -> bool:
    """Check if Lambda function exists"""
    command = [
        "aws", "lambda", "get-function",
        "--function-name", function_name,
        "--region", AWS_REGION
    ]
    return run_command(command)

def create_deployment_package(folder_path: str, function_name: str) -> Optional[str]:
    """Create deployment package for Lambda function"""
    temp_dir = tempfile.mkdtemp()
    package_path = os.path.join(temp_dir, f"{function_name}.zip")
    
    try:
        # Copy function files to temp directory
        shutil.copytree(folder_path, os.path.join(temp_dir, function_name), dirs_exist_ok=True)
        
        # Handle different runtime types
        if os.path.exists(os.path.join(folder_path, "package.json")):
            log_info("Installing Node.js dependencies...")
            if not run_command(["npm", "install", "--production"], cwd=os.path.join(temp_dir, function_name)):
                return None
        elif os.path.exists(os.path.join(folder_path, "requirements.txt")):
            log_info("Installing Python dependencies...")
            if not run_command(["pip", "install", "-r", "requirements.txt", "-t", "."], cwd=os.path.join(temp_dir, function_name)):
                return None
        
        # Create zip file
        with zipfile.ZipFile(package_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
            for root, dirs, files in os.walk(os.path.join(temp_dir, function_name)):
                for file in files:
                    file_path = os.path.join(root, file)
                    arcname = os.path.relpath(file_path, os.path.join(temp_dir, function_name))
                    zipf.write(file_path, arcname)
        
        return package_path
    
    except Exception as e:
        log_error(f"Failed to create deployment package: {e}")
        return None

def create_lambda(function_name: str, folder_path: str) -> bool:
    """Create new Lambda function"""
    log_info(f"Creating Lambda function: {function_name}")
    
    package_path = create_deployment_package(folder_path, function_name)
    if not package_path:
        return False
    
    try:
        # Get AWS account ID
        result = subprocess.run(
            ["aws", "sts", "get-caller-identity", "--query", "Account", "--output", "text"],
            capture_output=True, text=True, check=True
        )
        account_id = result.stdout.strip()
        
        # Create Lambda function
        command = [
            "aws", "lambda", "create-function",
            "--function-name", function_name,
            "--runtime", LAMBDA_RUNTIME,
            "--handler", LAMBDA_HANDLER,
            "--role", f"arn:aws:iam::{account_id}:role/lambda-execution-role",
            "--zip-file", f"fileb://{package_path}",
            "--timeout", str(LAMBDA_TIMEOUT),
            "--memory-size", str(LAMBDA_MEMORY),
            "--region", AWS_REGION
        ]
        
        if run_command(command):
            log_info(f"Lambda function {function_name} created successfully")
            return True
        return False
    
    finally:
        # Clean up
        if os.path.exists(package_path):
            os.remove(package_path)
        shutil.rmtree(temp_dir, ignore_errors=True)

def update_lambda(function_name: str, folder_path: str) -> bool:
    """Update existing Lambda function"""
    log_info(f"Updating Lambda function: {function_name}")
    
    package_path = create_deployment_package(folder_path, function_name)
    if not package_path:
        return False
    
    try:
        # Update Lambda function code
        command = [
            "aws", "lambda", "update-function-code",
            "--function-name", function_name,
            "--zip-file", f"fileb://{package_path}",
            "--region", AWS_REGION
        ]
        
        if run_command(command):
            log_info(f"Lambda function {function_name} updated successfully")
            return True
        return False
    
    finally:
        # Clean up
        if os.path.exists(package_path):
            os.remove(package_path)
        shutil.rmtree(temp_dir, ignore_errors=True)

def delete_lambda(function_name: str) -> bool:
    """Delete Lambda function"""
    log_warn(f"Deleting Lambda function: {function_name}")
    
    command = [
        "aws", "lambda", "delete-function",
        "--function-name", function_name,
        "--region", AWS_REGION
    ]
    
    if run_command(command):
        log_info(f"Lambda function {function_name} deleted successfully")
        return True
    return False

def main():
    """Main execution function"""
    if len(sys.argv) < 2:
        log_error("Usage: python lambda.py <function_name> [action]")
        log_error("Actions: create, update, delete (default: auto-detect)")
        sys.exit(1)
    
    function_name = sys.argv[1]
    action = sys.argv[2] if len(sys.argv) > 2 else "auto"
    
    # Validate function name format
    if not function_name.startswith("aws-lambda-"):
        log_error("Function name must start with 'aws-lambda-'")
        sys.exit(1)
    
    folder_path = function_name
    
    if not os.path.exists(folder_path):
        log_error(f"Function folder {folder_path} not found")
        sys.exit(1)
    
    log_info(f"Processing function: {function_name}")
    
    if action == "create":
        if lambda_exists(function_name):
            log_error(f"Lambda function {function_name} already exists")
            sys.exit(1)
        create_lambda(function_name, folder_path)
    
    elif action == "update":
        if not lambda_exists(function_name):
            log_error(f"Lambda function {function_name} does not exist")
            sys.exit(1)
        update_lambda(function_name, folder_path)
    
    elif action == "delete":
        if not lambda_exists(function_name):
            log_error(f"Lambda function {function_name} does not exist")
            sys.exit(1)
        delete_lambda(function_name)
    
    elif action == "auto":
        # Auto-detect: create if doesn't exist, update if it does
        if lambda_exists(function_name):
            log_info(f"Lambda function {function_name} exists, updating...")
            update_lambda(function_name, folder_path)
        else:
            log_info(f"Lambda function {function_name} does not exist, creating...")
            create_lambda(function_name, folder_path)
    
    else:
        log_error(f"Invalid action: {action}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Testing the Deployment

To test your setup:

  1. Create a new Lambda function:
mkdir aws-lambda-test-function
cd aws-lambda-test-function
echo 'exports.handler = async (event) => { return { statusCode: 200, body: "Hello World" }; };' > index.js
echo '{"name": "test-function", "version": "1.0.0"}' > package.json
cd ..
git add .
git commit -m "Add test Lambda function"
git push

Result: GitHub Actions detects the new folder and calls python lambda.py "aws-lambda-test-function" (auto mode creates the function)

  1. Update an existing function:
cd aws-lambda-test-function
echo 'exports.handler = async (event) => { return { statusCode: 200, body: "Updated Hello World" }; };' > index.js
cd ..
git add .
git commit -m "Update test Lambda function"
git push

Result: GitHub Actions detects changes in the folder and calls python lambda.py "aws-lambda-test-function" (auto mode updates the function)

  1. Delete a function:
rm -rf aws-lambda-test-function
git add .
git commit -m "Remove test Lambda function"
git push

Result: GitHub Actions detects the deleted folder and calls python lambda.py "aws-lambda-test-function" "delete" (explicitly deletes the function)

Conclusion

This automated Lambda deployment solution provides several benefits:

  • Consistency: All deployments follow the same process
  • Speed: No manual deployment steps required
  • Reliability: Reduces human error in deployment
  • Scalability: Easy to manage multiple Lambda functions
  • Version Control: All deployments are tracked in Git

The solution automatically handles create, update, and delete operations based on your repository structure, making it ideal for teams managing multiple Lambda functions. The folder naming convention ensures clear organization, while the deployment script handles the complexity of AWS interactions.

For production use, consider adding:

  • Environment-specific configurations
  • Rollback capabilities
  • Deployment notifications
  • Performance monitoring integration
  • Security scanning in the pipeline

This approach scales well as your Lambda function portfolio grows and provides a solid foundation for serverless application deployment.