DEV Community

Nonso Echendu
Nonso Echendu

Posted on

1

Building a FastAPI Book API with CI/CD Pipelines (Using Github Actions) and Docker Deployment

In this article, I’ll walk you through the development of a FastAPI-based book API, complete with a Continuous Integration (CI) and Continuous Deployment (CD) pipeline. This project was part of a Devops challenge with HNG, and is aimed to retrieve book details by ID, and deployed to an AWS ec2 instance using Docker.

Project Overview

The repository provided already included a predefined structure, and we had to ensure that all endpoints were correctly implemented and accessible via Nginx.

These were the goals to be achieved in this challenge:

  1. Add an endpoint to retrieve a book by its ID, so that it is accessible via this url path /api/v1/books/{book_id}.

  2. Set up a CI pipeline that automates tests whenever a pull request is made to the main branch.

  3. Set up a CD pipeline that automates the deployment process whenever changes are pushed to the main branch.

  4. Serve the application over NGINX

Project Structure

fastapi-book-project/
├── api/
│   ├── db/
│   │   ├── __init__.py
│   │   └── schemas.py      # Data models and in-memory database
│   ├── routes/
│   │   ├── __init__.py
│   │   └── books.py        # Book route handlers
│   └── router.py           # API router configuration
├── core/
│   ├── __init__.py
│   └── config.py           # Application settings
├── tests/
│   ├── __init__.py
│   └── test_books.py       # API endpoint tests
├── main.py                 # Application entry point
├── requirements.txt        # Project dependencies
└── README.md
Enter fullscreen mode Exit fullscreen mode

Prerequisites

Setting Up

The initial step was cloning the provided git repo, which contained the basic project structure. Here's a link to the project's git repo, https://github.com/NonsoEchendu/fastapi-book-project.

To clone:

git clone https://github.com/NonsoEchendu/fastapi-book-project.git
Enter fullscreen mode Exit fullscreen mode

Challenge Task #1

So starting with the first task in the challenge: Add an endpoint to retrieve a book by its ID.

To do this, I need to go to the books.py file found in the routes/ folder, and add the /api/v1/books/{book_id} endpoint.

I'm also going to add a get_book function to retrieve the book details by ID. And if the book is not found, the function throws a 404 Not Found error.

Endpoint definition:

