Manual CI/CD: FastAPI + PostgreSQL con GitHub Actions

Guía completa para implementar un pipeline de CI/CD que construye y despliega automáticamente una aplicación FastAPI con PostgreSQL usando GitHub Actions, Docker y un VPS.

Introducción

Este manual te guiará a través del proceso de creación de un proyecto FastAPI con PostgreSQL, configuración de GitHub Actions para construir imágenes Docker y almacenarlas en GitHub Packages, y finalmente desplegar la aplicación en un VPS usando Docker Compose con actualizaciones automáticas mediante CI/CD.

Nota: Este tutorial asume que tienes conocimientos básicos de Python, Docker, GitHub y administración de servidores Linux.

Estructura del Proyecto

Primero, crearemos la estructura básica de nuestro proyecto FastAPI con PostgreSQL.

project/ ├── app/ │ ├── __init__.py │ ├── main.py │ ├── database.py │ ├── models.py │ └── schemas.py ├── tests/ │ └── test_main.py ├── requirements.txt ├── Dockerfile ├── docker-compose.yml ├── .env.example └── .github/ └── workflows/ ├── ci.yml └── cd.yml

1. Configuración del Proyecto FastAPI

1.1 Crear entorno virtual e instalar dependencias

python -m venv venv
source venv/bin/activate # Linux/Mac venv\Scripts\activate # Windows
pip install fastapi uvicorn sqlalchemy psycopg2-binary python-dotenv

Crea un archivo requirements.txt con las dependencias:

fastapi==0.95.2
uvicorn==0.22.0
sqlalchemy==2.0.15
psycopg2-binary==2.9.6
python-dotenv==1.0.0

1.2 Configuración de la aplicación FastAPI

Crea el archivo principal de la aplicación en app/main.py:

from fastapi import FastAPI, HTTPException
from sqlalchemy import create_engine, Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from pydantic import BaseModel
import os
from dotenv import load_dotenv

load_dotenv()

# Configuración de la base de datos
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@db:5432/fastapi_db")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

Base = declarative_base()

# Modelo SQLAlchemy
class Item(Base):
    __tablename__ = "items"
    
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(String, index=True)

# Esquema Pydantic
class ItemCreate(BaseModel):
    name: str
    description: str

class ItemResponse(ItemCreate):
    id: int
    
    class Config:
        orm_mode = True

# Crear tablas
Base.metadata.create_all(bind=engine)

app = FastAPI()

@app.post("/items/", response_model=ItemResponse)
def create_item(item: ItemCreate):
    db = SessionLocal()
    db_item = Item(**item.dict())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    db.close()
    return db_item

@app.get("/items/{item_id}", response_model=ItemResponse)
def read_item(item_id: int):
    db = SessionLocal()
    item = db.query(Item).filter(Item.id == item_id).first()
    db.close()
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

@app.get("/")
def read_root():
    return {"message": "Welcome to FastAPI with PostgreSQL!"}

1.3 Archivo de entorno

Crea un archivo .env.example que servirá como plantilla para las variables de entorno:

# Configuración de la base de datos
DATABASE_URL=postgresql://user:password@db:5432/fastapi_db

# Configuración de FastAPI
APP_HOST=0.0.0.0
APP_PORT=8000
Importante: No versiones el archivo .env real en Git. Asegúrate de agregarlo a tu .gitignore.

2. Configuración de Docker

2.1 Dockerfile

Crea un Dockerfile para construir la imagen de tu aplicación:

# Etapa de construcción
FROM python:3.9-slim as builder

WORKDIR /app

