-
Notifications
You must be signed in to change notification settings - Fork 1
Continuous Learning
El aprendizaje continuo es un pilar fundamental del modelo operativo de la nube. No se trata solo de construir software, sino de aprender de cada iteración, cada fallo y cada decisión para mejorar continuamente el sistema y las prácticas del equipo.
Este documento recoge las lecciones aprendidas, los experimentos realizados y las mejoras implementadas a lo largo del proyecto Linker1.
Implementamos feature flags con LaunchDarkly para desacoplar el deployment del release, permitiendo desplegar código nuevo a producción sin exponerlo inmediatamente a los usuarios.
El flag new-ui controla qué versión de la interfaz web se sirve:
// StaticRoutes.java
String path = featureFlags.isNewUiEnabled()
? "/v2/index-v2.html" // nueva interfaz
: "/v1/index.html"; // interfaz original-
Deployments sin miedo: con la nueva UI desplegada pero el flag en OFF, pudimos validar que el código estaba correctamente empaquetado en producción sin riesgo. Si algo salía mal, no necesitábamos ni un rollback — solo cambiar el flag.
-
El poder del desacoplamiento: en desarrollo tradicional, "desplegar" y "lanzar una feature" son el mismo paso. Con feature flags, se convierten en dos decisiones independientes. Esto reduce dramáticamente la presión en el momento del deploy.
-
Testing en producción de forma segura: pudimos probar la nueva UI en el entorno real de producción, con tráfico real, antes de habilitarla para todos los usuarios. Esto no es "cowboy coding" — es progressive delivery controlado.
-
La arquitectura importa: encapsular LaunchDarkly detrás de
FeatureFlags(con inyección de dependencias) hizo que los tests fueran triviales — no necesitamos conectar al SDK real para probarStaticRoutes. Sin esa separación, los tests habrían sido frágiles y lentos.
Definimos un proceso progresivo para lanzar features:
Deploy con flag OFF → Testing interno → 10% usuarios → 50% → 100% → Cleanup
-
El rollout gradual no es solo para grandes empresas: incluso en un proyecto universitario, la diferencia entre "algo se rompió para todos" y "algo se rompió para el 10%" es enorme en términos de impacto y tiempo de recuperación.
-
El cleanup es tan importante como el rollout: los feature flags tienen un costo de mantenimiento. Dejar un flag viejo activo es deuda técnica. Documentamos explícitamente cuándo y cómo eliminar tanto el flag como el código legacy.
Qué pasó: La aplicación se desplegó correctamente, respondía 200 en todas las rutas, y los health checks pasaban. Pero no llegaba ninguna telemetría a Grafana Cloud.
Causa raíz: El SDK de OpenTelemetry usa grpc por defecto como protocolo OTLP, pero el gateway de Grafana Cloud (otlp-gateway-*.grafana.net) solo acepta http/protobuf. El error era silencioso: Failed to export ... Server responded with UNIMPLEMENTED aparecía solo en logs internos del SDK que no estábamos monitoreando.
Solución: Hardcodear OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf en la unidad systemd.
Lección: Los errores silenciosos son los más peligrosos. La app se veía completamente saludable mientras silenciosamente no exportaba nada. Siempre valida que tu pipeline de observabilidad realmente funciona end-to-end, no solo que la app arranca sin errores.
Qué pasó: Un token de Grafana Cloud contenía el carácter %. Al inyectarlo en la unidad systemd como Environment="OTEL_EXPORTER_OTLP_HEADERS=...", systemd lo interpretó como un specifier (%h = home directory, %n = unit name) y la unidad se rechazó. El servicio no arrancó.
Causa raíz: systemd trata % como inicio de un specifier y " como fin de un valor quoted en directivas Environment=.
Solución: Implementar una función systemd_escape_value() en tres lugares (deploy.sh, pipeline.yml, Terraform):
systemd_escape_value() {
printf '%s' "$1" | sed -e 's/%/%%/g' -e 's/"/\\"/g'
}Lección: No asumas que un string es "solo un string". Cada capa del stack (shell, YAML, systemd, base64) tiene sus propias reglas de escape. Un pipeline robusto debe escapar valores en cada frontera de confianza.
Qué pasó: Al redeployer sin pasar OTEL_EXPORTER_OTLP_ENDPOINT, la variable se escribía como Environment="OTEL_EXPORTER_OTLP_ENDPOINT=" (vacía pero presente). El SDK lanzaba ConfigurationException: OTLP endpoint must be a valid URL y la aplicación no arrancaba.
Causa raíz: El SDK distingue entre "variable no definida" (usa defaults internos) y "variable definida con valor vacío" (error de configuración).
Solución: Solo escribir la línea Environment= cuando el valor tiene contenido real:
if [ -n "$OTEL_EXPORTER_OTLP_ENDPOINT" ]; then
echo "Environment=\"OTEL_EXPORTER_OTLP_ENDPOINT=$OTEL_EXPORTER_OTLP_ENDPOINT\"" | \
sudo tee -a /etc/systemd/system/linker1.service > /dev/null
fiLección: "Empty" y "absent" no son lo mismo. Muchas bibliotecas distinguen entre un valor no definido y un valor vacío. Diseña tu pipeline para preservar la semántica de "ausencia" cuando corresponda.
Qué pasó: Al configurar Branch Protection en DEV, incluimos como checks requeridos los jobs del pipeline.yml. Pero ese workflow solo se ejecuta en push a main, nunca en PRs a DEV. Todos los PRs quedaron permanentemente bloqueados — GitHub esperaba checks que nunca llegarían.
Solución: Remover los checks de pipeline.yml de la configuración de branch protection y dejar solo los de ci.yml (que sí corre en PRs).
Lección: Un check requerido que nunca se ejecuta bloquea todo. Entiende cuándo corre cada workflow antes de hacerlo required. Lo que es "conveniencia" para tu proceso puede convertirse en "bloqueo total" si lo configuras mal.
Qué pasó: Al agregar el driver de MySQL, el maven-assembly-plugin fusionó los archivos SPI de ambos drivers (META-INF/services/java.sql.Driver) con estrategia "last wins", eliminando silenciosamente el registro del driver MySQL.
Solución: Registro explícito con Class.forName("com.mysql.cj.jdbc.Driver").
Lección: Los build tools hacen cosas silenciosas con los recursos. Cuando empaquetas un fat JAR, investiga cómo maneja archivos con el mismo path de diferentes dependencias. Lo que funciona con mvn exec:java puede romperse con java -jar.
Cada fallo encontrado llevó a mejoras concretas y permanentes:
| Fallo | Mejora implementada |
|---|---|
| Protocolo OTLP incorrecto | Hardcodeo de http/protobuf + documentación del porqué |
Secrets con %
|
systemd_escape_value() en 3 deployment paths |
| Endpoint vacío vs ausente | Escritura condicional de Environment=
|
| Checks inexistentes en Branch Protection | Auditoría de qué workflows corren dónde |
| SPI merge en fat JAR |
Class.forName() explícito + comentario explicativo |
| Pipeline fallido sin diagnóstico | Dump automático de journalctl + systemd-analyze verify en el pipeline |
Sin logs, métricas y traces, operar en producción es volar a ciegas. OpenTelemetry nos dio la capacidad de entender qué hacía nuestra aplicación en tiempo real, correlacionar problemas entre capas, y diagnosticar issues que de otra forma habrían tomado horas.
-
Restart=alwaysen systemd no es un "nice to have" — es la diferencia entre un incidente de 5 segundos y uno de horas. - Rollback automático no es para "cuando algo salga mal" — es para aceptar que algo va a salir mal y tener un plan.
- Health checks no son para reportes — son para que el sistema se auto-diagnostique.
Invertimos tanto tiempo diseñando el pipeline de CI/CD como escribiendo la aplicación. Pero esa inversión se paga sola: cada deployment es repetible, verificable y reversible. Deployer deja de ser un evento estresante y se convierte en una operación rutinaria.
El protocolo OTLP incorrecto fue nuestro mayor aprendizaje. La aplicación se veía perfectamente saludable mientras no exportaba absolutamente nada. Desde entonces, tratamos cada integración externa como una señal que hay que verificar end-to-end, no solo "que no crashee".
Feature flags sin cleanup, secrets sin rotación, branches sin protección — son problemas que parecen pequeños hasta que explotan. Aprendimos a tratarlos como bugs de primera clase, no como "cosas para hacer después".
Tener la VM definida en Terraform (infra/main.tf) y provisionada con cloud-init.yaml significa que podemos recrear todo el entorno desde cero. Si la VM se corrompe, si OCI tiene un problema en la zona, o si necesitamos escalar — la infraestructura es un terraform apply de distancia, no una serie de pasos manuales que alguien recuerda (o no).
No se trata de agregar CODEOWNERS o Branch Protection como un checkbox. Se trata de internalizar que cada cambio pasa por review, cada secret está gestionado, y cada deploy es auditable. Es más lento al principio, pero exponencialmente más seguro a largo plazo.
| Recurso | Por qué fue útil |
|---|---|
| 12-Factor App | Marco de referencia para decisiones arquitectónicas |
| OpenTelemetry Java Docs | Instrumentación manual vs auto-agent |
| Grafana Cloud OTLP Gateway | Configuración específica del endpoint y protocolo |
| systemd Unit File | Especificadores y escape de valores |
| LaunchDarkly Java SDK | Integración server-side con feature flags |
| OCI Bastion Service | Acceso SSH sin IP pública |