@router.get(
    "/{book_id}", response_model=Book, status_code=status.HTTP_200_OK
)
async def get_book(book_id: int) -> Book:
    book = db.books.get(book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    return book
Enter fullscreen mode Exit fullscreen mode


Next, i tested if the endpoint I just added is working.

To test, i'll be starting the application using Uvicorn:

uvicorn app.main:app --reload
Enter fullscreen mode Exit fullscreen mode

Then, access the endpoint on my browser url at http://127.0.0.1:8000/api/v1/books/1.

Image description

It works!

Dockerizing the Application

Before moving to the next tasks for the challenge - creating CI/CD pipelines, i'll want to containerize the app using docker.

So first, i'll be creating a Dockerfile in the project root's directory. The Dockerfile will be structured to install dependencies, set up a non-root user, and start the app using Uvicorn.

Here's the Dockerfile:

FROM python:3.9-slim

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Now, we can build the application into a docker image and run it inside a container while maintaining a clean and isolated environment.

To build the image, while in the project's root directory, run:

docker build -t fastapi-book .
Enter fullscreen mode Exit fullscreen mode

To run the app in a container:

docker run -d -p 8000:8000 fastapi-book
Enter fullscreen mode Exit fullscreen mode

You should be able to also access it on your browser url at http://127.0.0.1:8000/api/v1/books/1.

Challenge Task #2

Moving to the next task: Setting up a CI (Continuous Integration) pipeline.

This CI pipeline will simply automate a pytest whenever a pull request is made to the main branch. These tests are important as they ensure that any changes to the codebase do not break existing functionality.

The CI pipeline:

name: CI Pipeline

on:
  pull_request:
    branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Set Up Python
        uses: actions/setup-python@v4
        with:
          python-version: "3.9"

      - name: Install Dependencies
        run: |
          python -m venv venv
          source venv/bin/activate
          pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run Tests
        run: |
          source venv/bin/activate
          pytest --maxfail=1 --disable-warnings
Enter fullscreen mode Exit fullscreen mode

Challenge Task #3

Setting up a CD (Continuous Deployment) pipeline

This CD pipeline will automate the deployment process whenever changes are pushed to the main branch.

The CD pipeline:

name: Deployment Pipeline

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
          SERVER_IP: ${{ secrets.SERVER_IP }}
          USER: ${{ secrets.SERVER_USER }}

    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Debug Environment Variables
        run: |
          echo "Server IP: $SERVER_IP"
          echo "User: $USER"

      - name: Set Up SSH
        run: |
          # Setup SSH
          mkdir -p ~/.ssh 
          echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H $SERVER_IP >> ~/.ssh/known_hosts

      - name: Prepare EC2 Instance & Pull Latest Code
        run: |
          ssh $USER@$SERVER_IP << 'EOF'
            set -e  # Stop script if any command fails

            # Ensure project directory exists
            if [ ! -d "/home/$USER/fastapi-book-project" ]; then
              git clone https://github.com/NonsoEchendu/fastapi-book-project.git /home/$USER/fastapi-book-project
            fi

            # Go to project folder
            cd /home/$USER/fastapi-book-project

            # Fetch latest changes
            git reset --hard  # Ensure a clean state
            git pull origin main
          EOF

      - name: Install Docker
        run: |
          ssh $USER@$SERVER_IP << 'EOF'
            sudo apt-get update
            sudo apt-get install -y docker.io 
            sudo usermod -aG docker $USER
          EOF

          ssh $USER@$SERVER_IP "docker ps"

      - name: Deploy With Docker 
        run: |
          ssh $USER@$SERVER_IP << 'EOF'
            cd /home/$USER/fastapi-book-project
            docker stop fastapi-app || true
            docker rm fastapi-app || true
            docker build -t fastapi-app .
            docker run -d --name fastapi-app -p 8000:8000 fastapi-app
          EOF
Enter fullscreen mode Exit fullscreen mode

This setup ensures that changes pushed to the main branch were automatically pulled into the server. It also builds the image and runs the application in a Docker container.

The use of secrets also ensures secure authentication without exposing sensitive credentials.

To setup the repository secrets in your Github Actions, go to the github repository settings. Then go to **Secrets and variables**, then click on **Actions**. Next, click on **New repository secret** and add the 3 repo secrets we used in the CD pipeline, namely, SERVER_IP, SERVER_USER, SSH_PRIVATE_KEY.

Before creating these secrets, please ensure to already create an AWS ec2 instance. The values assigned to these secrets will be from the running ec2 instance.

Challenge Task #4

And now the last task - Serving the application over NGINX.

I want to use NGINX as a reverse proxy to route traffic from my ec2 instance's public ip address to my FastAPI application running on port 8000.

Inside my ec2 instance ubuntu server, i'll first install and enable Nginx using:

sudo apt-get update

sudo apt-get nginx

sudo systemctl start enable nginx
Enter fullscreen mode Exit fullscreen mode

Then i add the Nginx configuration file:

sudo nano etc/nginx/sites-available/fastapi-app
Enter fullscreen mode Exit fullscreen mode

And add this:

server {
    listen 80;
    server_name yourdomain.com;  # Replace with your domain or server IP

    location / {
        proxy_pass http://127.0.0.1:8000/;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Enter fullscreen mode Exit fullscreen mode

Next, i enable the configuration by creating a symlink, and remove the default site:

sudo ln -s /etc/nginx/sites-available/fastapi /etc/nginx/sites-enabled/

sudo rm /etc/nginx/sites-enabled/default
Enter fullscreen mode Exit fullscreen mode

Then i can restart NGINX:

sudo systemctl restart nginx
Enter fullscreen mode Exit fullscreen mode

Now i can access the endpoint directly without adding 8000 to the url, like this: http://ec2-public-ip/api/v1/books/{book_id}.
{book_id} here being a positive integer.

Image description

Conclusion

In summary, what was I able to achieve in this project?

I built a FastAPI-based book API that retrieves book details by ID. I also made the application production-ready by containerizing it using Docker and used Nginx as a reverse proxy.

I also implemented CI/CD pipelines using GitHub Actions ensuring that the application is thoroughly tested and automatically deployed whenever changes are pushed to the main branch.

If you’re interested in exploring the code, check out the GitHub repository. Feel free to fork it, experiment, and adapt it to your needs!

Till my next project, happy building! ✌🏽

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more