Skip to content

12‐Factor App

AnderProgramming edited this page Jul 17, 2026 · 4 revisions

La metodología 12-Factor App define doce principios para construir aplicaciones SaaS modernas que sean portables, escalables y resilientes. A continuación se documenta cómo Linker1 implementa cada factor, con referencias directas al código.


Resumen

# Factor Implementación en Linker1 Evidencia
I Codebase GitHub monorepo co-eiv-devsecops/linker1
II Dependencies Maven (pom.xml) pom.xml
III Config Variables de entorno LINKER_PORT, LINKER_DB_PATH, LD_SDK_KEY, OTEL_*, MYSQL_*, LOG_LEVEL
IV Backing Services SQLite (datos), MySQL (healthcheck), LaunchDarkly, Grafana Cloud Main.java, HealthCheck.java
V Build, Release, Run GitHub Actions → fat JAR → systemd ci.yml, pipeline.yml, release.yml
VI Processes Stateless (estado en SQLite, no en memoria) Proceso java -jar gestionado por systemd
VII Port Binding Puerto 8080 (autocontenido vía Javalin) Main.java:38-40
VIII Concurrency Proceso único con thread pool de Javalin/Jetty Escalable horizontalmente si se requiere
IX Disposability Startup rápido + shutdown graceful Main.java:118-126
X Dev/Prod Parity Mismo pipeline CI/CD, mismo artefacto JAR ci.yml, pipeline.yml
XI Logs Stdout como stream de eventos Logback → console + OpenTelemetry → Grafana
XII Admin Processes Scripts de administración deploy.sh, scripts/rollback.sh

Detalle por Factor

I. Codebase — Un codebase, múltiples deploys

"One codebase tracked in revision control, many deploys."

