From n00b to ZeroCool / Profesionalización

CI/CD: que el deploy no dependa de rezar

Pipeline CI/CD con GitHub Actions + Docker: tests, build, deploy con approvals, manejo de secrets y rollback. Menos fe, más trazabilidad.

Lo que vale la pena leer aquí

Llegas al release de las 6:00 pm, el Slack ya está hirviendo y alguien suelta el clásico: “¿Quién se avienta el deploy?”

Llegas al release de las 6:00 pm, el Slack ya está hirviendo y alguien suelta el clásico: “¿Quién se avienta el deploy?”

Ahí vas tú con la laptop ya medio guerrera, ventilador sonando como turbina. Te conectas a production con ese miedo que solo te deja el deploy que tiró todo la semana pasada. Corres un script que vive en un folder llamado scripts_viejos_ok_final2 y, cuando ves que falta una variable de entorno, haces lo que muchos: rezar tantito.

La neta: si tu deploy depende de rezar, no tienes proceso. Tienes fe.

Qué te vas a llevar

  • Qué sí necesita un pipeline CI/CD “de jale real” (no demo de conferencia).
  • Cómo separar CI (calidad) de CD (entrega) para que el workflow no sea un Frankenstein.
  • Ejemplo funcional con GitHub Actions + Docker, y cómo aterrizarlo en Kubernetes.
  • Controles que te salvan la vida: secretos, approvals, ambientes y rollback.
  • Los errores típicos que vuelven tu pipeline más frágil que el Wi‑Fi del cafecito.

Por qué esto importa cuando hay deadline

En México y LatAm pasa más de lo que aceptamos:

  • Staging “existe” pero es un namespace que nadie cuida, o de plano no existe.
  • El deploy lo hace “la persona que sabe” y si se va de vacaciones, el release se congela.
  • Tu Internet se muere justo cuando estás subiendo el build.
  • El jefe pide “un hotfix en corto” y terminas empujando directo a main con sudor frío.

CI/CD no es automatizar por deporte. Es bajar la incertidumbre:

  • Saber que lo que se construyó es lo que se desplegó.
  • Tener evidencia (logs, checks) para no discutir el “en mi máquina sí corrió”.
  • Poder revertir sin improvisar.

Decisión con filo: si tu sistema no aguanta un deploy repetible, tu “velocidad” es falsa. Solo estás acumulando deuda, con intereses y recargos.

Guía: un pipeline que no se rompe con el primer cambio

Caso común: app web (Node/Java/Python da igual) empaquetada en Docker y desplegada a algún lado.

1) Define el flujo (Git) antes de tocar YAML

No necesitas volverte talibán de GitFlow, pero sí reglas claras:

  • main: lo que está listo para producción.
  • PRs obligatorias hacia main.
  • Tags para releases (v1.8.3) o al menos un SHA inmutable.

Tradeoff real:

  • Si permites pushes directos a main, vas a tener deploys sorpresa.
  • Si el proceso es demasiado pesado (10 approvals), la banda lo va a brincar con hacks.

Setup recomendado:

  • Protege main: PR + status checks (CI) obligatorios.
  • Una aprobación mínima (dos si ya te ha dolido production).

2) Separa CI (calidad) de CD (entrega)

Piensa CI como: “¿Esto compila, pasa tests y produce artefacto?”

CD es: “¿Lo desplegamos con control, auditado y reversible?”

División que sí funciona en equipos chicos y medianos:

  • CI en cada PR: lint + tests + build.
  • CI al merge a main: build + push de imagen (artefacto) + versionado.
  • CD: despliegue por ambiente (dev/staging/prod), con approvals en prod.

3) Empaqueta de forma reproducible (Docker bien hecho)

Un Dockerfile pensado para producción te evita builds mágicos. Ejemplo (Node):

# Dockerfile
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM node:20-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

Decisión práctica: ¿tu imagen debe incluir tests? Normalmente no. Los tests van en CI, no en runtime.

4) CI con GitHub Actions (PR: tests + build)

