Construir API Serverless con DuckDB y FastAPI

Construir API Serverless con DuckDB y FastAPI

En el actual ecosistema de datos, la velocidad y la eficiencia de costes son fundamentales. Los directores de tecnología y líderes de ingeniería buscan constantemente eliminar la carga administrativa sin sacrificar el rendimiento. Este artículo describe un patrón de alto rendimiento y bajo coste para servir datos analíticos, combinando la velocidad "in-process" de DuckDB con el poder asíncrono de FastAPI, desplegados en un entorno completamente sin servidor.

Nos moveremos más allá de ejemplos triviales en memoria para construir una API analítica de nivel de producción, de solo lectura. El desafío arquitectónico central que resolvemos es una base de datos (DuckDB) en memoria, en un entorno de cálculo sin estado y temporal (sin servidor).

La solución implica tratar el archivo de la base de datos DuckDB como un artefacto inmutable y de solo lectura, creado por un proceso anterior y consumido a gran escala por nuestra API.

Equipo de Ingeniería de Software Compartido bajo Demanda, mediante Suscripción.

Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gerente de proyecto gratuito le ayudan a crear MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.

Try 4Geeks Teams

Situación en un mundo sin estado

Una función sin servidor (como AWS Lambda o Google Cloud Run) es temporal. Su sistema de archivos local es temporal y no se comparte entre instancias concurrentes.

  • Utilizando :memory:: duckdb.connect(':memory:') crea una base de datos en memoria. Esto es inútil para una API sin servidor, ya que la base de datos está vacía y se destruirá en el momento en que finalice la función.
  • Utilizando local.db: duckdb.connect('local.db') escribe en el disco ephemérico de la función. Esto también es inútil, ya que los datos no son persistentes y, crucialmente, no se comparten. Cada ejecución concurrente de tu función tendría una base de datos diferente y vacía.
  • La falacia de la "Escritura Concurrente": Se podría intentar montar un sistema de archivos compartido (como EFS) y que todas las funciones se conecten a shared/data.duckdb. Esto fallará. DuckDB es una base de datos en el proceso; utiliza un registro de escritura anticipada (WAL) y bloqueos a nivel de archivo para gestionar las escrituras. Múltiples procesos que escriben en el mismo archivo simultáneamente conducirán a la corrupción de la base de datos.

