Skip to content

Continuous Learning

Anderson Fabian Garcia Nieto edited this page Jul 9, 2026 · 5 revisions

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.


🧪 Experimentación con Feature Flags

Lo que hicimos

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.

Flag implementado: new-ui

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

Lecciones aprendidas

  1. 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.

  2. 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.

  3. 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.

  4. 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 probar StaticRoutes. Sin esa separación, los tests habrían sido frágiles y lentos.


🔄 Despliegues Graduales

Estrategia de Rollout

Definimos un proceso progresivo para lanzar features:

Deploy con flag OFF → Testing interno → 10% usuarios → 50% → 100% → Cleanup

Lecciones aprendidas

  1. 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.

  2. 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.


🔴 Análisis de Fallos del Pipeline

Fallo 1: El protocolo OTLP incorrecto

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.

Fallo 2: Secrets con caracteres especiales rompen systemd

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.

Fallo 3: Endpoint OTLP vacío vs ausente

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
fi

Lecció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.

Fallo 4: Branch Protection con checks inexistentes

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.

Fallo 5: Fat JAR y el registro de JDBC drivers

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.


📈 Mejoras Post-Incidente

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

🎓 Reflexiones como Equipo de Ingeniería

1. La observabilidad no es opcional — es infraestructura

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.

2. La resiliencia se diseña, no se espera

  • Restart=always en 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.

3. El pipeline es tan importante como el código

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.

4. Los errores silenciosos son el verdadero enemigo

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".

5. La deuda técnica de seguridad es la más cara

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".

6. Infrastructure as Code no es un lujo

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).

7. DevSecOps es una mentalidad, no un checklist

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.


📚 Recursos que nos ayudaron

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

Clone this wiki locally