Workflow mínimo y útil para PRs:

# .github/workflows/ci.yml
name: ci
on:
  pull_request:
    branches: ["main"]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"
      - run: npm ci
      - run: npm test
      - run: npm run build

Fricción real: si tus tests tardan 25 minutos, la gente los va a odiar y va a buscar cómo “saltárselos”. Si te está pasando:

  • paraleliza tests,
  • mete caching,
  • baja dependencias,
  • separa smoke tests vs full suite (full suite nocturna o por label).

5) Build y push de imagen (merge a main)

Aquí generas el artefacto que vas a desplegar: imagen versionada.

Ejemplo con GitHub Container Registry (GHCR):

# .github/workflows/build-and-push.yml
name: build-and-push
on:
  push:
    branches: ["main"]

permissions:
  contents: read
  packages: write

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/metadata-action@v5
        id: meta
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=sha

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Decisión que cambia el juego:

  • Deploy por latest = “quién sabe qué entró”.
  • Deploy por SHA/tag inmutable = control, trazabilidad y menos pleitos.

6) CD: despliega por ambiente y con aprobación (sin improvisar)

Si vas a prod con cada merge, pon mínimo un freno humano: GitHub Environments con required reviewers.

Ejemplo: deploy a Kubernetes con kubectl (simple, común y jalador). Supón que tienes:

  • KUBECONFIG como secret (o mejor OIDC con tu cloud provider, pero paso a paso),
  • manifests o Helm.
# .github/workflows/deploy-prod.yml
name: deploy-prod
on:
  workflow_dispatch:
    inputs:
      image:
        description: "Image tag (sha)"
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Set kubeconfig
        run: |
          mkdir -p ~/.kube
          echo "${KUBECONFIG_B64}" | base64 -d > ~/.kube/config
        env:
          KUBECONFIG_B64: ${{ secrets.KUBECONFIG_B64 }}

      - name: Update image
        run: |
          kubectl -n app set image deploy/myapp myapp=ghcr.io/${{ github.repository }}:${{ inputs.image }}
          kubectl -n app rollout status deploy/myapp

Con esto ya no estás “a ver si jala”:

  • el deploy es repetible,
  • sabes exactamente qué imagen entra,
  • tienes rollout status y evidencia.
CI/CD: que el deploy no dependa de rezar - visual explicativa 1
Visual de apoyo: Qué te vas a llevar

7) Rollback: práctica, no teoría

Rollback no es una slide bonita. Es el comando que necesitas cuando production arde y el cliente ya está marcando.

Para Kubernetes:

kubectl -n app rollout undo deploy/myapp
kubectl -n app rollout status deploy/myapp

Tradeoff:

  • rollout undo es rápido, pero depende de que el deployment conserve historial.
  • Si rompiste compatibilidad con config/DB, el rollback de app puede no salvarte.

Regla de oro que pega en la vida real:

  • Backward compatible primero, feature flags si aplica, migraciones en dos pasos.

8) Si no tienes Kubernetes (todavía): CD a VM con systemd

Porque sí: a veces el “cloud-native journey” empieza con una VM en DigitalOcean, Nginx y una app que solo debe funcionar.

La idea no cambia: despliegas un artefacto versionado y lo aplicas con un script controlado.

  • Copias un .tar.gz o jalas una imagen.
  • Reinicias el servicio.
  • Validación por healthcheck.

Ejemplo básico (SSH + docker run). Ojo: no es lo más seguro del mundo, pero es mejor que hacerlo a mano desde tu compu a las 11 pm:

name: deploy-vm
on:
  workflow_dispatch:
    inputs:
      image:
        required: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: SSH deploy
        uses: appleboy/ssh-action@v1.2.0
        with:
          host: ${{ secrets.VM_HOST }}
          username: ${{ secrets.VM_USER }}
          key: ${{ secrets.VM_SSH_KEY }}
          script: |
            docker login ghcr.io -u ${{ secrets.GHCR_USER }} -p ${{ secrets.GHCR_TOKEN }}
            docker pull ghcr.io/${{ github.repository }}:${{ inputs.image }}
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 80:3000 ghcr.io/${{ github.repository }}:${{ inputs.image }}

