Deploy a FastAPI App with CI/CD

Build a CI/CD pipeline that auto-deploys a FastAPI app to AWS.

Introduction

30 Second Summary

Every time a developer finishes writing code, getting it live for users usually means a tedious series of manual steps. One typo in the process can bring down an entire application.

In this project, you will build an automated pipeline that tests and deploys a Python web API to the cloud every time you push code. No manual steps required.

What You'll Build

A FastAPI REST API deployed to AWS Elastic Beanstalk with a fully automated CI/CD pipeline powered by GitHub Actions.

By the end of this project, you'll have:

  • A working FastAPI application with health checks and sample API endpoints.
  • A GitHub Actions workflow that automatically runs tests and deploys your code on every push to main.
  • A live API running on AWS Elastic Beanstalk, accessible via a public URL.
  • Secret Mission: Add a staging environment with manual approval gates before production deployment.

Are there any prerequisites?

This project assumes you are familiar with Python, Git, and basic command line usage. You do not need prior experience with AWS or CI/CD pipelines.

Set Up Your Environment

Before you can build a CI/CD pipeline, you need the infrastructure to support it. That means an AWS account where your app will live, credentials that let GitHub Actions deploy on your behalf, and a local Python project ready to develop.

In this step, you will create an AWS account, set up a dedicated deployment user with the right permissions, and initialize your local project with all the dependencies installed.

In this step, get ready to:
  • Create an AWS account and sign in to the Management Console.
  • Create an IAM user with deployment permissions and save your access keys.
  • Set up a local Python project with FastAPI, Gunicorn, and pytest installed.
Create an AWS account

Your FastAPI app will be deployed to AWS Elastic Beanstalk. To get started, you need an AWS account.

  • Go to https://aws.amazon.com and click Create an AWS Account.
  • Follow the sign-up wizard. Choose the Free plan when prompted.
  • Enter your email, create a password, and provide billing information when asked.
  • After account creation, sign in to the AWS Management Console.

Will I be charged?

A credit card is required during sign-up, but you will not be charged for the resources used in this project. New AWS accounts receive up to $200 in credits ($100 on sign-up, up to $100 as you explore services) that expire 12 months from account creation.

The EC2 instance this project creates is free-tier eligible (750 hours/month of t2.micro for your first 12 months).

Create an IAM user for GitHub Actions

Your CI/CD pipeline needs programmatic access to deploy code to AWS. Instead of using your root account credentials, you will create a dedicated IAM user with only the permissions needed for deployment.

  • In the AWS Management Console, search for IAM in the top search bar and select IAM.
  • In the left navigation pane, click Users.
  • Click Create user.
  • For User name, enter github-actions-deploy.
  • Click Next.

Now you will attach the permissions this user needs to manage Elastic Beanstalk deployments.

  • On the Set permissions page, select Attach policies directly.
  • In the search box, type AdministratorAccess-AWSElasticBeanstalk and check the box next to it.
  • Clear the search box, type AmazonS3FullAccess, and check that box as well.
  • Click Next, then click Create user.

Why these two policies?

The AdministratorAccess-AWSElasticBeanstalk policy grants full control over Elastic Beanstalk environments, applications, and their underlying resources. The AmazonS3FullAccess policy allows the deploy action to upload your application bundle to S3 before EB picks it up.

Next, create an access key so GitHub Actions can authenticate as this user.

  • Click the user name github-actions-deploy to open the user details page.
  • Select the Security credentials tab.
  • In the Access keys section, click Create access key.
  • On the use case page, select Third-party service and click Next.
  • Click Create access key.
  • Copy and save both the Access Key ID and Secret Access Key to a secure location (such as a password manager). You will not be able to view the secret key again after closing this page.

Save your keys now

This is your only opportunity to view the Secret Access Key. If you lose it, you will need to delete this access key and create a new one. Store both values somewhere safe. You will add them to GitHub as secrets in a later step.

Set up your project locally

With your AWS credentials ready, it is time to create the local project where you will build the FastAPI app and CI/CD pipeline.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder, move into it, and initialize a Git repository by running these commands:
mkdir fastapi-cicd
cd fastapi-cicd
git init

What does this do?

You create a folder called fastapi-cicd on your Desktop, move into it, and initialize Git so you can track changes and push to GitHub later.

  • Confirm the folder and Git setup by running:
ls -a

You should see a .git directory in the output. This confirms Git is initialized.

Now create a Python virtual environment to keep your project dependencies isolated.

  • Create a virtual environment by running:
python3 -m venv .venv
  • Activate the virtual environment:

macOS

source .venv/bin/activate

Windows

.venv\Scripts\activate

