Deploying serverless applications to the cloud has never been easier with AWS. You just upload your files as a zip to Lambda, turn on the function URL or integrate with an API Gateway, and voila! you are able to deploy your serverless app.
But, this manual process has risks. You still have to manually check if the code is right. It might be that what you deploy is faulty, which causes your application to break.
Since most developers upload their code to GitHub, what if there was a way to automate all of this checking and deployment with a single push to your repository?
This is where GitHub Actions comes in.
What is GitHub Actions?
GitHub Actions is a continuous integration and continuous delivery (CI/CD) platform built directly into GitHub. It allows you to automate your software development workflows right from where your code lives.
At its core, GitHub Actions uses the following concepts:
Workflows: These are automated processes defined in a YAML file (e.g., .github/workflows/deploy.yml). A workflow can be triggered by an event.
Events: An event is a specific activity in your repository that triggers a workflow. The most common event is a push to a branch, but it can also be a pull_request, a new issue, or even a scheduled cron job.
Jobs: A workflow is made up of one or more jobs. By default, jobs run in parallel. You can also configure them to run sequentially (e.g., a deploy job that runs only if a test job succeeds).
Runners: A runner is a server (a fresh virtual machine) that runs your workflow's jobs. GitHub provides runners for Linux, Windows, and macOS.
Steps: A job is a series of steps. A step can be a simple shell command (like pip install -r requirements.txt) or a pre-built, reusable command called an Action (like actions/checkout to check out your code).
By combining these, you can create a workflow that, upon a push to your main branch, automatically:
- Sets up a clean environment.
- Installs your dependencies.
- Runs your unit tests.
- If the tests pass, it securely deploys your code to AWS Lambda.
Demonstration
Let's first start with a very simple code snippet which we will use in this demonstration
import json
def lambda_handler(event, context):
return {
"statusCode": 200,
"body": json.dumps({
"message": "Hello dev.to article!",
})
}
Code Breakdown (app.py):
def lambda_handler(event, context)
- This is the main function that AWS Lambda will call. event contains data about the trigger (e.g., from an API call), and context contains runtime information.
return { ... }
- The function returns a dictionary. For a Lambda function responding to an HTTP request (like from a Function URL or API Gateway), it must include statusCode (like 200 for "OK") and a body (which must be a string, hence json.dumps).
Prerequisite 2: Your Test File
Let's now create our test file which our CI will use
test_app.py
import app
import json
def test_simple_lambda_handler():
# Call the handler
event = {}
context = {}
response = app.lambda_handler(event, context)
# Check the response
assert response['statusCode'] == 200
body = json.loads(response['body'])
assert "Hello dev.to article!" in body['message']
# We run the test function directly
test_simple_lambda_handler()
print("All tests passed!")
Code Breakdown (test_app.py):
import app
- This line imports our app.py file so we can access its lambda_handler function.
response = app.lambda_handler(event, context)
- We call our Lambda handler directly, passing in empty event and context objects (since our simple function doesn't use them).
assert response['statusCode'] == 200
- This is the core of the test. assert checks if a statement is true. If it's false, the test fails and stops. Here, we check if the statusCode is 200.
assert "..." in body['message']
- We check if the response message contains the text we expect.
test_simple_lambda_handler()
- We call the test function to run it.
print("All tests passed!")
- If all the assert statements pass, this line will run, letting us know our code works as expected.
Upload Your Code to GitHub
Before we can set up GitHub Actions, our code needs to live in a GitHub repository. We'll assume you've already:
Created a new repository on GitHub.
Committed your files (app.py, test_app.py, requirements.txt).
Pushed your code to the main branch.
Create AWS IAM User & Get Keys
Now, let's get the AWS credentials GitHub Actions will use. We'll assume you're familiar with the AWS IAM console.
You'll need to create a new IAM User with CLI access. Give it a descriptive name like github-lambda-deployer.
For permissions, attach the AWSLambda_FullAccess policy.
(Note: For a real-world project, it's best practice to create a more restrictive custom policy that only allows the lambda:UpdateFunctionCode and lambda:UpdateFunctionConfiguration actions on your specific function's ARN.)
After creating the user, generate an Access key and Secret access key. Copy these immediately; you will need them for the next step.
Add Access Keys to GitHub Secrets
Now let's securely store those keys in GitHub.
- In your GitHub repository, go to Settings.
- In the left sidebar, navigate to Secrets and variables > Actions.
- Click the New repository secret button.
Create two secrets, one for the Access Key, and one for the Secret Access Key:
- Click Add secret.
Create the second secret:
- Click Add secret.
You should now have two secrets, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY, listed under "Repository secrets." GitHub Actions can now access them using the ${{ secrets.NAME }} syntax.
Create Lambda Function
Let's now create the AWS Lambda function that we will use.
Create a function with name "blog-func" and Python 3.13 runtime

After it is done creating, Edit the handler from
lambda_handler.lambda_handlertoapp.lamba_handler
Create the GitHub Actions Workflow (CI/CD)
This is the heart of our pipeline. In your repository, create a new directory .github, and inside that, another directory workflows. Finally, create the file deploy.yml inside it. Replace the placeholders in this file before you commit.
aws-region
- Set this to the region where your Lambda function exists (e.g., us-west-2, ap-southeast-1).
--function-name
- Change YOUR-LAMBDA-FUNCTION-NAME to the exact name of your function in AWS.
File Path: .github/workflows/deploy.yml
# Name of your workflow
name: CI-CD for AWS Lambda
# When this workflow is triggered
on:
push:
branches:
- main # Run on pushes to the main branch
pull_request:
branches:
- main # Run on pull requests targeting main
# A workflow run is made up of one or more jobs
jobs:
# The "test" job (Continuous Integration)
test:
name: Run Unit Tests
runs-on: ubuntu-latest # Use a Linux runner
steps:
- name: Checkout repository
uses: actions/checkout@v4 # Action to check out your code
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11' # Use a specific Python version
- name: Install test dependencies
run: |
# No dependencies needed for this simple handler
echo "No test dependencies to install."
- name: Run tests (CI)
run: |
python test_app.py
# The "deploy" job (Continuous Deployment)
deploy:
name: Deploy to AWS Lambda
runs-on: ubuntu-latest
needs: test # This job will ONLY run if the "test" job succeeds
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Create Zip File
run: |
zip lambda_function.zip app.py
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
# Use the secrets you created in Step 3
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-southeast-1 # Change to your Lambda's region
- name: Deploy to AWS Lambda (CD)
run: |
aws lambda update-function-code \
--function-name blog-func \ # Change to Lambda Name
--zip-file fileb://lambda_function.zip
Workflow Breakdown
Let's break down that YAML file.
The Triggers:
on:
push:
branches:
- main
pull_request:
branches:
- main
This tells GitHub when to run the workflow. It will run on any push to the main branch, and also on any pull_request that targets the main branch. This is great for checking if a new change breaks the tests before you merge it.
Job 1: test (The "CI" part)
This job is our Continuous Integration check.
name: Run Unit Tests
- The display name in the Actions tab.
runs-on: ubuntu-latest
- It runs on a fresh Ubuntu virtual machine.
uses: actions/checkout@v4
- This action checks out your repository code onto the runner.
uses: actions/setup-python@v5
- This action sets up the Python version we want.
run: python test_app.py
- This is the key CI step. It runs our test file. If test_app.py fails (e.g., an assert is false), it will exit with an error, which fails the job and stops the entire pipeline.
Job 2: deploy (The "CD" part)
This job is our Continuous Deployment step.
needs: test
- This is the most important line for safety. It tells GitHub: Do not even start this job unless the test job finished successfully. This is what prevents you from deploying broken code.
run: zip lambda_function.zip app.py
- This bundles our app.py into the .zip file that Lambda requires.
uses: aws-actions/configure-aws-credentials@v4
- This is an official action from AWS. It uses the aws-access-key-id and aws-secret-access-key you provide to configure the AWS CLI on the runner.
with: ... ${{ secrets.AWS_ACCESS_KEY_ID }}
- This is how you securely pass your GitHub Secrets to the action. They are injected at runtime and never printed in the logs.
run: aws lambda update-function-code ...
- This is the final AWS CLI command that actually performs the deployment, uploading your new lambda_function.zip to the function you specified.
Commit and Push
Commit all your new files (specifically .github/workflows/deploy.yml) and push them to your main branch. Go to the "Actions" tab in your GitHub repository.
- You will see your "CI-CD for AWS Lambda" workflow start.
It will first run the test job. You can click to see the output, including "All tests passed!".
Once the test job succeeds, the deploy job will begin. You will see it zip the code, configure credentials, and run the aws lambda update-function-code command.
Check AWS: Go to your Lambda function in the AWS Console. You should see that the "Last modified" time has just updated. If you test your function in the console, it will now be running the code from your repository!
You can also see the result by turning on the Function URL and opening it!
Conclusion and Next Steps
You now have a robust and automated CI/CD pipeline. Any time you push a change to your main branch, your code will be automatically tested, and if the tests pass, it will be securely deployed to AWS Lambda. This directly solves the problem of "manially checking" and deploying "faulty" code.
From here, you can expand this pipeline to:
Use different branches for staging and production environments.
Store your Lambda function name as a GitHub Secret instead of hard-coding it.
Add a build step that installs requirements.txt into a folder before zipping.
Run more complex integration tests that actually invoke the Lambda URL.
That is it for this demonstration! Thank you for reading this informative blog, you can check out the code for this in my github











Top comments (0)