Linker1 mantiene un único repositorio Git en GitHub (co-eiv-devsecops/linker1). Desde este mismo codebase se generan los artefactos para todos los entornos. Las ramas siguen un flujo disciplinado:

  • main: rama de producción, protegida con PR + 1 aprobación + CI verde.
  • DEV: rama de integración, con las mismas protecciones.
  • feature/*, fix/*, chore/*: ramas efímeras para desarrollo.

El repositorio incluye CODEOWNERS (@AnderssonProgramming @Anderfg13 @daniel-pm19 @esteban0903 @Juan-Jose-D) para que todo cambio tenga un reviewer asignado automáticamente.

II. Dependencies — Declarar y aislar dependencias explícitamente

"Explicitly declare and isolate dependencies."

Todas las dependencias están declaradas explícitamente en pom.xml con versiones fijas:

<dependency>
    <groupId>io.javalin</groupId>
    <artifactId>javalin</artifactId>
    <version>6.5.0</version>
</dependency>

El plugin maven-assembly-plugin genera un fat JAR (linker1-1.0-jar-with-dependencies.jar) que empaqueta todas las dependencias en un único artefacto ejecutable, garantizando aislamiento total: el sistema operativo solo necesita Java 21 instalado.

Dependencias clave:

  • io.javalin:javalin:6.5.0 — servidor HTTP
  • org.xerial:sqlite-jdbc:3.49.1.0 — almacenamiento persistente
  • io.opentelemetry:opentelemetry-bom:1.63.0 — observabilidad (BOM para gestión de versiones)
  • com.launchdarkly:launchdarkly-java-server-sdk:7.8.0 — feature flags
  • ch.qos.logback:logback-classic:1.5.18 — logging
  • org.junit.jupiter:junit-jupiter:5.12.2 + org.mockito:mockito-core:5.18.0 — testing

III. Config — Almacenar configuración en el entorno

"Store config in the environment."

Cero configuración hardcodeada. Toda la configuración sensible o que varía entre entornos se inyecta mediante variables de entorno:

Variable Propósito Valor por defecto
LINKER_PORT Puerto HTTP del servidor 8080
LINKER_DB_PATH Ruta al archivo SQLite linker1.db
LD_SDK_KEY Clave del SDK de LaunchDarkly — (obligatoria, System.exit(1) si ausente)
LD_OFFLINE Modo offline de LaunchDarkly para testing false
OTEL_SERVICE_NAME Nombre del servicio en telemetría linker1
OTEL_EXPORTER_OTLP_ENDPOINT Endpoint OTLP (Grafana Cloud) — (degradación graciosa)
OTEL_EXPORTER_OTLP_HEADERS Headers de autenticación OTLP
OTEL_EXPORTER_OTLP_PROTOCOL Protocolo OTLP http/protobuf
MYSQL_HOST Host MySQL para healthcheck localhost
MYSQL_DATABASE Base de datos MySQL ""
MYSQL_USER Usuario MySQL ""
MYSQL_PWD Contraseña MySQL ""
LOG_LEVEL Nivel de verbosidad de logs INFO

En producción, estas variables se configuran en la unidad de systemd (/etc/systemd/system/linker1.service) como líneas Environment="...". En CI, se inyectan como GitHub Secrets y Variables.

IV. Backing Services — Tratar backing services como recursos conectables

"Treat backing services as attached resources."

Linker1 trata cada servicio externo como un recurso conectable que se puede intercambiar cambiando una variable de entorno:

┌─────────────┐       ┌──────────┐
│  Linker1    │──────▶│  SQLite   │  LINKER_DB_PATH
│  (App)      │       └──────────┘
│             │       ┌──────────┐
│             │──────▶│  MySQL   │  MYSQL_HOST / MYSQL_USER / MYSQL_PWD
│             │       └──────────┘
│             │       ┌──────────────┐
│             │──────▶│ LaunchDarkly │  LD_SDK_KEY
│             │       └──────────────┘
│             │       ┌──────────────┐
│             │──────▶│ Grafana Cloud│  OTEL_EXPORTER_OTLP_ENDPOINT
└─────────────┘       └──────────────┘
  • SQLite: almacenamiento de URLs acortadas. Conectable vía LINKER_DB_PATH.
  • MySQL: usado exclusivamente para el healthcheck (SELECT 1). Si no está configurado, /healthz devuelve 503 pero la app sigue funcionando.
  • LaunchDarkly: evaluación de feature flags en tiempo real.
  • Grafana Cloud: destino de métricas, traces y logs vía OTLP.

V. Build, Release, Run — Separación estricta de etapas

"Strictly separate build and run stages."

Las tres etapas están claramente separadas:

Etapa Herramienta Qué hace
Build mvn clean package en GitHub Actions Compila, ejecuta tests, genera el fat JAR
Release release.yml + GitHub Releases Empaqueta el JAR con un tag SemVer (v1.0.0) y lo publica
Run systemd en la VM de OCI Ejecuta java -jar linker1.jar con las variables de entorno del ambiente

El mismo artefacto binario (JAR) se usa en CI (smoke tests), se publica como Release, y se despliega en producción. La configuración se inyecta en runtime, nunca en build time.

VI. Processes — Ejecutar la app como procesos stateless

"Execute the app as one or more stateless processes."

Linker1 se ejecuta como un proceso único stateless:

  • El estado persistente vive en SQLite (archivo en /var/lib/linker1/linker1.db), no en la memoria del proceso.
  • Cualquier instancia del proceso puede ser reemplazada sin pérdida de datos.
  • No hay sesiones sticky, caché en memoria, ni estado compartido entre requests más allá de la base de datos.

VII. Port Binding — Exportar servicios mediante port binding

"Export services via port binding."

Linker1 es autocontenido: Javalin (sobre Jetty embebido) escucha directamente en el puerto 8080:

int port = Integer.parseInt(
    System.getenv().getOrDefault("LINKER_PORT", "8080")
);
app.start(port);

No depende de un contenedor de aplicaciones externo (Tomcat, WildFly). Nginx actúa como reverse proxy (80 → 8080) por conveniencia, pero la app funciona de forma autónoma.

VIII. Concurrency — Escalar mediante el modelo de procesos

"Scale out via the process model."

La aplicación usa el thread pool de Javalin/Jetty para manejar concurrencia dentro de un proceso. Adicionalmente:

  • Un ScheduledExecutorService de un solo thread muestrea métricas de sistema cada 30 segundos.
  • El modelo es horizontalmente escalable: si fuera necesario, se podrían ejecutar múltiples instancias detrás de un balanceador (SQLite se reemplazaría por MySQL en ese escenario).

IX. Disposability — Maximizar la robustez con startup rápido y shutdown graceful

"Maximize robustness with fast startup and graceful shutdown."

Startup rápido: la aplicación inicia en pocos segundos (no hay ORM, no hay scanning de classpath pesado).

Shutdown graceful: un ShutdownHook cierra ordenadamente todos los recursos:

Runtime.getRuntime().addShutdownHook(new Thread(() -> {
    telemetryScheduler.shutdown();  // para de muestrear métricas
    ldClient.close();               // cierra LaunchDarkly
    conn.close();                   // cierra SQLite
}));

Systemd está configurado con Restart=always y RestartSec=5, garantizando que si el proceso muere, se reinicia automáticamente en 5 segundos.

X. Dev/Prod Parity — Mantener desarrollo y producción lo más similares posible

"Keep development, staging, and production as similar as possible."

  • Mismo lenguaje y stack en todos los entornos: Java 21 + Maven + SQLite.
  • Mismo pipeline CI/CD: ci.yml ejecuta los mismos pasos (build, test, package, smoke) que corren antes de producción.
  • Mismo artefacto: el JAR generado en CI es exactamente el que se despliega en producción.
  • Variables de entorno: la única diferencia entre local y producción son los valores de las variables (endpoint OTLP, SDK key, etc.).
  • Los smoke tests de CI recrean el mismo escenario que producción: inician la app, crean links, verifican redirecciones.

XI. Logs — Tratar logs como streams de eventos

"Treat logs as event streams."

Linker1 nunca escribe archivos de log. Los logs se emiten como un stream a stdout:

  • Console: Logback escribe en stdout con formato estándar (timestamp, nivel, clase, mensaje).
  • OpenTelemetry: simultáneamente, el OpenTelemetryAppender de Logback exporta cada log line como un log record OTLP hacia Grafana Cloud.
<!-- logback.xml -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">...</appender>
<appender name="OTEL" class="io.opentelemetry.instrumentation.logback.appender.v1_0.OpenTelemetryAppender">
    <captureExperimentalAttributes>true</captureExperimentalAttributes>
    <captureCodeAttributes>true</captureCodeAttributes>
</appender>

En producción, systemd captura stdout como journald, accesible con journalctl -u linker1.service. En Grafana Cloud, los logs se consultan en Loki junto con métricas (Mimir) y traces (Tempo), correlacionados por el mismo service.name.

XII. Admin Processes — Ejecutar tareas administrativas como procesos one-off

"Run admin/management tasks as one-off processes."

Las tareas administrativas se manejan como scripts independientes que operan en el mismo entorno:

Script Propósito
deploy.sh Despliegue completo: clone/pull → build → systemd → nginx
scripts/rollback.sh Rollback a un tag SemVer anterior
scripts/linker1 Launcher ejecutable para el fat JAR

Estos scripts viven en el mismo repositorio, se versionan junto con el código, y operan sobre la misma infraestructura (mismo directorio, misma VM, mismo systemd service).


Conclusión

Linker1 cumple con los 12 factores de forma natural gracias a decisiones de diseño deliberadas: variables de entorno para toda la configuración, un único artefacto desplegable (fat JAR), backing services desacoplados, logs como streams, y scripts administrativos versionados. Esto la hace portable, resiliente y lista para operar en cualquier entorno cloud.

Clone this wiki locally