Un pipeline en verde es una afirmación muy chica: el último proceso de la cadena devolvió exit code 0. Eso es todo lo que garantiza. Entre esa afirmación y "el sistema quedó sano" hay una distancia concreta, medible, y con causas mecánicas identificables. Este artículo la recorre desde adentro: cómo se produce el verde, qué relojes existen para cerrar la brecha, cuáles de esos relojes se pueden falsear y por qué, y qué instrumentación hace falta para que apretar el botón de rollback sea una decisión evaluable.
Vale desarmarlo con cuidado, porque la mayoría de las decisiones de observabilidad de deploy se heredan del repo del que se copió el pipeline y nunca se vuelven a discutir.
Qué afirma exactamente el verde
Un job de CI es un árbol de procesos. El runner ejecuta cada step, mira el exit code, y si alguno es distinto de cero corta la cadena. El estado final del job es una función de esos exit codes y nada más. Cuando el CD reporta un deployment_status en estado success, está reportando que el paso que vos elegiste como "el deploy" terminó bien.
La pregunta interesante es cuál es ese paso y qué espera. Mirá dos comandos que la gente usa indistintamente:
kubectl apply -f deploy.yaml
# exit 0 = el API server validó y persistió el objeto en etcd.
# No corrió un solo pod todavía.
kubectl rollout status deploy/api --timeout=300s
# exit 0 = el ReplicaSet nuevo alcanzó la cantidad de réplicas disponibles.
El primero es un ACK de admisión: no observa nada del sistema en ejecución. El segundo sí espera, pero conviene saber qué significa "disponible". Un pod se cuenta como available cuando pasó su readiness probe y se mantuvo listo durante minReadySeconds — un campo del Deployment cuyo valor por defecto es 0. Con la configuración por defecto, un pod se declara disponible el primer instante en que la readiness probe devuelve OK, sin haber servido un solo request real. Del otro lado, progressDeadlineSeconds (default 600) define cuánto espera el controller antes de marcar el rollout como fallido.
De ahí sale la definición precisa de la brecha. El verde afirma convergencia a un estado declarado, y el estado declarado describe el artefacto, no su comportamiento bajo carga.
Esa definición es predictiva. Si el verde solo prueba convergencia, todo lo que dependa de tiempo o de volumen queda del otro lado: caminos de código con carga perezosa que recién se compilan al primer request de ese endpoint, pools de conexiones que se agotan al pico y no al arranque, caches frías que disparan un thundering herd contra la base, GC en régimen estacionario, un cron que corre a la medianoche, una migración que se comporta distinto con concurrencia real. Ninguna de esas fallas puede aparecer antes del exit code 0, porque ninguna ocurre en la ventana que el comando observa.
Las cuatro métricas, leídas como relojes
Accelerate (Forsgren, Humble y Kim) formalizó cuatro medidas del rendimiento de entrega, agrupadas en dos ejes: throughput —frecuencia de despliegue y lead time for changes— y estabilidad —change failure rate y tiempo de recuperación ante un despliegue fallido—. El hallazgo central del programa es que los dos ejes se mueven juntos: los equipos que despliegan más seguido también fallan menos y se recuperan más rápido. No hay un trade-off que administrar.
El modelo se movió desde entonces, y conviene fechar lo que uno cita. Al momento de escribir esto, DORA publica cinco métricas: el viejo time to restore service pasó a llamarse failed deployment recovery time —precisamente porque se medía mal, mezclando cualquier caída con las causadas por un deploy— y se sumó deployment rework rate. El agrupamiento también cambió: hoy failed deployment recovery time queda del lado de throughput, y las de inestabilidad son change fail rate y deployment rework rate. El análisis que sigue trabaja sobre las cuatro clásicas, que son las que implementan las herramientas de hoy —la API de DORA metrics de GitLab sigue el modelo de cuatro—, pero la quinta se lee con el mismo criterio.
Mecánicamente, cada métrica es un contador o un cronómetro con dos extremos. Ahí está todo lo que importa: quién define cada extremo.
| Métrica | Arranca en | Corta en | Fuente de verdad natural |
|---|---|---|---|
| Frecuencia de despliegue | — (es un contador) | — | Eventos de deploy a producción |
| Lead time for changes | Timestamp del commit más viejo del changeset | Ese código sirviendo tráfico | Git + registro de deploys |
| Change failure rate | — (es un cociente) | — | Deploys fallidos / deploys totales |
| Failed deployment recovery time | Inicio de la degradación | Restauración del servicio | Alertas y registro de incidentes |
Por qué dos de estas se falsean solas
Una métrica se falsea cuando alguno de sus extremos lo define, con criterio propio, la misma persona a la que se evalúa con el resultado. Aplicado al cuadro de arriba, el diagnóstico sale directo.
Lead time es el más duro de falsear. Su extremo inicial vive en Git, en objetos que ya existían antes de que nadie supiera que se iban a medir. La medición correcta toma el timestamp del commit más viejo del changeset, y ahí está el matiz que casi todas las implementaciones se comen: un squash colapsa N commits en uno nuevo cuyo author date es el del squash, no el del trabajo original, así que leer el commit resultante borra el t0 real. La distorsión más común es más simple todavía: medir desde el merge commit, que por construcción nace minutos antes del deploy y deja lead times fantásticos. En los dos casos el arreglo es leer la lista de commits del pull request y quedarse con el más viejo, un dato que la API del VCS devuelve entero.
Frecuencia de despliegue se falsea en la definición de "un deploy". Es un contador puro: el numerador lo define quien cuenta. Partir una release en diez promotions consecutivas del mismo artefacto multiplica el número por diez sin mover una línea de código. Contar deploys a staging lo infla más. El arreglo es mecánico: contá únicamente los eventos donde cambió el digest de la imagen que corre en producción. Un re-deploy del mismo digest es una operación, no un cambio.
Change failure rate se falsea en el numerador. Si "falla" significa "alguien abrió un incidente", el equipo medido decide cuántas fallas tuvo. El arreglo también es mecánico: definí falla por eventos observables. Un deploy cuenta como fallido si fue seguido de un rollback, si hubo un hotfix al mismo servicio dentro de una ventana fija, o si el error budget del servicio se consumió por encima del umbral en la ventana atribuible a ese deploy. Las tres condiciones se derivan de datos que ya existen.
Una métrica de entrega es confiable en la medida en que sus dos extremos vengan de sistemas que no participan de la conversación sobre la métrica.
Nada de esto pide herramientas nuevas. GitLab expone las cuatro en su API de DORA metrics, y el proyecto open source Four Keys de Google Cloud las deriva de webhooks de tu VCS y tu CD —para GitLab acepta tanto eventos de pipeline como de deployment, y cuál de los dos configurás ya cambia el conteo—. Lo que hay que revisar en cualquiera de los dos es la regla de conteo, porque es ahí donde vive el problema.
El reloj de la recuperación premia la ceguera
El cuarto reloj merece un párrafo aparte, porque su extremo blando produce un incentivo invertido y no una simple imprecisión.
El cierre es externo y observable: la alerta se apaga, el SLI vuelve dentro del umbral. El arranque, en cambio, suele ser el momento en que un humano declaró el incidente. Con esa definición, el cronómetro empieza a correr cuando el equipo se da cuenta, así que darse cuenta tarde acorta el número medido.
La comparación deja el efecto a la vista. Un equipo con buena detección se entera a los tres minutos de que el deploy degradó el servicio, discute, prueba dos hipótesis y resuelve a los cuarenta: reporta 37 minutos de recuperación. Otro equipo no tiene alertas, se entera a la hora por el ticket de un cliente y resuelve en veinte: reporta 20 minutos. El segundo se ve mejor en el tablero y sirvió errores durante ochenta minutos.
El arreglo es tomar el extremo inicial de un sistema que no participa de la conversación: el primer disparo de la alerta que después se asoció al incidente, o el timestamp del deploy cuando la causa fue el deploy. Ese cambio de origen mueve el número para arriba en casi todos los equipos, y es la única versión que se puede comparar entre servicios.
Correlacionar un deploy con las métricas del sistema
Para atribuir un cambio de comportamiento a un deploy necesitás una dimensión compartida entre el registro de deploys y las series de tiempo. Esa dimensión es la versión, y tiene que estar como label en todo lo que emite el servicio.
El contrato ya está estandarizado. Las semantic conventions de OpenTelemetry definen service.name, service.version y deployment.environment.name como resource attributes, y al ser del resource se propagan a traces, métricas y logs del mismo proceso sin que lo pidas en cada punto de instrumentación. Datadog implementa la misma idea con unified service tagging: DD_ENV, DD_SERVICE, DD_VERSION. Lo importante del mecanismo es que el valor de la versión lo inyecta el pipeline en el entorno del contenedor, y no queda escrito en el código.
Con eso, la comparación entre versiones es una query. Si tus métricas de aplicación no llevan el label de versión, lo agregás con un join contra un gauge de build info:
sum by (version) (
rate(http_requests_total{service="api",status=~"5.."}[2m])
* on(instance) group_left(version) app_build_info
)
/
sum by (version) (
rate(http_requests_total{service="api"}[2m])
* on(instance) group_left(version) app_build_info
)
Ese join es también la respuesta al caso OTLP. Si ingerís a Prometheus por OTLP, los resource attributes no viajan como labels de cada serie: service.name, service.namespace y service.instance.id mapean a job e instance, y el resto queda depositado en la métrica target_info. Filtrar directo por service_version no devuelve nada. Hace falta el mismo group_left contra target_info, o promover explícitamente esos atributos a labels en la configuración de ingesta.
El segundo instrumento es la marca temporal del deploy sobre los mismos dashboards. La Grafana annotations API acepta un POST desde el propio job, y Sentry maneja el concepto de release con sentry-cli releases, que habilita crash-free session rate por versión.
curl -sS -X POST "$GRAFANA_URL/api/annotations" \
-H "Authorization: Bearer $GRAFANA_TOKEN" \
-H "Content-Type: application/json" \
-d "{\"tags\":[\"deploy\",\"$SERVICE\",\"$GIT_SHA\"],\"text\":\"deploy $SERVICE $GIT_SHA\"}"
Ahora la consecuencia mecánica menos obvia. Comparar "los quince minutos anteriores contra los quince posteriores" es una comparación sesgada, porque entre las dos ventanas cambió la composición del tráfico: la hora, el mix de clientes, el batch nocturno. La única forma de eliminar ese sesgo es que las dos versiones reciban el mismo tráfico al mismo tiempo. Eso no es una preferencia estética por el canary: es la razón por la que el canary detecta cosas que el blue/green no.
| Estrategia | Qué señal produce | Qué no ve |
|---|---|---|
| Recreate | Ninguna comparativa: hay downtime y una sola versión | Todo lo que no explote al arranque |
| Rolling update | Mezcla de versiones sin separar métricas por versión | La atribución, si no hay label de versión |
| Blue/green | Producción completa antes y después, en ventanas distintas | Diferencias enmascaradas por el cambio de tráfico entre ventanas |
| Canary | Dos versiones, mismo tráfico, mismo instante | Fallas que necesitan volumen alto o mucho tiempo |
| Feature flag | El artefacto ya está desplegado; el comportamiento se enciende por cohorte y se apaga sin redeploy | Fallas del artefacto mismo: arranque, migraciones, dependencias nuevas |
| Shadow / mirroring | Comportamiento real sin exponer usuarios | Efectos de escritura, salvo que aísles el estado |
Instrumentar para decidir el rollback
Un rollback con criterio necesita tres cosas definidas antes del deploy: la señal, el umbral y la ventana. Si las tres existen, la decisión se puede evaluar sola; si falta una, la decisión vuelve a ser una discusión.
La señal tiene que ser un SLI del servicio: proporción de requests exitosos, latencia en un percentil alto, tasa de sesiones sin crash. Un contador de infraestructura como CPU o memoria describe la máquina y no la experiencia. El umbral conviene expresarlo como burn rate contra el error budget —la técnica multiventana y multi-burn-rate del SRE Workbook de Google— porque una tasa de error absoluta dispara distinto según el volumen, y el burn rate normaliza eso. La ventana es el tiempo de bake: cuánto tiene que sostenerse la señal antes de promover.
Argo Rollouts implementa exactamente esa estructura como objeto declarativo, y Flagger resuelve lo mismo del lado de service mesh:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-rate
spec:
args:
- name: service
- name: version
metrics:
- name: error-rate
interval: 1m
count: 10 # ventana de bake: 10 mediciones, una por minuto
failureLimit: 2 # tolera dos mediciones malas antes de abortar
successCondition: result[0] < 0.01
provider:
prometheus:
address: http://prometheus.monitoring.svc:9090
query: |
sum(rate(http_requests_total{service="{{args.service}}",version="{{args.version}}",status=~"5.."}[2m]))
/
sum(rate(http_requests_total{service="{{args.service}}",version="{{args.version}}"}[2m]))
Las dos queries filtran por version y no solo por service, y esa media línea es la que sostiene todo lo de la sección anterior: sin el filtro, el análisis mide canary y estable juntos, y el error del canary —que recibe una fracción del tráfico— queda diluido en el volumen de la versión vieja hasta desaparecer bajo el umbral. El valor del argumento lo aporta el propio Rollout:
analysis:
templates:
- templateName: error-rate
args:
- name: service
value: api
- name: version
valueFrom:
podTemplateHashValue: Latest
Lo que hace ese objeto es convertir los tres parámetros en una condición evaluable y darle al controller la autoridad de abortar. failureLimit existe por una razón mecánica: una medición aislada por encima del umbral puede ser ruido de muestreo, y abortar con n=1 genera rollbacks espurios que erosionan la confianza en el mecanismo.
Hay un tercer estado que conviene conocer antes de escribir el YAML. Si definís successCondition y failureCondition a la vez, todo resultado que no satisfaga ninguna de las dos devuelve Inconclusive, y un análisis inconcluso pausa el rollout esperando intervención manual en lugar de decidir. Definí una sola de las dos condiciones si lo que querés es una decisión automática, y reservá el par para los casos donde la pausa sea deliberada.
Cuándo el rollback automático es la decisión equivocada
El canary decide con estadística, y la estadística necesita muestra. Hacé la cuenta antes de montar el mecanismo.
Un servicio interno que recibe quince requests por minuto y un canary con el 100% del tráfico durante cinco minutos junta 75 requests. Con una tasa de error base del 1% esperás 0,75 errores en esa muestra; con una del 5%, esperás 3,75. Un solo error de más ya representa 1,3 puntos porcentuales. La ventana no distingue una regresión de una coincidencia, y cualquier umbral que pongas va a producir falsos positivos o falsos negativos según de qué lado lo corras. Con el 10% del tráfico al canary, la muestra son 7 requests y el análisis directamente no existe.
Debajo de cierto volumen quedan tres caminos, y todos son razonables:
- Alargar la ventana de bake hasta juntar muestra, aceptando que el deploy tarda horas.
- Cambiar la señal por una de mayor frecuencia: latencia por percentil, que tiene una observación por request, en lugar de una tasa de errores que cuenta eventos raros.
- Dejar la promoción manual y usar el análisis como reporte, sin autoridad para abortar.
El costo también decide. Blue/green mantiene dos flotas completas en pie durante toda la ventana de verificación: si la ventana es de una hora, pagás capacidad duplicada esa hora, y si querés una ventana de un día, pagás capacidad duplicada un día. Ese costo es el que empuja las ventanas de verificación a durar menos de lo que necesita la señal, y conviene reconocerlo como restricción de presupuesto en lugar de discutirlo como criterio técnico. El canary paga solo la capacidad del subconjunto, y el feature flag no paga capacidad extra pero tampoco protege del artefacto.
Cuando volver atrás ya no está disponible
Falta el instrumento que más se olvida: saber si el rollback sigue siendo una salida posible. La máquina de decisión de arriba tiene una rama que asume que volver al digest anterior restaura el estado anterior, y hay tres familias de cambios que rompen ese supuesto.
| Qué cambió | Por qué el rollback no alcanza | Qué lo vuelve reversible |
|---|---|---|
| Migración de esquema forward-only | El artefacto viejo no sabe leer el esquema nuevo; volver la imagen atrás produce un incidente distinto y peor | Patrón expand/contract: expandir compatible, desplegar, contraer en un deploy posterior |
| Mensajes ya consumidos y confirmados con el formato nuevo | El ack es irreversible: los mensajes no vuelven a la cola, y el consumer viejo no puede reprocesar lo que ya no está | Versionar el payload y sostener el consumer capaz de leer las dos versiones durante toda la ventana de rollback |
| Rotation de un secret completada | El valor viejo ya no existe en el store, así que el artefacto anterior arranca sin credencial válida | Mantener las dos versiones activas en el secret manager hasta que expire la ventana de rollback, y recién ahí desactivar la vieja |
La forma de saberlo en el momento de decidir es marcarlo en el propio deploy: un flag en los metadatos del release que declare si ese cambio tocó esquema, contrato de mensajes o secrets. Con ese flag, el controller elige entre rollback y remediación forward sin consultar a nadie. Sin él, la pregunta "¿puedo volver atrás?" se responde leyendo diffs bajo presión, que es la peor condición posible para leer un diff.
Herramientas
| Herramienta | Para qué sirve acá |
|---|---|
| Four Keys | Implementación de referencia del equipo DORA. Leer sus queries es la mejor forma de ver dónde se define un deploy y dónde un fallo |
| Apache DevLake | Consolida Jira, Git, CI y monitoreo en un modelo único. La opción cuando el lead time hay que reconstruirlo cruzando fuentes |
| Argo Rollouts | Canary y blue/green en Kubernetes con promotion o rollback automático según métricas |
| AnalysisTemplate de Argo Rollouts | La doc del mecanismo de análisis: umbrales contra Prometheus, Datadog o New Relic, y el detalle de los tres estados |
| Flagger | El equivalente del ecosistema Flux, con desplazamiento gradual de tráfico y webhooks de prueba por paso |
| Anotaciones de Grafana | El instrumento más barato de todos: una línea vertical en cada gráfico con el SHA que la produjo |
Por dónde empezar
El orden importa, porque cada paso vuelve verificable al siguiente.
- Marcá el deploy. Una anotación de Grafana con servicio, SHA y digest, posteada desde el job. Es un
curly elimina el trabajo de cruzar timestamps a mano durante un incidente. - Etiquetá con la versión.
service.versioncomo resource attribute, inyectada por el pipeline. Si las métricas de aplicación no la llevan, armá el join contraapp_build_infootarget_info. - Escribí la regla de conteo. Un deploy es un digest distinto corriendo en producción; una falla es un rollback, un hotfix dentro de la ventana o una quema de error budget atribuible. Recontá el último trimestre con esa regla antes de publicar ningún número.
- Escribí señal, umbral y ventana en YAML, sin darle autoridad al controller. Corré el análisis en modo observación durante varias releases y compará su veredicto contra lo que decidió el equipo. Escribir el umbral obliga a fijar el número cuando nadie está apurado, que es la única condición en la que se elige bien.
- Recién ahí automatizá el abort. Automatizar antes de validar los umbrales produce rollbacks espurios, y cada rollback espurio le enseña al equipo a desconfiar del mecanismo. Ajustá
failureLimitcon la dispersión que hayas medido en el paso anterior.
Ninguna de esas piezas es cara. La que más cuesta es la primera: aceptar que el pipeline nunca prometió lo que le estábamos leyendo.
Keep going
Reading
- DORA's software delivery performance metrics — La fuente canónica. Define cada métrica y explicita por qué el conteo de deploys y el change fail rate son los dos puntos donde se cuela el falseo.
- DORA Research: 2025 — El reporte anual con la muestra y los benchmarks del año. Para citar percentiles reales en vez de afirmar que los equipos buenos despliegan seguido.
- Capability: Monitoring and observability — DORA trata la observabilidad como capacidad predictiva del rendimiento de entrega. Es el puente argumental entre el pipeline en verde y el sistema sano.
- Canarying Releases (The Site Reliability Workbook, cap. 16) — Tamaño de la población canaria, duración de la ventana de evaluación y significancia estadística. El capítulo que explica bajo qué volumen un canary deja de decidir.
- Semantic Conventions: CI/CD metrics — El estándar abierto de atributos de pipeline, run y deployment environment. Emitir el evento de deploy con esta convención evita el join manual por timestamp.
- Using the Four Keys to measure your DevOps performance — De qué eventos concretos se derivan las métricas. Deja a la vista que la definición de deploy y la de fallo son decisiones de instrumentación.
Videos
- State of the Art of DORA Metrics & AI Integration • Nathen Harvey & Charles Humble • GOTO 2025 — Harvey lidera el programa DORA: explicación de primera mano de por qué se cambiaron las definiciones y cómo se malinterpretan las métricas.
- Measuring Software Delivery With DORA Metrics — Farley va al mecanismo por el que una métrica convertida en objetivo se corrompe, que es exactamente cómo un equipo llega a tener los números lindos con el sistema roto.
- DORA Metrics in Practice: Make them Observable through your Platform — Cómo derivar las métricas desde la telemetría de la plataforma en lugar del CI, y cómo correlacionar el evento de deploy con las métricas del sistema.