-
Notifications
You must be signed in to change notification settings - Fork 1
12‐Factor App
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.
| # | 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
|
"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.
"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
"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.
"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,/healthzdevuelve 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.
"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.
"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.
"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.
"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
ScheduledExecutorServicede 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).
"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.
"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.ymlejecuta 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.
"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
OpenTelemetryAppenderde 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.
"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).
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.