API REST desarrollada con Node.js y Express, aplicando prácticas de DevOps: containerización, CI/CD, monitoreo y despliegue continuo.
| Tecnología | Versión | Uso | Documentación |
|---|---|---|---|
| Node.js | 22 | Runtime de JavaScript | nodejs.org |
| Express | 5 | Framework web para la API | expressjs.com |
| Jest | 30 | Framework de testing | jestjs.io |
| Supertest | 7 | HTTP assertions en tests | github.com/ladjs/supertest |
| StandardJS | 17 | Linter de código | standardjs.com |
| Docker | — | Containerización | docs.docker.com |
| Docker Compose | — | Orquestación local | docs.docker.com/compose |
| GitHub Actions | — | CI/CD | docs.github.com/actions |
| Docker Hub | — | Registry de imágenes | hub.docker.com |
| Render | — | Plataforma de despliegue | render.com/docs |
| OpenTelemetry | — | Instrumentación | opentelemetry.io |
| Grafana Cloud | — | Monitoreo (trazas y métricas) | grafana.com/docs |
| Método | Endpoint | Descripción | Status |
|---|---|---|---|
| GET | / |
Hello World | 200 |
| GET | /health |
Estado de la aplicación | 200 |
| GET | /error |
Simulación de error | 500 |
npm install
npm run devLa app levanta en http://localhost:3000.
Se utiliza Jest como framework de testing y Supertest para realizar requests HTTP sobre la app sin levantar un servidor real.
Supertest recibe la instancia de Express directamente (app) sin necesidad de que el servidor esté escuchando en ningún puerto, lo que hace los tests más rápidos y aislados.
npm run testHay tests para los tres endpoints cubriendo status code y estructura de respuesta.
Se usa Babel (@babel/preset-env + babel-jest) para que Jest pueda procesar los módulos ES (import/export), ya que Jest no los soporta nativamente y el proyecto usa "type": "module" en el package.json.
Docker garantiza que la app corra de la misma forma en cualquier entorno, eliminando el problema de "funciona en mi máquina". La imagen base es node:22-alpine — Alpine Linux es una distribución minimalista que reduce considerablemente el tamaño final de la imagen.
FROM node:22-alpine # Imagen base liviana
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev # Solo dependencias de producción
COPY . .
EXPOSE 3000
CMD ["node", "--import", "./instrumentation.mjs", "index.js"]El flag --import carga instrumentation.mjs antes de ejecutar index.js, asegurando que OpenTelemetry esté activo desde el primer momento.
# Buildear la imagen
docker build -t tp-devops .
# Correr el contenedor
docker run -p 3000:3000 tp-devopsDocker Compose orquesta múltiples contenedores como un único servicio. En este proyecto se usa para levantar el stack completo de observabilidad en desarrollo local.
| Servicio | Imagen | Puerto | Descripción |
|---|---|---|---|
| app | (build local) | 3000 | API Node.js |
| otel-collector | otel/opentelemetry-collector-contrib |
4317, 4318 | Recolector de telemetría |
| jaeger | jaegertracing/all-in-one |
16686 | Visualización de trazas |
| prometheus | prom/prometheus |
9090 | Almacenamiento de métricas |
| grafana | grafana/grafana |
3001 | Dashboard de métricas |
docker compose upApp → OTel Collector → Prometheus (métricas) → Grafana (visualización)
→ Jaeger (trazas)
El otel-collector-config.yml define cómo el collector recibe datos (OTLP gRPC/HTTP), los procesa en batches y los exporta a Jaeger y Prometheus.
El pipeline está dividido en tres workflows de GitHub Actions:
| Action | Versión | Descripción | Link |
|---|---|---|---|
actions/checkout |
v6 | Clona el repositorio en el runner | github.com/actions/checkout |
docker/login-action |
— | Login a Docker Hub | github.com/docker/login-action |
docker/metadata-action |
— | Genera tags y labels para la imagen | github.com/docker/metadata-action |
docker/build-push-action |
— | Buildea y pushea la imagen | github.com/docker/build-push-action |
actions/attest |
v4 | Genera attestation de la imagen | github.com/actions/attest |
Se dispara en cada push a cualquier branch.
Jobs:
- lint — verifica el estilo de código con StandardJS. StandardJS es un linter con reglas predefinidas sin necesidad de configuración.
- tests — corre la suite de Jest.
Ambos jobs corren en paralelo e instalan dependencias de forma independiente.
Se dispara cuando se crea un tag con formato vX.X.X desde la branch master.
La condición push: ${{ github.event.base_ref == 'refs/heads/master' }} en el step de build-push garantiza que solo se publique la imagen si el tag fue creado desde master, aunque el workflow haya sido disparado por un tag de otra branch.
Steps:
- Checkout del repositorio
- Login a Docker Hub usando secrets (
DOCKER_USERNAME,DOCKER_PASSWORD) docker/metadata-action— genera automáticamente los tags de la imagen basándose en el tag de git:1.0.0(versión exacta)1.0(última patch de esa minor)1(último release del major)latest(última versión publicada)
docker/build-push-action— buildea y publica la imagen en Docker Hubactions/attest— genera una attestation criptográfica que certifica que la imagen fue construida por este workflow y no fue modificada
Se dispara automáticamente cuando el workflow de publicación termina con éxito, usando el trigger workflow_run.
Llama al deploy hook de Render (URL almacenada en el secret RENDER_DEPLOY_HOOK_URL) mediante curl, lo que indica a Render que debe desplegar la última imagen disponible.
Push a cualquier branch → CI: lint + tests
Crear tag vX.X.X en master → Publish: imagen a Docker Hub con tags semver
→ Deploy: Render despliega la nueva imagen
Las imágenes de Docker siguen Semantic Versioning:
MAJOR(v2.0.0) — cambios que rompen compatibilidad con versiones anterioresMINOR(v1.1.0) — funcionalidad nueva manteniendo compatibilidadPATCH(v1.0.1) — corrección de bugs
Para publicar una nueva versión se crea una release desde GitHub asociando el tag correspondiente, lo que dispara el pipeline automáticamente.
OpenTelemetry es el estándar abierto para observabilidad. Permite capturar trazas y métricas sin acoplarse a un vendor específico — el mismo código puede enviar datos a Grafana Cloud, Datadog, New Relic, Jaeger, etc., cambiando solo variables de entorno.
- Trazas — registro de cada request con duración, status y spans internos
- Métricas — datos numéricos agregados (latencia, tasa de requests, errores)
El paquete @opentelemetry/auto-instrumentations-node instrumenta automáticamente librerías populares (Express, http, etc.) sin modificar el código de la app.
El archivo instrumentation.mjs se carga antes de la app mediante el flag --import de Node.js:
node --import ./instrumentation.mjs index.js
| Paquete | Descripción |
|---|---|
@opentelemetry/exporter-trace-otlp-http |
Exporta trazas via OTLP HTTP |
@opentelemetry/exporter-metrics-otlp-http |
Exporta métricas via OTLP HTTP |
App (Render) → Grafana Cloud via OTLP HTTP
| Variable | Descripción |
|---|---|
OTEL_SERVICE_NAME |
Nombre del servicio en Grafana |
OTEL_EXPORTER_OTLP_ENDPOINT |
Endpoint de Grafana Cloud |
OTEL_EXPORTER_OTLP_HEADERS |
Token de autenticación (Basic Auth) |
OTEL_EXPORTER_OTLP_PROTOCOL |
Protocolo de exportación (http/protobuf) |
OTEL_NODE_RESOURCE_DETECTORS |
Detectores de recursos del SDK (env,host,os) |
Grafana Cloud → Explore → seleccionar datasource de traces → tab Search.
Las trazas del endpoint /error aparecen con status Error, permitiendo crear dashboards de tasa de errores vs requests exitosos.
tp-devops/
├── .github/
│ └── workflows/
│ ├── pipeline.yml # CI: lint y tests en cada push
│ ├── publish.yml # CD: publica imagen Docker en tags vX.X.X
│ └── deploy.yml # CD: deploy a Render post-publicación
├── __tests__/
│ └── app.test.js # Tests unitarios de los endpoints
├── grafana/
│ └── provisioning/
│ └── datasources/
│ └── datasource.yml # Datasource de Prometheus para Grafana local
├── app.js # Definición de rutas Express
├── index.js # Entry point, levanta el servidor
├── instrumentation.mjs # Setup de OpenTelemetry SDK
├── babel.config.js # Config de Babel para Jest
├── Dockerfile # Imagen de producción
├── docker-compose.yml # Stack local de observabilidad
├── otel-collector-config.yml # Config del OTel Collector
├── prometheus.yml # Config de scraping de Prometheus
└── render.yaml # Config de infraestructura en Render