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:
- 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)
- 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)
- 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.