You should see (.venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.

What is a virtual environment?

A virtual environment creates an isolated space for your Python packages. Packages you install here stay separate from your system Python, preventing version conflicts between projects.

  • Install FastAPI and Gunicorn by running:
pip install "fastapi[standard]" gunicorn
  • Install pytest and httpx (your testing tools) by running:
pip install pytest httpx

What are these packages for?

FastAPI is the web framework you will use to build your API. Gunicorn is the production server that will run your app on AWS. Pytest is a testing framework, and httpx provides the HTTP client that FastAPI's test client uses under the hood.

  • Verify FastAPI installed correctly by running:
python -c "import fastapi; print(fastapi.__version__)"

You should see a version number printed (e.g. 0.136.3). This confirms FastAPI is installed and importable.

Not seeing a version number?

Make sure your virtual environment is active. You should see (.venv) at the start of your terminal prompt. If not, re-run the activation command above.

If you see a ModuleNotFoundError, the pip install command may not have completed successfully. Run it again and check for any error messages.

Help me troubleshoot my FastAPI installation.

Your environment is fully set up. You have an AWS account with deployment credentials, a local Git repository, and all the Python packages you need. Next up, you will build the FastAPI application itself.

Build the FastAPI Application

Your development environment is ready with Python, FastAPI, and all the tools installed. Now it's time to build the actual API that will eventually run on AWS.

In this step, you'll create a FastAPI REST API with multiple endpoints, configure it for AWS Elastic Beanstalk deployment, and write automated tests to verify everything works before it ever reaches the cloud.

In this step, get ready to:
  • Create a FastAPI application with health check and API endpoints.
  • Configure Elastic Beanstalk deployment files.
  • Write automated tests using pytest.
Create the application file

Elastic Beanstalk looks for a file called application.py by default when starting your app. Using this filename means zero extra configuration on the AWS side.

  • Create a new file called application.py in your project root and add the following code:
from fastapi import FastAPI

app = FastAPI(title="FastAPI CI/CD Demo", version="1.0.0")


@app.get("/")
def health_check():
    return {"status": "healthy", "version": "1.0.0"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "query": q}


@app.get("/info")
def app_info():
    return {
        "app_name": "FastAPI CI/CD Demo",
        "description": "A FastAPI app deployed via GitHub Actions to AWS Elastic Beanstalk",
        "endpoints": ["/", "/items/{item_id}", "/info"],
    }

What does this code do?

This file defines your entire API with three endpoints:

  • GET / is a health check that returns the app's status and version. Monitoring tools and load balancers use this to confirm the app is alive.
  • GET /items/{item_id} is a sample endpoint that accepts a path parameter and an optional query string. It demonstrates how FastAPI handles typed parameters.
  • GET /info returns metadata about the API itself, including a list of all available endpoints.
  • Save application.py.
  • Verify the app loads correctly by running:
python -c "from application import app; print(app.title)"

You should see FastAPI CI/CD Demo printed in your terminal.

Seeing a ModuleNotFoundError?

  • Make sure your virtual environment is still activated. You should see (.venv) or (venv) at the start of your terminal prompt.
  • Confirm you're running the command from the same directory where application.py lives.

Help me debug my FastAPI import error

✔️ Awesome, I've got everything!

Great. Make sure you've saved application.py before moving on.

ⓧ I'd like to double check the full code

from fastapi import FastAPI

app = FastAPI(title="FastAPI CI/CD Demo", version="1.0.0")


@app.get("/")
def health_check():
    return {"status": "healthy", "version": "1.0.0"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "query": q}


@app.get("/info")
def app_info():
    return {
        "app_name": "FastAPI CI/CD Demo",
        "description": "A FastAPI app deployed via GitHub Actions to AWS Elastic Beanstalk",
        "endpoints": ["/", "/items/{item_id}", "/info"],
    }
Create the Elastic Beanstalk configuration files

Elastic Beanstalk needs a few configuration files to know how to start your app and which dependencies to install. These files live in your project root alongside application.py.

  • Create a file called Procfile (no file extension) and add this line:
web: gunicorn --worker-class asgi --bind :8000 application:app

What is a Procfile?

The Procfile tells Elastic Beanstalk exactly how to start your application. This line launches Gunicorn (a production web server) with ASGI worker support on port 8000, which is EB's default port.

The --worker-class asgi flag enables Gunicorn's native async worker, which is required for FastAPI since it's an ASGI framework. The application:app part tells Gunicorn to find the app object inside application.py.

  • Create requirements.txt to pin your production dependencies:
fastapi[standard]==0.136.3
gunicorn==26.0.0
  • Create a .ebignore file to exclude unnecessary files from deployment:
.git
.gitignore
tests/
__pycache__/
*.pyc
.venv/
venv/
.github/

Why do we need .ebignore?

The .ebignore file works like .gitignore but specifically for Elastic Beanstalk deployments. It prevents test files, cached bytecode, and your local virtual environment from being uploaded to AWS. This keeps your deployment package small and fast.

  • Save all three files.
  • Confirm the files exist by running:
ls

You should see application.py, Procfile, requirements.txt, and .ebignore listed in the output.

Don't see .ebignore in the output?

Files starting with a dot are hidden by default on macOS and Linux. Run ls -a to see hidden files. If .ebignore still doesn't appear, double-check that you saved the file in the correct directory.

Help me find my .ebignore file

Write automated tests

Automated tests are the safety net that prevents broken code from reaching production. Your CI/CD pipeline will run these tests on every push, and deployment only proceeds if they all pass.

  • Create a tests/ directory by running:
mkdir tests
  • Create a file called test_application.py inside the tests/ directory and add the following code:
from fastapi.testclient import TestClient

from application import app

client = TestClient(app)


def test_health_check():
    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "healthy"
    assert data["version"] == "1.0.0"


def test_read_item():
    response = client.get("/items/42?q=test")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 42
    assert data["query"] == "test"


def test_read_item_no_query():
    response = client.get("/items/1")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 1
    assert data["query"] is None


def test_app_info():
    response = client.get("/info")
    assert response.status_code == 200
    data = response.json()
    assert data["app_name"] == "FastAPI CI/CD Demo"
    assert "/" in data["endpoints"]

What do these tests do?

Each test function hits one of your API endpoints using FastAPI's TestClient and checks the response. No real server starts during testing. The TestClient simulates HTTP requests directly against your app.

  • test_health_check verifies the root endpoint returns a 200 status with the correct JSON body.
  • test_read_item confirms path parameters and query strings are parsed correctly.
  • test_read_item_no_query checks that optional parameters default to None when omitted.
  • test_app_info validates the info endpoint returns the expected app name and endpoint list.
  • Save tests/test_application.py.
  • Run your tests by executing:
pytest

You should see output showing all 4 tests passing with green checkmarks.

Seeing test failures or import errors?

  • Make sure you're running pytest from your project root directory (the folder containing application.py), not from inside the tests/ folder.
  • If you see ModuleNotFoundError: No module named 'httpx', confirm httpx is installed in your virtual environment. TestClient depends on it.
  • Check that your function names, return values, and endpoint paths in application.py match the test assertions exactly.

Help me fix my failing pytest tests

✔️ Awesome, I've got everything!

Great. All 4 tests passing means your API is working correctly and ready for deployment.

ⓧ I'd like to double check the full code

from fastapi.testclient import TestClient

from application import app

client = TestClient(app)


def test_health_check():
    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "healthy"
    assert data["version"] == "1.0.0"


def test_read_item():
    response = client.get("/items/42?q=test")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 42
    assert data["query"] == "test"


def test_read_item_no_query():
    response = client.get("/items/1")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 1
    assert data["query"] is None


def test_app_info():
    response = client.get("/info")
    assert response.status_code == 200
    data = response.json()
    assert data["app_name"] == "FastAPI CI/CD Demo"
    assert "/" in data["endpoints"]

Your FastAPI application is built, configured for deployment, and fully tested. Next up, you'll create the GitHub Actions workflow that automatically runs these tests and deploys your app to AWS on every push.

Create the GitHub Actions CI/CD Pipeline

Your FastAPI app is built and tests are passing locally. Now you need a way to run those tests and deploy your app automatically every time you push code.

This is where CI/CD comes in. You will create a GitHub Actions workflow that watches your main branch and reacts to every push: first running your tests, then deploying to AWS if they pass.

In this step, get ready to:
  • Create the GitHub Actions workflow directory and file.
  • Define a test job that installs dependencies and runs pytest.
  • Define a deploy job that pushes your app to AWS Elastic Beanstalk.
Create the workflow file

GitHub Actions looks for workflow files inside a specific directory path in your repository. The path must be exactly .github/workflows/ at your project root.

  • Create the workflow directory by running this command:
mkdir -p .github/workflows
  • Create the workflow file by running:
touch .github/workflows/deploy.yml
  • Confirm the file exists by running:
ls .github/workflows/

You should see deploy.yml in the output.

  • Open deploy.yml in VS Code and paste in the workflow header:
name: CI/CD Pipeline

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

env:
  AWS_REGION: us-east-1
  APPLICATION_NAME: fastapi-cicd-app
  ENVIRONMENT_NAME: fastapi-cicd-env

What does this code do?

  • name gives your workflow a human-readable label that shows up in the GitHub Actions tab.
  • on defines the triggers. This workflow runs on every push to main and on pull requests targeting main.
  • env sets workflow-level environment variables so you can reuse your AWS region, application name, and environment name across jobs without repeating them.

YAML not highlighting correctly?

Make sure your file is saved with the .yml extension. VS Code uses the extension to apply YAML syntax highlighting.

help me fix YAML syntax highlighting in VS Code

Define the test job

The test job is the first gate in your pipeline. It installs your dependencies and runs pytest exactly like you did locally.

  • In deploy.yml, add the following below the env block:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest httpx

      - name: Run tests
        run: pytest

What does this code do?

  • runs-on: ubuntu-latest tells GitHub to run this job on a fresh Linux virtual machine.
  • actions/checkout@v4 pulls your repository code into the runner so subsequent steps can access it.
  • actions/setup-python@v5 installs Python 3.11 and caches pip packages for faster runs.
  • The install step installs your production dependencies from requirements.txt plus the test tools (pytest and httpx).
  • The final step runs pytest to execute your test suite. If any test fails, the job fails and deployment is blocked.
  • Save deploy.yml.

Seeing red squiggly lines in the YAML?

YAML is extremely sensitive to indentation. Each nested level must use exactly 2 spaces (not tabs). Make sure jobs: is at column 0, test: is indented 2 spaces, and runs-on: is indented 4 spaces.

help me fix YAML indentation errors in my GitHub Actions workflow

Define the deploy job

The deploy job is the second gate. It only runs after the test job passes and only on pushes to main (not on pull requests).

  • In deploy.yml, add the following below the test job (still inside the jobs: block, at the same indentation level as test:):
  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    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: ${{ env.AWS_REGION }}

      - name: Deploy to Elastic Beanstalk
        uses: aws-actions/aws-elasticbeanstalk-deploy@v1.0.0
        with:
          aws-region: ${{ env.AWS_REGION }}
          application-name: ${{ env.APPLICATION_NAME }}
          environment-name: ${{ env.ENVIRONMENT_NAME }}
          solution-stack-name: "64bit Amazon Linux 2023 v4.13.0 running Python 3.11"
          option-settings: |
            [
              {
                "Namespace": "aws:autoscaling:launchconfiguration",
                "OptionName": "IamInstanceProfile",
                "Value": "aws-elasticbeanstalk-ec2-role"
              },
              {
                "Namespace": "aws:elasticbeanstalk:environment",
                "OptionName": "ServiceRole",
                "Value": "aws-elasticbeanstalk-service-role"
              },
              {
                "Namespace": "aws:ec2:instances",
                "OptionName": "InstanceTypes",
                "Value": "t2.micro"
              }
            ]

What does this code do?

  • needs: test makes this job wait for the test job to succeed before starting. If tests fail, the deploy never runs.
  • The if condition ensures deployment only happens on direct pushes to main. Pull requests trigger the test job but skip deployment.
  • aws-actions/configure-aws-credentials@v4 authenticates the runner using the secrets you will add to GitHub in the next step.
  • aws-actions/aws-elasticbeanstalk-deploy@v1.0.0 packages your code and deploys it to Elastic Beanstalk. It auto-creates the application and environment if they do not exist yet.
  • option-settings configures the IAM roles and instance type (t2.micro for free tier eligibility).

Why does the deploy job depend on the test job?

The needs: test line creates a dependency chain. Without it, both jobs would run in parallel and broken code could reach production. This dependency is the safety net that makes CI/CD reliable.

  • Save deploy.yml.

Deploy job not aligned correctly?

The deploy: key must be at the same indentation level as test: (2 spaces from the left margin). If it is nested further in, GitHub will not recognize it as a separate job.

help me fix the indentation of my deploy job in the GitHub Actions workflow

✔️ Awesome, I've got everything!

Great. Double check you have saved deploy.yml with all three sections (header, test job, deploy job).

ⓧ I'd like to double check the full code

name: CI/CD Pipeline

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

env:
  AWS_REGION: us-east-1
  APPLICATION_NAME: fastapi-cicd-app
  ENVIRONMENT_NAME: fastapi-cicd-env

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"

      - name: Install dependencies
        run: |
          pip install -r requirements.txt
          pip install pytest httpx

      - name: Run tests
        run: pytest

  deploy:
    runs-on: ubuntu-latest
    needs: test
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    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: ${{ env.AWS_REGION }}

      - name: Deploy to Elastic Beanstalk
        uses: aws-actions/aws-elasticbeanstalk-deploy@v1.0.0
        with:
          aws-region: ${{ env.AWS_REGION }}
          application-name: ${{ env.APPLICATION_NAME }}
          environment-name: ${{ env.ENVIRONMENT_NAME }}
          solution-stack-name: "64bit Amazon Linux 2023 v4.13.0 running Python 3.11"
          option-settings: |
            [
              {
                "Namespace": "aws:autoscaling:launchconfiguration",
                "OptionName": "IamInstanceProfile",
                "Value": "aws-elasticbeanstalk-ec2-role"
              },
              {
                "Namespace": "aws:elasticbeanstalk:environment",
                "OptionName": "ServiceRole",
                "Value": "aws-elasticbeanstalk-service-role"
              },
              {
                "Namespace": "aws:ec2:instances",
                "OptionName": "InstanceTypes",
                "Value": "t2.micro"
              }
            ]

Your CI/CD pipeline is defined. Next up, you will push this code to GitHub, add your AWS secrets, and watch the pipeline run for the first time.

Configure Secrets and Deploy

Your CI/CD pipeline is written and ready to go. Now it needs two things to actually work: your code on GitHub (so GitHub Actions can access it), and your AWS credentials stored as secrets (so the deploy job can authenticate with AWS).

In this step, you will push your project to GitHub, configure the secrets your workflow needs, and watch the entire pipeline run for the first time.

In this step, get ready to:
  • Push your code to a new GitHub repository.
  • Add AWS credentials as GitHub secrets.
  • Trigger the pipeline and watch your app deploy to AWS.
Push your code to GitHub

GitHub Actions workflows only run on code hosted on GitHub. You need to create a remote repository and push your local project to it.

  • Open GitHub in your browser and sign in.
  • In the upper-right corner of any page, click the + menu and select New repository.
  • Enter fastapi-cicd-app in the Repository name field.
  • Select Public for repository visibility.
  • Do NOT check Add a README file or any other initialization options (you already have local files to push).
  • Click Create repository.

GitHub shows a Quick Setup page with instructions for pushing an existing repository. You will use these commands in your terminal.

  • Commit all your project files and push to GitHub by running these commands:
git add application.py requirements.txt Procfile .ebignore tests/ .github/
git commit -m "Initial commit: FastAPI app with CI/CD pipeline"
git branch -M main
git remote add origin [[REPO_URL="https://github.com/your-username/fastapi-cicd-app.git"]]
git push -u origin main

What do these commands do?

  • git add stages only the project files (not your virtual environment folder).
  • git commit saves the staged files as your first commit.
  • git branch -M main ensures your default branch is named main (matching the workflow trigger).
  • git remote add origin connects your local repo to the GitHub repository you just created.
  • git push -u origin main uploads your code to GitHub and sets main as the default upstream branch.
  • Refresh your GitHub repository page in the browser. You should see all your project files listed (application.py, requirements.txt, Procfile, .ebignore, the tests/ folder, and the .github/ folder).

Push rejected or permission denied?

  • If you see a permission error, make sure you are signed in to the correct GitHub account in your terminal. You may need to configure a personal access token or SSH key.
  • If the push is rejected because the remote contains files you do not have locally, you likely checked Add a README during repo creation. Delete the repository on GitHub and create a new one without any initialization options.
  • help me push my code to GitHub
Add AWS secrets to your GitHub repository

Your workflow file references secrets.AWS_ACCESS_KEY_ID and secrets.AWS_SECRET_ACCESS_KEY. GitHub needs these values stored securely so the deploy job can authenticate with AWS without exposing credentials in your code.

  • In your GitHub repository, click Settings in the top navigation bar.
  • In the left sidebar, click Secrets and variables, then click Actions.
  • Click New repository secret.
  • In the Name field, enter AWS_ACCESS_KEY_ID.
  • In the Secret field, paste the Access Key ID you saved from your IAM user in Step 1.
  • Click Add secret.
  • Click New repository secret again.
  • In the Name field, enter AWS_SECRET_ACCESS_KEY.
  • In the Secret field, paste the Secret Access Key you saved from your IAM user in Step 1.
  • Click Add secret.

Why use GitHub secrets?

GitHub secrets are encrypted and only exposed to workflows at runtime. They never appear in logs or pull request diffs. This keeps your AWS credentials safe while still allowing automated deployments.

Your workflow accesses them with the ${{ secrets.SECRET_NAME }} syntax. GitHub masks these values in job logs automatically.

  • Confirm you see both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY listed on the Actions secrets page.

Don't see the Settings tab?

  • The Settings tab only appears if you are the repository owner. Make sure you are viewing the repository you just created under your own GitHub account.
  • help me find where to add GitHub repository secrets
Trigger the pipeline and watch it deploy

Your pipeline is configured to trigger on every push to main. The push you already did should have triggered the first run. If it ran before your secrets were added, the test job would pass but the deploy job would fail. Let's make sure everything runs cleanly.

  • Before triggering the pipeline, you need to create the aws-elasticbeanstalk-ec2-role and aws-elasticbeanstalk-service-role in your AWS account. AWS no longer creates these automatically on first deployment.

Why do I need to create these roles manually?

AWS updated its security guidelines to prevent services from automatically creating roles with trust policies to other services. On a new account, the aws-elasticbeanstalk-ec2-role instance profile does not exist yet.

Your workflow references this role in the option-settings section. Without it, the deploy job will fail with a missing instance profile error.

  • Switch back to the AWS Management Console.
  • Search for IAM in the search bar and open the IAM service.
  • Click Roles in the left sidebar.
  • Click Create role.
  • For Trusted entity type, select AWS service.
  • For Service or use case, choose Elastic Beanstalk.
  • For Use case, choose Elastic Beanstalk – Compute.
  • Click Next.
  • Verify the permissions policies include AWSElasticBeanstalkWebTier, AWSElasticBeanstalkWorkerTier, and AWSElasticBeanstalkMulticontainerDocker.
  • Click Next.
  • Enter aws-elasticbeanstalk-ec2-role as the Role name.
  • Click Create role.

Now create the service role.

  • Click Create role again.
  • For Trusted entity type, select AWS service.
  • For Service or use case, choose Elastic Beanstalk.
  • For Use case, choose Elastic Beanstalk (the default option, not the Compute one).
  • Click Next.
  • Click Next again (the default policies are pre-selected).
  • Enter aws-elasticbeanstalk-service-role as the Role name.
  • Click Create role.

With both roles created, your pipeline can now provision the Elastic Beanstalk environment. Time to trigger a fresh run.

  • Trigger a fresh pipeline run by pushing an empty commit from your terminal:
git commit --allow-empty -m "Trigger CI/CD pipeline"
git push
  • In your GitHub repository, click the Actions tab.
  • You should see a workflow run named "CI/CD Pipeline" in progress. Click it to view the details.
  • Watch the test job complete first. Once it passes, the deploy job starts.
  • The first deployment takes approximately 5 minutes as AWS provisions the EC2 instance and configures the environment.

What is happening during the deploy job?

The aws-actions/aws-elasticbeanstalk-deploy@v1.0.0 action automatically creates both the Elastic Beanstalk application (fastapi-cicd-app) and the environment (fastapi-cicd-env) if they do not already exist.

Behind the scenes, AWS is launching a t2.micro EC2 instance, installing Python 3.11, setting up nginx as a reverse proxy, and starting your FastAPI app with Gunicorn.

  • Once the deploy job completes (green checkmark), find your application URL in the deploy job logs. Look for a line containing the .elasticbeanstalk.com domain.
  • You can also find it in the AWS Console. Search for Elastic Beanstalk, click your fastapi-cicd-env environment, and look for the Domain field at the top.
  • Open the URL in your browser. You should see the JSON response: {"status": "healthy", "version": "1.0.0"}.

Deploy job failed?

  • If you see an error mentioning a missing instance profile, double-check that you created the aws-elasticbeanstalk-ec2-role role with exactly that name (no extra spaces). The role must exist before the deploy action runs.
  • If you see a credentials error, verify that both AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are added correctly in your repository secrets. Watch for trailing spaces when pasting.
  • If the solution stack name is not found, the platform version may have been retired. Check the Elastic Beanstalk console for the current Python 3.11 platform version in us-east-1 and update your workflow file.
  • help me debug my failed Elastic Beanstalk deployment in GitHub Actions

Your CI/CD pipeline is live. Every push to main now triggers an automatic test-and-deploy cycle. Next up, you will prove this works by making a code change and watching the full automated cycle from push to production.

Verify the Full CI/CD Cycle

Your pipeline is live and your API is running on AWS. But a CI/CD pipeline is only valuable if it responds to changes automatically.

In this step, you will push a code change and watch the full cycle in action: push, test, deploy, live. Then you will prove the safety net works by showing that a failing test blocks deployment.

In this step, get ready to:
  • Make a version change to your API and push it to trigger the pipeline.
  • Watch the automated test-and-deploy cycle update your live application.
  • Prove that failing tests block deployment by opening a pull request with a broken test.
Make a code change

The simplest way to prove your pipeline works end-to-end is to change something visible in the API response. You will bump the version number from "1.0.0" to "1.1.0" in the health check, update the test to match, and push the change.

  • In VS Code, open application.py.
  • Find this line inside the health_check function:
    return {"status": "healthy", "version": "1.0.0"}
  • Replace "1.0.0" with "1.1.0", so the line now looks like this:
    return {"status": "healthy", "version": "1.1.0"}
  • Now open tests/test_application.py.
  • Find this assertion inside the test_health_check function:
    assert data["version"] == "1.0.0"
  • Replace "1.0.0" with "1.1.0", so the line now looks like this:
    assert data["version"] == "1.1.0"
  • Save both files.
  • Commit and push your changes to main by running these commands:
git add .
git commit -m "Bump version to 1.1.0"
git push origin main

What does this trigger?

Pushing to main triggers your GitHub Actions workflow automatically. The test job will run your updated tests against the updated code. If tests pass, the deploy job sends the new version to Elastic Beanstalk.

✔️ Awesome, I've got everything!

Great. Double check you have saved both files before moving on.

ⓧ I'd like to double check the full code

Here is the complete application.py file:

from fastapi import FastAPI

app = FastAPI(title="FastAPI CI/CD Demo", version="1.0.0")


@app.get("/")
def health_check():
    return {"status": "healthy", "version": "1.1.0"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: str = None):
    return {"item_id": item_id, "query": q}


@app.get("/info")
def app_info():
    return {
        "app_name": "FastAPI CI/CD Demo",
        "description": "A FastAPI app deployed via GitHub Actions to AWS Elastic Beanstalk",
        "endpoints": ["/", "/items/{item_id}", "/info"],
    }

And here is the complete tests/test_application.py file:

from fastapi.testclient import TestClient

from application import app

client = TestClient(app)


def test_health_check():
    response = client.get("/")
    assert response.status_code == 200
    data = response.json()
    assert data["status"] == "healthy"
    assert data["version"] == "1.1.0"


def test_read_item():
    response = client.get("/items/42?q=test")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 42
    assert data["query"] == "test"


def test_read_item_no_query():
    response = client.get("/items/1")
    assert response.status_code == 200
    data = response.json()
    assert data["item_id"] == 1
    assert data["query"] is None


def test_app_info():
    response = client.get("/info")
    assert response.status_code == 200
    data = response.json()
    assert data["app_name"] == "FastAPI CI/CD Demo"
    assert "/" in data["endpoints"]
Watch the automated redeployment

Your push has already kicked off the workflow. Now you can watch both jobs execute in real time from the GitHub Actions dashboard.

  • Go to your GitHub repository in the browser.
  • Click the Actions tab.
  • Click the latest workflow run (it should be titled "Bump version to 1.1.0" or similar).
  • Watch the test job complete first. It runs your updated pytest suite with the new "1.1.0" assertion.
  • Once the test job passes, the deploy job starts automatically and updates your Elastic Beanstalk environment.
  • Wait for the deploy job to show a green checkmark. This takes 2-3 minutes as AWS updates the running instance.
  • Once the workflow completes, open your application URL in the browser (format: your-env-name.region.elasticbeanstalk.com).
  • Confirm the health check now returns {"status": "healthy", "version": "1.1.0"}.

Why does this matter?

You just changed two lines of code, pushed them, and your live API updated itself. No SSH, no manual file transfers, no deployment scripts to remember. This is the core value of CI/CD: code changes flow from your editor to production automatically and safely.

Still seeing version 1.0.0?

Make sure the deploy job finished with a green checkmark in the Actions tab. If it is still running, wait for it to complete.

Try a hard refresh in your browser (Cmd+Shift+R on macOS or Ctrl+Shift+R on Windows) to bypass any cached response.

help me debug why my Elastic Beanstalk deployment is not showing the updated version

Verify test failures block deployment

A pipeline that deploys everything is dangerous. The real value of your needs: test dependency is that broken code never reaches production. You will prove this by deliberately breaking a test on a pull request branch.

  • Create a new branch by running this command:
git checkout -b test-failure-demo
  • Open tests/test_application.py.
  • Find this line inside test_health_check:
    assert data["status"] == "healthy"
  • Replace "healthy" with "broken", so the line now looks like this:
    assert data["status"] == "broken"
  • Save the file.
  • Commit and push this branch by running these commands:
git add .
git commit -m "Introduce deliberate test failure"
git push origin test-failure-demo
  • Go to your GitHub repository in the browser.
  • You should see a banner offering to create a pull request for your new branch. Click Compare & pull request.
  • Click Create pull request to open the PR.
  • Watch the Actions tab (or the checks section at the bottom of the PR). The test job runs and fails with a red X.
  • Notice that the deploy job never appears. Because the workflow uses needs: test and the if condition only matches pushes to main, broken code on a PR branch can never reach production.

What just happened?

You proved two safety mechanisms at once. First, the needs: test dependency means a failing test job stops the entire pipeline. Second, the if: github.event_name == 'push' && github.ref == 'refs/heads/main' condition means deployments only happen on pushes to main, never on pull requests.

Together, these two rules form a safety net that protects your live application from broken code.

Test job passed when it should have failed?

Make sure you saved tests/test_application.py before committing. Check the commit on GitHub to verify the changed assertion is present in the pushed file.

help me figure out why my deliberately broken test passed in GitHub Actions

Secret mission

Add a Staging Environment with Manual Approval

In production teams, code never ships straight to production. Can you add a staging environment that deploys automatically, with production requiring your manual approval before going live?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The EC2 instances behind your Elastic Beanstalk environments run 24/7 and count against your AWS Free Tier hours.

Cost warning

Each Elastic Beanstalk environment runs a t2.micro EC2 instance continuously. AWS Free Tier gives you 750 hours/month of t2.micro usage for the first 12 months. A single environment fits within this limit, but two environments (production + staging from the Secret Mission) use approximately 1,460 hours/month and will exceed the free tier.

After your free tier expires, each running environment costs approximately $8.50/month. If you are not actively using this project, terminate your environments to stop all charges.

Resources you used:

  • AWS Elastic Beanstalk application fastapi-cicd-app with environments fastapi-cicd-env and fastapi-cicd-staging (each running a t2.micro EC2 instance).
  • S3 bucket created automatically by Elastic Beanstalk for deployment artifacts.
  • IAM user github-actions-deploy with access keys.
  • GitHub repository secrets (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY).
  • Local project files on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or want to keep the project as a live portfolio piece.

  • Your environments will continue running and serving your API.
  • A single environment stays within AWS Free Tier for the first 12 months (750 hours/month of t2.micro).
  • If you completed the Secret Mission and have two environments running, you will exceed the free tier limit and start incurring charges. Consider terminating the staging environment if you only want to keep production live.

Pause - I'll come back to this later

Elastic Beanstalk does not support pausing environments. To stop charges, you must terminate your environments entirely. Follow the Delete tab instructions to remove your environments, then recreate them later using your GitHub Actions workflow.

  • Your code remains safely in your GitHub repository.
  • When you are ready to resume, push a commit to main and the pipeline will recreate your Elastic Beanstalk environment automatically (the deploy action creates environments that do not exist).
  • Keep your GitHub secrets and IAM user in place so the pipeline can redeploy without reconfiguration.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to ensure nothing is left running.

Terminate your Elastic Beanstalk environments
  • Open the Elastic Beanstalk console and select us-east-1 from the Regions list.
  • In the navigation pane, choose Environments, then choose fastapi-cicd-env from the list.
  • Choose Actions, then choose Terminate environment.
  • Confirm termination in the dialog box by typing the environment name.
  • Repeat for fastapi-cicd-staging if you completed the Secret Mission.
Delete the Elastic Beanstalk application
  • In the Elastic Beanstalk console navigation pane, choose Applications, then choose fastapi-cicd-app.
  • Choose Actions, then choose Delete application.
  • Confirm by typing the application name, then choose Delete.
Delete the S3 deployment bucket
  • Open the S3 console.
  • Find the bucket created by Elastic Beanstalk (its name starts with elasticbeanstalk-us-east-1- followed by your account ID).
  • Select the bucket, choose Empty, confirm, then choose Delete and confirm again.
Delete the IAM user
  • Open the IAM console.
  • In the navigation pane, choose Users.
  • Select the checkbox next to github-actions-deploy.
  • Choose Delete at the top of the page.
  • Enter the username github-actions-deploy to confirm, then choose Delete.
Remove GitHub secrets
  • Go to your GitHub repository.
  • Navigate to Settings, then Secrets and variables, then Actions.
  • Delete AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
Delete local project files
  • Remove your local project folder by running this command:

macOS

cd ~/Desktop
rm -rf fastapi-cicd-app

Windows

cd ~\Desktop
rmdir /s /q fastapi-cicd-app

That's a Wrap!

That's a Wrap!

Nice work! You just built a fully automated CI/CD pipeline that tests and deploys a FastAPI application to AWS every time you push code.

You've learned how to:

  • Built a GitHub Actions workflow that runs automated tests with pytest and deploys to AWS Elastic Beanstalk on every push to main.
  • Configured IAM credentials and GitHub secrets to securely connect your pipeline to AWS without exposing sensitive keys.
  • Verified the full automated cycle end-to-end, proving that failing tests block deployment and protect production from bugs.
  • Secret Mission: Added a staging environment with manual approval gates so code is validated in staging before reaching production.

Ready to quiz yourself?