Proxy de APIs escalable con sistema de rate limiting para MercadoLibre.
- API Proxy para MercadoLibre
- Tecnologías Usadas
- Limites técnicos:
- 🚀 Setup del proyecto
- Documentación de endpoints
- 📋 Archivo de configuración
- Upload de imagen a Dockerhub
- ☸️ Deploy a Kubernetes
- Explicaciones del desarrollo
- Integración con Prometheus
- Healtcheck
- Diagrama de clases
- Lifespan de la app
- Secuencia de cada request
- Secuencia de la carga de la configuración
- 📄 Licencia
- Solamente se puede cargar un archivo de configuración (
config.yaml) y este solamente puede tener el encoding UTF-8.
# 1. Clonar repositorio
git clone https://github.com/KrappRamiro/meli-proxy
cd api-proxy
# 2. Crear venv (Python 3.12)
python -m venv .venv
source .venv/bin/activate # Linux/Mac
.\.venv\Scripts\activate # Windows
# 3. Instalar dependencias
pip install -e .[dev,test]
# 4. Crear archivo .env
# ⚠️ ATENCION: Leer los comentarios del archivo para saber qué valores usar
cp .env.example .env# 1. Levantar redis de fondo
cd docker/
docker compose up redis -d
# 2. Ejecutar servidor local con autorecarga
cd ../
uvicorn src.api_proxy.main:app --reload --port 8081 --env-file .env --log-level debug# Formatear código
black .
# Correr linter y corregir errores automáticamente
ruff check --fix .
# Hacer checkeo de tipos estáticos
mypy src/# Ejecutar tests
coverage run -m pytest
# Console report
coverage report
# HTML report
coverage html
# XML report (Para CI/CD)
coverage xml# Crear archivo .env
# ⚠️ ATENCION: Leer los comentarios del archivo para saber qué valores usar
cp .env.example .env.docker
cd docker/
# Levantar proyecto
docker compose up --buildPara verlo, levantar la app y acceder al endpoint docs/
El archivo config/config.yaml define las reglas de rate limiting para el proxy.
Se recarga automáticamente cuando se modifican las reglas (sin necesidad de reiniciar la app).
rules:
- type: "<tipo_regla>"
# ... parámetros específicos de cada regla, ver sección de Tipos de Reglas Disponibles ...
limit: <cantidad>
window: <segundos>- type: "ip"
ip: "<dirección_ipv4>"
limit: <int> # Máximo de requests
window: <int> # Ventana de tiempo en segundosEjemplo:
- type: "ip"
ip: "127.0.0.1" # Rate limit para localhost
limit: 15 # Límite de 15 requests
window: 60 # Expire de 60 segundos- type: "path"
pattern: "<patron>" # Requerido (sintaxis de wildcard)
limit: <int>
window: <int>Ejemplos de patrones válidos:
user/*: Coincide con/user/pepey/user/123items/*: Coincide con/items/MLA123y/items/MLA456categories: Coincide exactamente con/categories
Para más información de qué patrones están permitidos, ver la función matches_pattern en src/api_proxy/utils.py
- type: "ip_path"
ip: "<dirección_ipv4>"
pattern: "<patron>"
limit: <int>
window: <int>Aplica solo cuando coinciden ambos criterios
Es una combinación de la regla por IP y la regla por Route
Para más información de qué patrones están permitidos, ver la función matches_pattern en src/api_proxy/utils.py
rules:
# IP específica
- type: "ip"
ip: "100.100.100.100"
limit: 1000 # 1000 reqs
window: 60 # por minuto
# Ruta general
- type: "path"
pattern: "items/*"
limit: 100 # 100 reqs
window: 10 # cada 10 segundos
# Combinación IP + Ruta
- type: "ip_path"
ip: "192.168.1.5"
pattern: "user/profile"
limit: 30 # 30 reqs
window: 3600 # por horadocker build --file docker/Dockerfile --tag krappramiro/meli-proxy:latest .
docker login
docker push krappramiro/meli-proxy:latestPara deployear nuestra app usamos 📦 Helm, el gestor de paquetes para K8s: Lo usamos porque simplifica la instalación y configuración de aplicaciones mediante "charts".
- 🧩 Define toda la infraestructura de la app (Deployments, Services, etc.) en un solo chart.
- ⚙️ Permite personalizar configuraciones usando un archivo
values.yaml - 🔄 Facilita parametrizar nuestros deployments
Es un archivo de configuración que personaliza cómo se despliega el chart. Ejemplo:
replicaCount: 3 # Número de "copias" del contenedor para alta disponibilidad
image:
repository: nginx # Nombre de la imagen Docker 🐳
tag: latest # Versión de la imagen 🏷️
resources:
requests: # Recursos mínimos que Kubernetes garantiza ⚡
memory: "128Mi"
cpu: "50m"
limits: # Límite máximo de recursos que el contenedor puede usar 🚧
memory: "256Mi"
cpu: "200m"- 🏳️ Valores por defecto: Definidos en
helm/chart/values.yaml. - 🎨 Personalización: Los archivos en
helm/values/sobrescriben valores según el ambiente (ej: testing, producción).
helm/
└── values/
└── prod.yaml # Config para prodCada archivo AMBIENTE.yaml está relacionado a cada ambiente.
helm upgrade meli-proxy helm/chart/ --namespace meli-proxy --create-namespace --install --values helm/values/prod.yamlEsta estructura es para seguir el layout recomendado por https://packaging.python.org/en/latest/tutorials/packaging-projects/
Hacer esto evita problemas de importación y es requerido por setuptools
Ver para más información https://www.pyopensci.org/python-package-guide/package-structure-code/python-package-structure.html
Para hacer que esta carpeta sea un package.
Esto permite dos cosas: la primera es tener namespaces organizados, y la segunda es poder ejecutar código de init al importar el paquete (para hacer cosas como por ejemplo, exponer la instancia de FastAPI como parte del paquete).
Si algún día se quiere convertir el proyecto en una librería, ya está todo preparado.
Porque eso nos permite crear endpoints internos, como health/, docs/ y metrics/ sin que colisionen con la función de proxy
Es un JSON schema (ver https://json-schema.org/) con la estructura que config.yaml debe tener
Se expone en el endpoint metrics/
Ver https://github.com/trallnag/prometheus-fastapi-instrumentator
Bajo el endpoint health/ se expone un healthcheck que responde con un 200 OK si la app está funcionando
classDiagram
direction LR
class Rule {
<<Abstract>>
+int limit
+int window
+matches(ip: str, path: str) bool
+generate_key(ip: str, path: str) str
}
class IPRule {
+str ip
+matches(ip: str, path: str) bool
+generate_key(ip: str, path: str) str
}
class PathRule {
+str pattern
+matches(ip: str, path: str) bool
+generate_key(ip: str, path: str) str
}
class IPPathRule {
+str ip
+str pattern
+matches(ip: str, path: str) bool
+generate_key(ip: str, path: str) str
}
class ConfigLoader {
+str config_path
+list[Rule] rules
+reload(self) None
-_load_config() list[Rule]
}
class ConfigWatcher {
+Observer observer
+str config_path
+FileUpdateHandler handler
+start(self) None
+stop(self) None
}
class FileUpdateHandler {
+str target_path
+callable callback
+on_modified(self, event) None
}
class RateLimiter {
+redis.asyncio.Redis redis_client
+list[Rule] rules
+is_allowed(self, ip: str, path: str) bool
+load_rules(self, rules: list[Rule]) None
}
%% B --|> A means "B inherits from A"
IPRule --|> Rule: inherits
PathRule --|> Rule: inherits
IPPathRule --|> Rule: inherits
%% *-- means Composition
%% Composition implies that the parent (ConfigWatcher) owns the child (FileUpdateHandler),
%% and the child can't exist without the parent
ConfigWatcher *-- FileUpdateHandler: Es el handler que usa para llamar a la función de callback
%% === Associations ===
%% --> means Association
%% based on https://stackoverflow.com/a/1230901/15965186
%% An association almost always implies that one object has the other object as a field/property/attribute (terminology differs).
%% Association: A has-a C object (as a member variable)
RateLimiter --> Rule: Lo acepta como parametro
%% --------------------
%% === Dependencies ===
%% ..> means Dependency
%% based on https://stackoverflow.com/a/1230901/15965186
%% A dependency typically (but not always) implies that an object accepts another object as a method parameter, instantiates, or uses another object. A dependency is very much implied by an association.
%% Dependency: A references B (as a method parameter or return type)
RateLimiter ..> ConfigLoader : Lo usa para obtener las reglas
ConfigWatcher ..> ConfigLoader: Llama a reload() cuando hay un cambio en config.yaml
%% --------------------
note for ConfigWatcher "Implements Observer pattern for config file changes"
note for RateLimiter "Uses Redis INCR+EXPIRE"
note for FileUpdateHandler "Filters duplicate filesystem events"
sequenceDiagram
participant SistemaOperativo
participant App
box Componentes Internos
participant Redis
participant RateLimiter
participant ConfigLoader
participant ConfigWatcher
participant Prometheus Instrumentator
end
SistemaOperativo->>App: Inicia aplicación
activate App
App->>Redis: Se conecta a Redis
activate Redis
App->>ConfigLoader: Carga config.yaml
activate ConfigLoader
App->>RateLimiter: Inicializa con reglas de ConfigLoader
activate RateLimiter
App->>ConfigWatcher: Inicia monitoreo de cambios en config.yaml
activate ConfigWatcher
App->>Prometheus Instrumentator: Expone métricas
activate Prometheus Instrumentator
SistemaOperativo->>App: SIGTERM
App->>ConfigWatcher: Detiene el watch usando stop()
deactivate ConfigWatcher
App->>Redis: Cierra la conexión
deactivate Redis
par App to RateLimiter
App->>RateLimiter: Desinstanciado automáticamente
deactivate RateLimiter
and App to ConfigLoader
App->>ConfigLoader: Desinstanciado automáticamente
deactivate ConfigLoader
and App to Prometheus Instrumentator
App->>Prometheus Instrumentator: Desinstanciado automáticamente
deactivate Prometheus Instrumentator
end
deactivate App
SistemaOperativo-->>App: Proceso finalizado
sequenceDiagram
actor Cliente
participant App
participant RateLimiter
participant Redis
participant API_MeLi
Cliente->>+App: Request a /proxy/{path}
App->>+RateLimiter: Consulta si está permitido (IP + Path)
loop Para cada Regla
RateLimiter->>+Redis: Incrementa contador (INCR)
alt Primera request
Redis-->>RateLimiter: Valor 1
RateLimiter->>Redis: Establece TTL (EXPIRE)
else Requests subsiguientes
Redis-->>RateLimiter: Contador actual
end
alt Límite de requests excedido
RateLimiter-->>App: Denegar request <br/> devolviendo false en is_allowed
App-->>Cliente: Responder 429 Too Many Requests
end
end
RateLimiter-->>-App: Permitir request <br/> devolviendo true en is_allowed
App->>+API_MeLi: Proxy de la solicitud
API_MeLi-->>-App: Response de API
App-->>-Cliente: Retorna response original
sequenceDiagram
participant App
participant ConfigLoader
participant ConfigWatcher
participant FileSystem
participant RateLimiter
App->>ConfigLoader: Crea ConfigLoader con ruta config.yaml
activate ConfigLoader
ConfigLoader->>FileSystem: Lee archivo config.yaml
FileSystem-->>ConfigLoader: Devuelve contenido YAML
ConfigLoader->>ConfigLoader: Parsea reglas con parse_rules()
ConfigLoader->>RateLimiter: Actualiza reglas con load_rules()
ConfigLoader-->>App: Retorna instancia configurada
deactivate ConfigLoader
App->>ConfigWatcher: Instancia ConfigWatcher (config.yaml, callback=ConfigLoader.reload)
activate ConfigWatcher
ConfigWatcher->>FileSystem: Comienza monitoreo de cambios
ConfigWatcher-->>App: Watcher iniciado
deactivate ConfigWatcher
loop Monitoreo de filesystem
ConfigWatcher->>FileSystem: Observa cambios en config.yaml
alt En caso de archivo modificado
FileSystem->>ConfigWatcher: Notifica evento on_modified
activate ConfigWatcher
ConfigWatcher->>ConfigLoader: Ejecuta callback reload()
activate ConfigLoader
ConfigLoader->>FileSystem: Vuelve a leer config.yaml
FileSystem-->>ConfigLoader: Nuevo contenido del archivo
ConfigLoader->>ConfigLoader: Re-parsea reglas
ConfigLoader->>RateLimiter: Actualiza reglas con load_rules(new_rules)
ConfigLoader-->>ConfigWatcher: Recarga completada
deactivate ConfigLoader
ConfigWatcher-->>FileSystem: Continúa monitoreo del filesystem
deactivate ConfigWatcher
end
end
MIT License - Ver LICENSE para detalles.