Consecuencia real: si no controlas secretos y permisos, alguien termina pegando llaves en un chat. Y luego “misteriosamente” aparece minería de cripto en tu VM.

Screenshots sugeridos

  • GitHub Actions con el pipeline pasando (CI) y el job de deploy esperando aprobación (environment).
  • Settings → Environments → production con Required reviewers configurado.
  • Packages/Container registry mostrando imágenes por SHA.
  • kubectl rollout status y kubectl rollout history en terminal.
  • Un PR con checks obligatorios y branch protection activado.

Errores comunes (y cómo salir del hoyo)

1) Deploy con latest

Síntoma: “según yo desplegué lo mismo, pero no es igual”.

Arreglo: usa tags inmutables (SHA) y registra el tag desplegado en el release/issue.

2) Secrets en el repo o en el YAML

Síntoma: alguien subió .env “por error” y ahora hay rotación de credenciales a las 2 am.

Arreglo: GitHub Secrets / Vault / Secret Manager. Y escaneo (Gitleaks, GitHub secret scanning).

3) Un solo pipeline para todo, imposible de entender

Síntoma: tocar una línea rompe staging y prod; nadie sabe por qué.

Arreglo: separa workflows por intención: ci.yml, build-and-push.yml, deploy-*.yml.

4) Tests lentos y flaky

Síntoma: re-runs hasta que “pegue”.

Arreglo: encuentra la flakiness, fija dependencias, usa retries solo en tests de red y mete mocks donde toca.

5) No hay healthchecks reales

Síntoma: el deploy “pasó”, pero el sitio está muerto.

Arreglo: agrega readiness/liveness (K8s) o un endpoint /health validado desde el pipeline.

CI/CD: que el deploy no dependa de rezar - visual explicativa 2
Visual de apoyo: Por qué esto importa cuando hay deadline

Checklist final (para dejar de rezar)

  • main protegido: PR obligatorio + checks de CI.
  • CI corre lint/tests/build en cada PR.
  • Build produce artefacto inmutable (imagen por SHA/tag).
  • CD separado por ambientes (staging/prod).
  • Prod requiere aprobación (Environment reviewers).
  • Secrets fuera del repo, rotación definida.
  • Deploy valida salud (rollout status, healthcheck real).
  • Rollback probado (sí: probado) y documentado.
  • Changelog mínimo: qué versión se desplegó, cuándo y por quién.

FAQ

1) ¿CI/CD es solo para microservicios y Kubernetes?

No. Si tienes un monolito en una VM, también aplica. La magia es la repetibilidad: build inmutable + deploy controlado.

2) ¿Qué va primero: CI o CD?

CI primero. Si no confías en tu build/tests, automatizar el deploy solo acelera el caos.

3) ¿Cómo manejo variables por ambiente sin volverlo un infierno?

Separa configuración de código. Usa Secrets/ConfigMaps (K8s) o Secret Manager. Versiona la forma de configurar, no los valores.

4) ¿Qué tan “cloud-native” es usar GitHub Actions y kubectl directo?

Es bastante común y válido. Más adelante puedes moverte a Helm, Argo CD o Flux para GitOps. Primero gana estabilidad.

5) ¿Cuándo necesito GitOps (Argo/Flux) en vez de pipelines que hacen kubectl?

Cuando:

  • tienes varios servicios/equipos,
  • necesitas auditoría fuerte del estado deseado,
  • quieres reconciliación automática,
  • y ya te duele mantener scripts de deploy.

Si tu equipo es chico y el producto está en modo “moverse rápido”, kubectl con disciplina puede ser suficiente.

Siguiente episodio

Ya no dependes de rezar para desplegar. El siguiente golpe de realidad es ver qué pasa cuando algo falla.

Lo que sigue: observabilidad cloud-native sin ahogarte en dashboards.