SRE: Ingeniería de Releases y Entrega Progresiva
Apoya este blog
Si te resulta util este contenido, considera apoyar el blog.
Introducción
A lo largo de esta serie de SRE cubrimos un montón de terreno: SLIs y SLOs, gestión de incidentes, observabilidad, chaos engineering, planificación de capacidad, GitOps, gestión de secretos, optimización de costos, gestión de dependencias, y confiabilidad de bases de datos. Tenemos SLOs, alertas, runbooks, pipelines de observabilidad, experimentos de caos y workflows de GitOps. Pero nada de eso importa si tus deployments siguen causando incidentes.
Los deployments son la causa número uno de incidentes en la mayoría de las organizaciones. Cada vez que empujás código nuevo a producción, estás introduciendo un cambio, y los cambios son donde viven las fallas. La ingeniería de releases es la disciplina de hacer que los deployments sean seguros, predecibles y aburridos. La entrega progresiva va un paso más allá, desplegando cambios gradualmente a subconjuntos pequeños de usuarios, validando en cada paso, y haciendo rollback automáticamente cuando algo sale mal.
En este artículo vamos a cubrir canary deployments con Argo Rollouts, deployments blue-green, feature flags en Elixir, rollback automático, SLOs de deployment, hooks de sync en ArgoCD, releases basados en GitOps, y políticas de cadencia de releases.
Vamos al tema.
Canary deployments con Argo Rollouts
Un canary deployment manda un porcentaje pequeño de tráfico a la versión nueva primero. Si el canario se mantiene saludable, vas aumentando el tráfico gradualmente. Si se enferma, lo sacás antes de que alguien más se vea afectado.
Argo Rollouts es un controlador de Kubernetes que reemplaza el Deployment estándar con un CRD Rollout que te da control detallado sobre el proceso de despliegue. Instalalo primero:
# Instalar Argo Rollouts
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# Instalar el plugin de kubectl
brew install argoproj/tap/kubectl-argo-rollouts
Ahora definamos un Rollout canary para nuestra aplicación Elixir:
# rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: tr-web
spec:
replicas: 4
selector:
matchLabels:
app: tr-web
template:
metadata:
labels:
app: tr-web
spec:
containers:
- name: tr-web
image: kainlite/tr:v1.2.0
ports:
- containerPort: 4000
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "1000m"
memory: "512Mi"
readinessProbe:
httpGet:
path: /healthz
port: 4000
initialDelaySeconds: 10
strategy:
canary:
canaryService: tr-web-canary
stableService: tr-web-stable
trafficRouting:
nginx:
stableIngress: tr-web-ingress
steps:
- setWeight: 5
- pause: { duration: 2m }
- analysis:
templates:
- templateName: canary-success-rate
args:
- name: service-name
value: tr-web-canary
- setWeight: 20
- pause: { duration: 3m }
- analysis:
templates:
- templateName: canary-success-rate
- setWeight: 50
- pause: { duration: 5m }
- setWeight: 100
La sección steps define el proceso de rollout:
- 5% del tráfico va a la versión nueva, después pausa de 2 minutos
- El análisis corre chequeando la tasa de error contra nuestro SLO
- 20% del tráfico si el análisis pasó, subimos y pausamos 3 minutos
- 50% del tráfico por 5 minutos
- 100% del tráfico promoción completa si todo se ve bien
También necesitás los services stable y canary:
# services.yaml
apiVersion: v1
kind: Service
metadata:
name: tr-web-stable
spec:
selector:
app: tr-web
ports:
- port: 80
targetPort: 4000
---
apiVersion: v1
kind: Service
metadata:
name: tr-web-canary
spec:
selector:
app: tr-web
ports:
- port: 80
targetPort: 4000
Para manejar rollouts usá el plugin de kubectl:
# Ver el rollout
kubectl argo rollouts get rollout tr-web --watch
# Promover manualmente un rollout pausado
kubectl argo rollouts promote tr-web
# Abortar y volver a la versión estable
kubectl argo rollouts abort tr-web
Deployments blue-green
Blue-green corre dos ambientes completos en paralelo. “Blue” es la versión actual, “green” es la nueva. Desplegás green, la testeás, y cambiás todo el tráfico de una. Si algo se rompe, volvés a blue.
El trade-off contra canary es simplicidad (sin cambio gradual) pero necesitás el doble de recursos durante el deployment y todos los usuarios se mueven de golpe. Acá va un Rollout blue-green:
# blue-green-rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: tr-web-bluegreen
spec:
replicas: 4
selector:
matchLabels:
app: tr-web
template:
metadata:
labels:
app: tr-web
spec:
containers:
- name: tr-web
image: kainlite/tr:v1.2.0
ports:
- containerPort: 4000
readinessProbe:
httpGet:
path: /healthz
port: 4000
strategy:
blueGreen:
activeService: tr-web-active
previewService: tr-web-preview
autoPromotionEnabled: false
scaleDownDelaySeconds: 30
prePromotionAnalysis:
templates:
- templateName: bluegreen-smoke-test
args:
- name: service-name
value: tr-web-preview
postPromotionAnalysis:
templates:
- templateName: canary-success-rate
args:
- name: service-name
value: tr-web-active
Cuando actualizás el tag de la imagen:
- Se crean pods nuevos al lado de los existentes
- El service preview apunta a los pods nuevos para testear
- Análisis de pre-promoción corre smoke tests contra preview
- Promoción manual requerida ya que
autoPromotionEnabledes false- El tráfico cambia todo de una de blue a green
- Los pods viejos escalan a cero después de
scaleDownDelaySeconds
Feature flags
Los feature flags te permiten desacoplar el deployment del release. Deployás el código pero la funcionalidad está oculta detrás de un flag que podés activar en runtime sin un nuevo deployment.
Acá va un sistema de feature flags simple en Elixir usando ETS:
# lib/tr/feature_flags.ex
defmodule Tr.FeatureFlags do
use GenServer
@table :feature_flags
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
def enabled?(feature) when is_atom(feature) do
case :ets.lookup(@table, feature) do
[{^feature, %{enabled: true, percentage: 100}}] -> true
[{^feature, %{enabled: true, percentage: pct}}] -> :rand.uniform(100) <= pct
_ -> false
end
end
def enabled?(feature, user_id) when is_atom(feature) do
case :ets.lookup(@table, feature) do
[{^feature, %{enabled: true, percentage: 100}}] -> true
[{^feature, %{enabled: true, percentage: pct}}] ->
hash = :erlang.phash2({feature, user_id}, 100)
hash < pct
_ -> false
end
end
def enable(feature, percentage \\ 100) when is_atom(feature) do
GenServer.call(__MODULE__, {:enable, feature, percentage})
end
def disable(feature) when is_atom(feature) do
GenServer.call(__MODULE__, {:disable, feature})
end
@impl true
def init(_opts) do
table = :ets.new(@table, [:named_table, :set, :public, read_concurrency: true])
load_defaults()
{:ok, %{table: table}}
end
@impl true
def handle_call({:enable, feature, percentage}, _from, state) do
:ets.insert(@table, {feature, %{enabled: true, percentage: percentage}})
{:reply, :ok, state}
end
@impl true
def handle_call({:disable, feature}, _from, state) do
:ets.insert(@table, {feature, %{enabled: false, percentage: 0}})
{:reply, :ok, state}
end
defp load_defaults do
defaults = Application.get_env(:tr, :feature_flags, [])
Enum.each(defaults, fn {name, config} ->
:ets.insert(@table, {name, config})
end)
end
end
Configurá los defaults y usálo en tus vistas:
# config/config.exs
config :tr, :feature_flags, [
new_search_ui: %{enabled: false, percentage: 0},
dark_mode: %{enabled: true, percentage: 100},
experimental_editor: %{enabled: true, percentage: 10}
]
# En un LiveView
def render(assigns) do
~H"""
<%= if Tr.FeatureFlags.enabled?(:new_search_ui) do %>
<.new_search_component />
<% else %>
<.legacy_search_component />
<% end %>
"""
end
La variante enabled?/2 usa hashing consistente para que el usuario 42 siempre obtenga el mismo resultado
a cualquier porcentaje. Podés hacer rollout progresivo:
Tr.FeatureFlags.enable(:new_search_ui, 25) # 25% de usuarios
Tr.FeatureFlags.enable(:new_search_ui, 50) # 50% de usuarios
Tr.FeatureFlags.enable(:new_search_ui, 100) # todos
Tr.FeatureFlags.disable(:new_search_ui) # kill switch
Automatización de rollbacks
La forma más rápida de recuperarte de un deployment malo es hacer rollback. Con la automatización correcta, esto puede pasar en menos de un minuto sin intervención humana.
Con Argo Rollouts, el rollback es automático cuando el análisis falla. El rollout se aborta y el tráfico vuelve a la versión estable. Para deployments con ArgoCD, podés automatizar el rollback en tu pipeline de CI:
# .github/workflows/deploy.yaml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy a producción
run: |
kubectl set image deployment/tr-web \
tr-web=kainlite/tr:${{ github.sha }}
- name: Esperar rollout
id: rollout
continue-on-error: true
run: kubectl rollout status deployment/tr-web --timeout=180s
- name: Correr smoke tests
id: smoke
if: steps.rollout.outcome == 'success'
continue-on-error: true
run: |
for i in $(seq 1 5); do
STATUS=$(curl -s -o /dev/null -w '%{http_code}' \
https://segfault.pw/healthz)
if [ "$STATUS" != "200" ]; then exit 1; fi
sleep 2
done
- name: Rollback si falla
if: steps.rollout.outcome == 'failure' || steps.smoke.outcome == 'failure'
run: |
echo "Deployment falló, haciendo rollback..."
kubectl rollout undo deployment/tr-web
exit 1
También podés usar kubectl directamente para rollbacks rápidos:
# Rollback nativo de Kubernetes
kubectl rollout undo deployment/tr-web
# Rollback de ArgoCD a revisión anterior
argocd app history tr-web
argocd app rollback tr-web <revision-anterior>
El principio clave es que los rollbacks deben ser automáticos, rápidos, y no requerir decisión humana.
SLOs de deployment
En el artículo de SLIs y SLOs definimos SLOs para nuestros servicios. Ahora usamos esos mismos SLOs como gates de deployment. Si un canary viola el SLO, el deployment se detiene.
Argo Rollouts usa AnalysisTemplates para consultar Prometheus y decidir si un deployment está saludable:
# analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: canary-success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 30s
count: 5
successCondition: result[0] >= 0.99
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring.svc:9090
query: |
sum(rate(
http_requests_total{service="{{args.service-name}}", status!~"5.."}[2m]
)) /
sum(rate(
http_requests_total{service="{{args.service-name}}"}[2m]
))
Este template:
- Consulta Prometheus cada 30 segundos por la tasa de éxito
- Corre 5 mediciones para tener suficientes datos
- Requiere 99% de tasa de éxito coincidiendo con nuestro SLO
- Permite 2 fallas antes de marcar el análisis como fallido
También podés gatear deployments por error budget. Si queda menos del 20% de tu error budget de 30 días, bloqueá el deployment:
# analysis-error-budget.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: error-budget-gate
spec:
metrics:
- name: error-budget-remaining
interval: 1m
count: 1
successCondition: result[0] > 0.2
provider:
prometheus:
address: http://prometheus.monitoring.svc:9090
query: |
1 - (
(1 - (
sum(rate(http_requests_total{service="tr-web", status!~"5.."}[30d])) /
sum(rate(http_requests_total{service="tr-web"}[30d]))
)) / (1 - 0.999)
)
Combiná múltiples análisis en los pasos de tu rollout para validación integral:
steps:
- setWeight: 10
- pause: { duration: 2m }
- analysis:
templates:
- templateName: canary-success-rate
- templateName: canary-latency
args:
- name: service-name
value: tr-web-canary
Hooks de pre y post sync
ArgoCD soporta hooks de recursos que corren en puntos específicos durante el sync. Son perfectos para migraciones de base de datos antes del deployment, smoke tests después, y notificaciones en varias etapas.
Hook de pre-sync para migraciones de base de datos:
# migration-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tr-web-migrate
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: migrate
image: kainlite/tr:v1.2.0
command: ["/app/bin/tr"]
args: ["eval", "Tr.Release.migrate()"]
envFrom:
- secretRef:
name: tr-web-env
restartPolicy: Never
backoffLimit: 3
Hook de post-sync para smoke tests:
# smoke-test-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tr-web-smoke-test
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: smoke-test
image: curlimages/curl:latest
command: ["/bin/sh", "-c"]
args:
- |
STATUS=$(curl -s -o /dev/null -w '%{http_code}' http://tr-web-stable/healthz)
if [ "$STATUS" != "200" ]; then exit 1; fi
echo "¡Smoke tests pasaron!"
restartPolicy: Never
backoffLimit: 1
Hook de notificación por fallas:
# notification-hook.yaml
apiVersion: batch/v1
kind: Job
metadata:
name: tr-web-notify
annotations:
argocd.argoproj.io/hook: SyncFail
argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
template:
spec:
containers:
- name: notify
image: curlimages/curl:latest
command: ["/bin/sh", "-c"]
args:
- |
curl -X POST "${SLACK_WEBHOOK_URL}" \
-H 'Content-Type: application/json' \
-d '{"text": "¡Sync FALLÓ para tr-web en producción!"}'
envFrom:
- secretRef:
name: slack-webhook
restartPolicy: Never
Los tipos de hooks disponibles son:
- PreSync corre antes del sync (migraciones, backups)
- Sync corre durante el sync junto con los otros recursos
- PostSync corre después de que todos los recursos están sincronizados y saludables
- SyncFail corre cuando el sync falla (notificaciones de alerta)
Releases manejados por GitOps
Con GitOps, cada deployment es un commit de git. Esto te da un audit trail completo y la capacidad de usar git revert como mecanismo de rollback.
El ArgoCD Image Updater detecta nuevas imágenes de contenedor y actualiza el repositorio de git automáticamente:
# Annotations del image updater en la Application de ArgoCD
metadata:
annotations:
argocd-image-updater.argoproj.io/image-list: tr=kainlite/tr
argocd-image-updater.argoproj.io/tr.update-strategy: semver
argocd-image-updater.argoproj.io/tr.allow-tags: regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
argocd-image-updater.argoproj.io/write-back-method: git
argocd-image-updater.argoproj.io/git-branch: main
Para un flujo basado en PRs con review antes de producción, usá un GitHub Action que cree un PR de promoción:
# .github/workflows/promote.yaml
name: Promover a Producción
on:
workflow_run:
workflows: ["Build and Push"]
types: [completed]
branches: [main]
jobs:
promote:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: actions/checkout@v4
with:
repository: kainlite/tr-infra
token: ${{ secrets.INFRA_REPO_TOKEN }}
- name: Actualizar tag de imagen
run: |
cd k8s/overlays/production
kustomize edit set image \
kainlite/tr=kainlite/tr:${{ github.event.workflow_run.head_sha }}
- name: Crear PR
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: bump tr-web a ${{ github.event.workflow_run.head_sha }}"
title: "Deploy tr-web ${{ github.event.workflow_run.head_sha }}"
branch: deploy/tr-web-${{ github.event.workflow_run.head_sha }}
base: main
Usá overlays de Kustomize para promoción entre ambientes:
# k8s/overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: kainlite/tr
newTag: abc123-staging
namespace: staging
# k8s/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: kainlite/tr
newTag: v1.2.0
namespace: default
El workflow completo:
- El developer pushea código al repo de la aplicación
- CI buildea y testea, pushea una imagen de contenedor
- El image updater detecta la nueva imagen y actualiza staging
- Los tests de staging pasan incluyendo análisis canary
- Se crea un PR para promover a producción
- El equipo revisa y mergea el PR
- ArgoCD sincroniza con la estrategia de Argo Rollout
- El análisis canary valida contra los SLOs
- El rollout completo se completa si todo está saludable
Cada paso es rastreable a través de git. Si algo sale mal, hacés git revert del PR de promoción y ArgoCD
hace rollback.
Cadencia de releases y freezes
Las herramientas copadas son importantes, pero también necesitás políticas sobre cuándo deployar. ArgoCD soporta sync windows:
# argocd-project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
syncWindows:
# Permitir syncs lunes a jueves, 9am a 4pm UTC
- kind: allow
schedule: "0 9 * * 1-4"
duration: 7h
applications: ["*"]
# Nada de deploys viernes a la tarde
- kind: deny
schedule: "0 14 * * 5"
duration: 10h
applications: ["*"]
# Freeze de fin de año (20 dic al 1 ene)
- kind: deny
schedule: "0 0 20 12 *"
duration: 288h
applications: ["*"]
# Siempre permitir syncs manuales para emergencias
- kind: allow
schedule: "* * * * *"
duration: 24h
applications: ["*"]
manualSync: true
Guías prácticas:
- Deployá seguido, deployá chico: cambios más pequeños son más fáciles de debuggear
- Nada de deploys viernes a la tarde: a menos que disfrutes los pages de fin de semana
- Freezes de feriados: planeálos con anticipación, comunicalos claramente
- Excepciones de emergencia: siempre tené un proceso para hotfixes críticos
- Ventanas de deploy: deployá solo cuando haya alguien para mirar
También podés forzar esto en CI:
# check-deploy-window.sh
#!/bin/bash
set -euo pipefail
HOUR=$(date -u +%H)
DAY=$(date -u +%u) # 1=Lunes, 7=Domingo
if [ "$DAY" -ge 6 ]; then
echo "Deploy bloqueado: no se deploya en fin de semana"; exit 1
fi
if [ "$DAY" -eq 5 ] && [ "$HOUR" -ge 14 ]; then
echo "Deploy bloqueado: no se deploya viernes a la tarde"; exit 1
fi
if [ "$HOUR" -lt 9 ] || [ "$HOUR" -ge 16 ]; then
echo "Deploy bloqueado: fuera de ventana (09:00-16:00 UTC)"; exit 1
fi
echo "Ventana de deploy abierta, continuando..."
El balance es entre seguridad y velocidad. Demasiadas restricciones y tu equipo deja de deployar, lo que en realidad hace los deployments más riesgosos porque cada uno contiene más cambios.
Notas finales
La ingeniería de releases se trata de hacer que los deployments sean aburridos. Cuando tenés canary deployments que validan contra tus SLOs, estrategias blue-green con rollback instantáneo, feature flags para desacoplar deployment de release, y pipelines de GitOps con audit trail completo, los deployments se convierten en operaciones rutinarias en vez de eventos que dan miedo.
Empezá con una pieza, tal vez canary deployments con un análisis simple de tasa de error, y construí desde ahí. El objetivo no es cero deployments, es cero incidentes causados por deployments. Shippeá rápido, shippeá seguro, y dejá que la automatización atrape los problemas antes de que tus usuarios lo hagan.
¡Espero que te haya resultado útil y lo hayas disfrutado! ¡Hasta la próxima!
Errata
Si encontrás algún error o tenés alguna sugerencia, por favor mandame un mensaje para que se corrija.
También podés revisar el código fuente y los cambios en las fuentes acá
$ Comentarios
Online: 0Por favor inicie sesión para poder escribir comentarios.