SRE: Ingeniería de Releases y Entrega Progresiva

2026-03-21 | Gabriel Garrido | 12 min de lectura
Share:

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:


  1. 5% del tráfico va a la versión nueva, después pausa de 2 minutos
  2. El análisis corre chequeando la tasa de error contra nuestro SLO
  3. 20% del tráfico si el análisis pasó, subimos y pausamos 3 minutos
  4. 50% del tráfico por 5 minutos
  5. 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:


  1. Se crean pods nuevos al lado de los existentes
  2. El service preview apunta a los pods nuevos para testear
  3. Análisis de pre-promoción corre smoke tests contra preview
  4. Promoción manual requerida ya que autoPromotionEnabled es false
  5. El tráfico cambia todo de una de blue a green
  6. 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:


  1. El developer pushea código al repo de la aplicación
  2. CI buildea y testea, pushea una imagen de contenedor
  3. El image updater detecta la nueva imagen y actualiza staging
  4. Los tests de staging pasan incluyendo análisis canary
  5. Se crea un PR para promover a producción
  6. El equipo revisa y mergea el PR
  7. ArgoCD sincroniza con la estrategia de Argo Rollout
  8. El análisis canary valida contra los SLOs
  9. 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: 0

Por favor inicie sesión para poder escribir comentarios.

2026-03-21 | Gabriel Garrido