El patrón correcto para servidores sin servidor, por lo tanto, esdesacoplar el proceso de escritura de datos de la API de lectura de datos.

  1. Preparación de datos (La "ruta de escritura"): Un proceso separado y programado (por ejemplo, una ejecución de dbt, una acción de GitHub o una canalización de CI/CD) es responsable de la ingestión de datos. Ejecuta, recopila datos de las fuentes y crea un nuevo archivo de base de datos DuckDB (analytics.db.
  2. Almacenamiento de artefactos: Este archivo se sube luego al almacenamiento de objetos en la nube (por ejemplo, AWS S3 o Google Cloud Storage).
  3. API sin servidor (La "ruta de lectura"): Nuestra aplicación FastAPI, que se ejecuta en Lambda o Cloud Run, está configurada para conectarse a este archivo de S3/GCS en modo de solo lectura.

Esta arquitectura permite una alta concurrencia de lectura. Un millón de usuarios concurrentes son gestionados por un millón de instancias separadas sin servidor, cada una con su propio de solo lecturaacceso

Paso 1: El código de la aplicación FastAPI

Nuestra aplicación FastAPI será sencilla. La clave está en dónde y cómo se conecta a DuckDB. Utilizaremos la extensión httpfs de DuckDB, que permite leer directamente desde S3 y GCS.

Primero, definamos la estructura del proyecto:

/fastapi-duckdb
|-- app/
|   |-- main.py
|-- Dockerfile
|-- requirements.txt

requirements.txt

Necesitamos FastAPI, un servidor ASGI (Uvicorn), y DuckDB. Para AWS Lambda, también necesitamos <s1>mangummangum para adaptar la aplicación ASGI.

fastapi
uvicorn
duckdb
mangum  # Required only for AWS Lambda

app/main.py

Este código inicializa la conexión de DuckDB al iniciar la aplicación. El evento "inicio" es clave, ya que permite establecer la conexión una vez que la instancia sin servidor se inicia (un "inicio frío") y reutilizarla para todas las solicitudes posteriores "calientes".startupEl evento es crucial, ya que permite establecer la conexión una sola vez cuando se inicia la instancia sin servidor (un "inicio en frío") y reutilizarla para todas las solicitudes posteriores "calientes".

import os
import duckdb
from fastapi import FastAPI, Depends
from contextlib import asynccontextmanager

# Retrieve the database path from environment variables
# Example: 's3://my-data-bucket/analytics.db'
# Example: 'gs://my-data-bucket/analytics.db'
DB_PATH = os.environ.get("DUCKDB_PATH")
READ_ONLY_MODE = True

class DuckDBConnection:
    """
    A class to manage the DuckDB connection lifecycle.
    
    It initializes the connection on startup and closes it on shutdown.
    It installs and loads necessary extensions (httpfs) and sets
    credentials for S3/GCS if provided via environment variables.
    """
    def __init__(self):
        self.connection = None

    def connect(self):
        print(f"Connecting to DuckDB at: {DB_PATH}")
        self.connection = duckdb.connect(database=DB_PATH, read_only=READ_ONLY_MODE)
        
        # Install and load the httpfs extension to read from S3/GCS
        self.connection.install_extension("httpfs")
        self.connection.load_extension("httpfs")
        
        # Configure credentials for GCS
        if DB_PATH.startswith("gs://"):
            gcs_key_id = os.environ.get("GCS_KEY_ID")
            gcs_secret = os.environ.get("GCS_SECRET")
            if gcs_key_id and gcs_secret:
                print("Setting GCS credentials...")
                self.connection.execute("CREATE SECRET gcs_secret (TYPE GCS, KEY_ID ?, SECRET ?)", [gcs_key_id, gcs_secret])
                self.connection.execute("SET s3_region='auto'") # Use 'auto' for GCS
                self.connection.execute("SET s3_url_style='path'")
            
        # Configure credentials for S3
        elif DB_PATH.startswith("s3://"):
            aws_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
            aws_secret = os.environ.get("AWS_SECRET_ACCESS_KEY")
            aws_region = os.environ.get("AWS_REGION", "us-east-1")
            
            if aws_key_id and aws_secret:
                print(f"Setting S3 credentials for region: {aws_region}")
                self.connection.execute(f"SET s3_region='{aws_region}'")
                self.connection.execute(f"SET s3_access_key_id='{aws_key_id}'")
                self.connection.execute(f"SET s3_secret_access_key='{aws_secret}'")
            else:
                print("Using IAM role for S3 access.")

        print("DuckDB connection established.")

    def close(self):
        if self.connection:
            self.connection.close()
            print("DuckDB connection closed.")

    def get_connection(self):
        return self.connection

# Initialize the connection manager
db_manager = DuckDBConnection()

@asynccontextmanager
async def lifespan(app: FastAPI):
    # On startup
    db_manager.connect()
    yield
    # On shutdown
    db_manager.close()

app = FastAPI(lifespan=lifespan)

# Dependency to get the DB connection
def get_db():
    return db_manager.get_connection()


@app.get("/")
async def root():
    return {"message": "Serverless DuckDB API is running"}

@app.get("/query")
async def query_db(sql: str, db=Depends(get_db)):
    """
    Execute a SQL query.
    WARNING: In production, NEVER pass raw SQL from a user.
    This is for demonstration. Build specific endpoints.
    """
    try:
        # Execute the query and fetch results as a dictionary
        result = db.sql(sql).to_df().to_dict('records')
        return {"data": result}
    except Exception as e:
        return {"error": str(e)}

@app.get("/tables")
async def show_tables(db=Depends(get_db)):
    """A safe endpoint to list all tables."""
    try:
        tables = db.sql("SHOW TABLES").to_df().to_dict('records')
        return {"tables": tables}
    except Exception as e:
        return {"error": str(e)}

# --- AWS Lambda Specific ---
# If deploying to Lambda, wrap the app with Mangum
# This part is ignored by Uvicorn/Cloud Run
try:
    from mangum import Mangum
    handler = Mangum(app)
except ImportError:
    pass

Paso 2: Despliegue en Google Cloud Run

Cloud Run es el entorno más sencillo para esta pila. Crea un contenedor y lo ejecuta, gestionando la escalabilidad desde cero hasta N.

Dockerfile

# Use an official Python runtime as a parent image
FROM python:3.11-slim

# Set the working directory in the container
WORKDIR /code

# Copy the requirements file and install dependencies
COPY ./requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# Copy the application code
COPY ./app /code/app

# Command to run the application using uvicorn
# Uvicorn will look for the 'app' variable in /code/app/main.py
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

Pasos de implementación:

  1. Conceder Permisos: Asegúrese de que la cuenta de servicio de su instancia de Cloud Run tenga los permisos "Visualizador de Objetos de Almacenamiento" en el bucket de GCS que contiene su archivo DuckDB.
  2. Establecer Variables de Entorno: Al implementar la instancia de Cloud Run, establezca lo siguiente:
    • DUCKDB_PATH: gs://your-bucket-name/analytics.db
    • (Opcional) Si no está utilizando los permisos predeterminados de la cuenta de servicio, establezca GCS_KEY_ID y GCS_SECRET.

Equipo de Ingeniería de Software Compartido bajo Demanda, mediante Suscripción.

Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda, a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gestor de proyectos gratuito, le ayudan a construir MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.

Try 4Geeks Teams

Construir y desplegar:

gcloud run deploy fastapi-duckdb-api \
  --source . \
  --platform managed \
  --region us-central1 \
  --allow-unauthenticated \
  --set-env-vars="DUCKDB_PATH=gs://your-bucket-name/analytics.db"

Cloud Run construirá automáticamente el contenedor, lo subirá a Artifact Registry y lo desplegará.

Paso 3: Despliegue en AWS Lambda

Implementar en Lambda es más complejo debido a su entorno de ejecución. DuckDB es un binario compilado, y el que instalas con pip install en un Mac (ARM64) no funcionará en Lambda (x86_64 o ARM64 Linux).

Usted tiene dos soluciones principales:

  1. Implementación de Imagen de Contenedor (Recomendado): Este es el método moderno y preferido. Utiliza el mismo Dockerfile que el ejemplo de Cloud Run.Implementación ZIP con Capas de Lambda (Tradicional):
  2. Si no puedes utilizar contenedores, debes utilizar una capa de Lambda de DuckDB precompilada.Si no puede utilizar contenedores, deberá utilizar una capa Lambda de DuckDB precompilada.

Solución A: Implementación de Lambda en un contenedor

  1. Crear función Lambda:
    • En la consola de AWS, cree una nueva función Lambda.
    • Seleccione "Imagen de contenedor" como la fuente.
    • Navegue por ECR y seleccione su fastapi-duckdb-api:latest imagen.
    • Permisos: Adjunte un rol de IAM a la función que tenga s3:GetObject permisos en su analytics.db archivo.
    • Variables de entorno: Establezca DUCKDB_PATHas3://your-bucket-name/analytics.db.
  2. Crear API Gateway:
    • Cree una nueva API HTTP en API Gateway.
    • Cree una ruta (por ejemplo, GET / y una ruta de proxy {proxy+}).
    • Configure una "Integración" para que apunte a su nueva función Lambda.

Construir y desplegar en ECR:

# Authenticate Docker with ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com

# Create the ECR repository
aws ecr create-repository --repository-name fastapi-duckdb-api

# Build, tag, and push
docker build -t fastapi-duckdb-api .
docker tag fastapi-duckdb-api:latest <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-duckdb-api:latest
docker push <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/fastapi-duckdb-api:latest

Modifique Dockerfile: Agregue el handler "mangum".

# Use an AWS-provided base image for Python Lambda
FROM public.ecr.aws/lambda/python:3.11

# Set the working directory
WORKDIR ${LAMBDA_TASK_ROOT}

# Copy requirements and install
COPY ./requirements.txt ./requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# Copy the application code
COPY ./app ./app

# Set the command to run the Mangum handler
# This tells Lambda to execute 'handler' in 'app.main'
CMD ["app.main.handler"]

Solución B: Implementación de Lambda ZIP (con Capas)

Si debe utilizar un método de despliegue ZIP, no puede simplementepip install duckdb. Debe utilizar una capa precompilada.

  1. Encuentra una Capa: Busca una capa pública de DuckDB Lambda compatible con tu entorno de ejecución de Python (por ejemplo, awslambda-layer-duckdb).
  2. Añade ARN de la Capa: En la configuración de tu función Lambda, añade el ARN de la Capa.
  3. Empaqueta el Código: Comprime tu app/ directorio sin duckdb en requirements.txt (como lo proporciona la capa).
  4. Sube: Sube el archivo ZIP como la fuente de código de tu Lambda.
  5. Configura: Establece el manejador a app.main.handler y configura API Gateway y variables de entorno como en la solución de contenedor.

Equipo de ingeniería de software compartido bajo demanda, mediante suscripción.

Acceda a un equipo flexible y compartido de ingeniería de software bajo demanda a través de una suscripción mensual predecible. Desarrolladores, diseñadores, ingenieros de control de calidad y un gerente de proyecto gratuito le ayudan a crear MVPs, escalar productos e innovar con tecnologías modernas como React, Node.js y más.

Try 4Geeks Teams

Implicaciones de Rendimiento y Costo

Esta arquitectura presenta ventajas y desventajas claras a nivel ejecutivo:

  • Costo: Extremadamente bajo. Solo paga por el almacenamiento de objetos (cientos de centavos por GB) y por el tiempo de ejecución del servidor sin servidor (milisegundos). No hay costo por un servidor de base de datos inactivo.
  • Escalabilidad: Prácticamente ilimitada. Su capacidad de lectura escala con su límite de concurrencia del servidor sin servidor, que suele ser de miles de peticiones concurrentes.
  • Rendimiento:
    • Arranque en frío: La primera solicitud a una nueva instancia tendrá latencia. La función debe inicializarse y DuckDB debe establecer su conexión remota a S3/GCS. Esto puede llevar de 1 a 3 segundos.
    • Lecturas en caliente: Una vez que está "caliente", la API es excepcionalmente rápida. Las consultas se ejecutan con el motor DuckDB integrado, a menudo en milisegundos.
  • Frescura de los datos: Este es el principal compromiso. Los datos son de solo lectura y solo tan frescos como su última construcción de preparación de datos. Este patrón no es adecuado para datos transaccionales en tiempo real, pero es perfecto para paneles de control, inteligencia empresarial (BI) y puntos finales analíticos donde la frescura de los datos de unos pocos minutos o horas es aceptable.

Un Nuevo Estándar para APIs Analíticas

Al combinar FastAPI, DuckDB y la computación sin servidor, hemos creado una solución altamente escalable, rentable y de fácil gestión para el servicio de datos analíticos. La clave es adoptar el patrón de "artefacto inmutable y de solo lectura". Esta arquitectura desacopla la ingestión de datos del servicio de datos, permitiendo que cada uno se escale de forma independiente y eficiente, eliminando la necesidad de un costoso servidor de base de datos analítica siempre activo para muchos casos de uso comunes.elemento inmutable, de solo lectura

Preguntas frecuentes

¿Cómo facilita la combinación de DuckDB y FastAPI una arquitectura sin servidor?

Al combinar DuckDB con FastAPI, los desarrolladores pueden crear una API analítica de alta concurrencia sin necesidad de administrar un servidor de base de datos dedicado. DuckDB funciona como un motor SQL en proceso que permite a la aplicación FastAPI leer datos directamente desde el almacenamiento de objetos en la nube (como AWS S3 o Google Cloud Storage) utilizando la extensión httpfs. Esta configuración trata el archivo de la base de datos como un artefacto de solo lectura, lo que permite que la API se escale infinitamente en instancias sin servidor (como AWS Lambda o Cloud Run) manteniendo bajos costos y un alto rendimiento de lectura.

¿Cuál es la solución para gestionar datos con estado en un entorno de servidor sin estado?

El principal desafío en los entornos sin servidor es que las instancias de cómputo son temporales y no comparten almacenamiento local. Para abordar esto, se utiliza el patrón del "artefacto de solo lectura". En lugar de escribir en una base de datos local o en memoria, el archivo de la base de datos DuckDB se genera por un proceso upstream (p. ej., una tubería CI/CD) y se sube a un almacenamiento de objetos inmutable. La API sin servidor luego se conecta a este archivo remoto en modo de solo lectura, desacoplando el almacenamiento de datos del nivel de cómputo y asegurando la consistencia en las ejecuciones concurrentes.

¿Cuáles son las ventajas y desventajas en términos de rendimiento y costo de utilizar este patrón de API de datos sin servidor?

Esta arquitectura ofrece importantes ahorros de costos, ya que solo se paga por el almacenamiento y el tiempo de ejecución real, eliminando los gastos de servidores de base de datos inactivos. Si bien las solicitudes "calientes" proporcionan respuestas excepcionalmente rápidas (a menudo en milisegundos), las "arrancadas frías" pueden introducir unos pocos segundos de latencia mientras se establece la conexión con el almacenamiento de objetos. Además, dado que la base de datos es de solo lectura y se actualiza mediante procesos por lotes, esta solución es ideal para paneles de análisis (OLAP) en lugar de sistemas transaccionales en tiempo real (OLTP) que requieren datos frescos instantáneamente.