# Instalar dependencias del sistema
RUN apt-get update && apt-get install -y \
    gcc \
    python3-dev \
    && rm -rf /var/lib/apt/lists/*

# Copiar requirements e instalar dependencias
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# Etapa final
FROM python:3.9-slim

WORKDIR /app

# Copiar dependencias instaladas desde la etapa de construcción
COPY --from=builder /root/.local /root/.local
COPY . .

# Asegurarse de que los scripts en .local sean ejecutables
ENV PATH=/root/.local/bin:$PATH

# Variables de entorno por defecto
ENV APP_HOST=0.0.0.0
ENV APP_PORT=8000

# Exponer el puerto
EXPOSE ${APP_PORT}

# Comando para ejecutar la aplicación
CMD ["uvicorn", "app.main:app", "--host", "${APP_HOST}", "--port", "${APP_PORT}", "--reload"]

2.2 Docker Compose

Crea un archivo docker-compose.yml para orquestar los servicios:

version: '3.8'

services:
  app:
    build: .
    ports:
      - "${APP_PORT}:${APP_PORT}"
    environment:
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
      - APP_HOST=${APP_HOST}
      - APP_PORT=${APP_PORT}
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:13-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    restart: unless-stopped

volumes:
  postgres_data:
Nota: Las variables como ${DB_USER}, ${DB_PASSWORD}, etc., se pueden definir en un archivo .env o pasarse directamente al comando docker-compose.

3. Configuración de GitHub Actions para CI/CD

3.1 Crear workflows

Crea el directorio .github/workflows y dentro dos archivos:

ci.yml (Integración Continua)

name: CI Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:13-alpine
        env:
          POSTGRES_USER: test_user
          POSTGRES_PASSWORD: test_password
          POSTGRES_DB: test_db
        ports:
          - 5432:5432
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.9'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install pytest
        
    - name: Run tests
      env:
        DATABASE_URL: postgresql://test_user:test_password@localhost:5432/test_db
      run: |
        pytest -v
    
  build:
    needs: test
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Log in to GitHub Container Registry
      uses: docker/login-action@v2
      with:
        registry: ghcr.io
        username: ${{ github.repository_owner }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata (tags, labels) for Docker
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ghcr.io/${{ github.repository }}
        tags: |
          type=sha
          type=ref,event=branch
    
    - name: Build and push Docker image
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}

cd.yml (Despliegue Continuo)

name: CD Pipeline

on:
  workflow_run:
    workflows: ["CI Pipeline"]
    branches: [main]
    types:
      - completed

jobs:
  deploy:
    if: ${{ github.event.workflow_run.conclusion == 'success' }}
    runs-on: ubuntu-latest
    
    steps:
    - name: SSH to VPS and deploy
      uses: appleboy/ssh-action@v0.1.10
      with:
        host: ${{ secrets.VPS_HOST }}
        username: ${{ secrets.VPS_USER }}
        key: ${{ secrets.VPS_SSH_KEY }}
        script: |
          cd /opt/fastapi-app
          docker-compose down
          docker image prune -af
          docker-compose pull
          docker-compose up -d

3.2 Configurar secrets en GitHub

Para que el workflow de despliegue funcione, necesitas configurar los siguientes secrets en tu repositorio GitHub:

  1. VPS_HOST - Dirección IP o dominio de tu VPS
  2. VPS_USER - Usuario SSH del VPS (normalmente 'root' o un usuario con privilegios)
  3. VPS_SSH_KEY - Clave privada SSH para autenticación en el VPS
Advertencia de seguridad: Nunca incluyas credenciales directamente en tus archivos de workflow. Siempre usa GitHub Secrets para información sensible.

4. Configuración del VPS

4.1 Preparar el servidor

Conéctate a tu VPS via SSH y ejecuta los siguientes comandos:

sudo apt update && sudo apt upgrade -y
sudo apt install -y docker.io docker-compose
sudo systemctl enable docker
sudo systemctl start docker

4.2 Configurar el directorio del proyecto

mkdir -p /opt/fastapi-app
cd /opt/fastapi-app

Crea un archivo docker-compose.yml similar al de tu proyecto, pero con las configuraciones de producción:

version: '3.8'

services:
  app:
    image: ghcr.io/tu-usuario/tu-repositorio:main
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@db:5432/${DB_NAME}
      - APP_HOST=0.0.0.0
      - APP_PORT=8000
    depends_on:
      - db
    restart: unless-stopped

  db:
    image: postgres:13-alpine
    environment:
      - POSTGRES_USER=${DB_USER}
      - POSTGRES_PASSWORD=${DB_PASSWORD}
      - POSTGRES_DB=${DB_NAME}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

volumes:
  postgres_data:

Crea un archivo .env en el VPS con tus configuraciones de producción:

# Configuración de la base de datos
DB_USER=prod_user
DB_PASSWORD=una_contraseña_segura_y_compleja
DB_NAME=fastapi_prod

# Configuración de FastAPI
APP_HOST=0.0.0.0
APP_PORT=8000
Seguridad: Asegúrate de que el archivo .env tenga permisos restrictivos (chmod 600) y que no sea accesible por otros usuarios.

4.3 Configurar autenticación con GitHub Packages

Para que tu VPS pueda descargar imágenes de GitHub Packages:

echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin
Nota: Necesitarás un token de acceso personal de GitHub con permisos para leer packages. Este token también debe almacenarse de forma segura en tu VPS.

5. Flujo de Trabajo Completo

5.1 Desarrollo local

  1. Clona el repositorio
  2. Crea una nueva rama para tus cambios
  3. Realiza tus modificaciones
  4. Ejecuta pruebas localmente
  5. Confirma y sube tus cambios
  6. Crea un Pull Request a la rama main

5.2 Pipeline de CI

  1. GitHub Actions ejecuta los tests cuando se crea un PR o se hace push a main
  2. Si los tests pasan, se construye la imagen Docker
  3. La imagen se sube a GitHub Packages con etiquetas adecuadas

5.3 Pipeline de CD

  1. Cuando el workflow de CI se completa exitosamente en la rama main
  2. El workflow de CD se activa y se conecta al VPS via SSH
  3. Detiene los contenedores actuales
  4. Limpia imágenes no utilizadas
  5. Descarga la nueva versión de la imagen
  6. Reinicia los contenedores con la nueva versión

6. Pruebas y Verificación

6.1 Verificar el despliegue

Después del despliegue, puedes verificar que todo funciona correctamente:

curl http://tu-vps-ip:8000

Deberías recibir una respuesta como:

{"message":"Welcome to FastAPI with PostgreSQL!"}
curl -X POST -H "Content-Type: application/json" -d '{"name":"test","description":"test item"}' http://tu-vps-ip:8000/items/

Deberías recibir una respuesta como:

{"name":"test","description":"test item","id":1}

7. Solución de Problemas

7.1 Problemas comunes

Los tests fallan en GitHub Actions

El despliegue falla

La aplicación no se conecta a PostgreSQL

8. Mejoras y Consideraciones Adicionales

8.1 Seguridad

8.2 Escalabilidad

8.3 Monitoreo

Conclusión

Has configurado exitosamente un pipeline completo de CI/CD para una aplicación FastAPI con PostgreSQL. Cada vez que hagas cambios y los subas a la rama main, GitHub Actions se encargará de:

  1. Ejecutar los tests
  2. Construir una nueva imagen Docker
  3. Subirla a GitHub Packages
  4. Desplegar automáticamente los cambios en tu VPS
¡Felicidades! Ahora tienes un flujo de trabajo profesional que automatiza el proceso de despliegue, asegurando consistencia y reduciendo errores humanos.