CI/CD y DevOps

"Si duele hacerlo frecuentemente, hacelo más frecuentemente — para que el dolor te fuerce a arreglarlo." — Jez Humble (sobre deploys frecuentes).

Qué vas a aprender en este capítulo

CI/CD (Continuous Integration / Continuous Delivery) y DevOps son las prácticas que permiten a los equipos modernos desplegar código decenas de veces al día con alta confianza. Este capítulo cubre el flujo de trabajo profesional con Git, la configuración de pipelines automáticos, y los fundamentos de Docker para entornos reproducibles.


5.1 Flujo de trabajo con Git

💡 Intuición

Git es la herramienta de control de versiones más usada en el mundo. Pero usarlo bien en equipo requiere un flujo de trabajo (workflow) acordado. Sin un flujo definido, el repositorio se convierte en caos.

Git Flow (el más documentado) y Trunk-Based Development (el que usan Google y Netflix) son los dos enfoques principales.

En equipos pequeños como el de La Esquina (2-4 desarrolladores), un flujo simplificado funciona perfecto:

  • main: siempre deployable.
  • feature/xxx: ramas de trabajo.
  • Pull Requests para mergear con revisión.

📐 Fundamento

Ramas (branches):

main ─────────────────────────────────────────── ▶ producción
        │                              │
        └── feature/login ─ PR ──────── merge
               │
               └── commit1 ─ commit2 ─ commit3

Comandos de flujo diario:

# 1. Actualizar main antes de empezar
git checkout main
git pull origin main

# 2. Crear rama de feature
git checkout -b feature/agregar-pago-tarjeta

# 3. Trabajar (commits frecuentes, mensajes claros)
git add src/pagos.py tests/test_pagos.py
git commit -m "feat: agregar soporte para pago con tarjeta de crédito"

# 4. Subir la rama
git push origin feature/agregar-pago-tarjeta

# 5. Abrir Pull Request en GitHub
# 6. Peer review + CI automático
# 7. Merge a main
# 8. Borrar la rama
git branch -d feature/agregar-pago-tarjeta

Convención de mensajes de commit (Conventional Commits):

tipo(scope): descripción breve

Tipo Cuándo usarlo
feat Nueva funcionalidad
fix Corrección de bug
refactor Cambio de código sin cambiar comportamiento
test Agregar o mejorar pruebas
docs Documentación
chore Mantenimiento (deps, config)

Ejemplos:

  • feat(pedidos): implementar envío en tiempo real a cocina
  • fix(auth): corregir sesión que no expiraba en móviles
  • test(fiscal): agregar casos límite para cálculo de IVA

Pull Requests — buenas prácticas:

  • Una PR = una funcionalidad o fix. No mezclar.
  • PRs pequeñas (< 400 líneas de cambios). Fáciles de revisar.
  • Descripción clara: qué cambiaste y por qué.
  • Ligar a la historia de usuario correspondiente.
  • Al menos un revisor aprueba antes de mergear.

🛠️ En la práctica

Protección de ramas en GitHub:

En Settings → Branches → Add rule:

  • Branch name pattern: main
  • Require pull request reviews before merging: ✓ (1 review)
  • Require status checks to pass before merging: ✓ (CI debe pasar)
  • Restrict who can push to matching branches: ✓ (solo admins)

Esto garantiza que nadie puede romper main por accidente.


5.2 Continuous Integration (CI)

💡 Intuición

Integración continua significa que cada vez que un desarrollador hace push, el sistema automáticamente:

  1. Descarga el código.
  2. Instala dependencias.
  3. Corre todas las pruebas.
  4. Si algo falla, notifica al equipo.
  5. Si todo pasa, el código está "integrado" con éxito.

¿Por qué "continua"? Porque en el pasado se integraba todo al final del proyecto. Para entonces, los conflictos eran enormes. Con CI, cada push es una integración pequeña y los problemas se detectan en minutos, no meses.

El contrato del equipo con CI: Si el CI falla, es la prioridad #1 arreglarlo. El CI roto bloquea a todo el equipo.

📐 Fundamento

GitHub Actions — CI integrado en GitHub:

Se configura en archivos YAML en .github/workflows/.

Pipeline CI básico para La Esquina:

# .github/workflows/ci.yml
name: CI — Tests y Linting

on:
  push:
    branches: [ main, "feature/*" ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout código
        uses: actions/checkout@v4

      - name: Configurar Python 3.11
        uses: actions/setup-python@v4
        with:
          python-version: "3.11"

      - name: Instalar dependencias
        run: |
          pip install -r requirements.txt
          pip install pytest pytest-cov

      - name: Linting con flake8
        run: flake8 src/ tests/

      - name: Ejecutar pruebas con cobertura
        run: |
          pytest --cov=src --cov-fail-under=80

      - name: Publicar reporte de cobertura
        uses: codecov/codecov-action@v3

--cov-fail-under=80: Si la cobertura baja del 80%, el CI falla. Esto previene que se agregue código sin pruebas.

Pasos típicos de un pipeline CI:

  1. Checkout del código
  2. Setup del entorno (lenguaje, versión)
  3. Instalar dependencias (pip install, npm install)
  4. Lint (verificar estilo de código)
  5. Pruebas unitarias
  6. Pruebas de integración (si aplica)
  7. Análisis de cobertura
  8. (Opcional) Análisis de seguridad (bandit, snyk)
  9. (Opcional) Análisis de deuda técnica (SonarQube)

5.3 Continuous Delivery / Deployment (CD)

💡 Intuición

Continuous Delivery: El software siempre está en un estado listo para ser desplegado a producción. El deployment es una decisión humana.

Continuous Deployment: El deployment es automático después de que el CI pasa. Cada push a main va directamente a producción (con ciertas salvaguardas).

La diferencia:

  • Delivery: CI pasa → artefacto listo → humano decide cuándo deployar.
  • Deployment: CI pasa → deploy automático.

Para La Esquina, empezamos con Delivery (deploy manual después del CI) y podemos evolucionar a Deployment cuando haya más confianza en las pruebas.

📐 Fundamento

Pipeline CD básico:

# .github/workflows/deploy.yml (se ejecuta solo en main)
name: CD — Deploy a Producción

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    needs: test  # solo si el job 'test' del CI pasó
    runs-on: ubuntu-latest
    environment: production  # requiere aprobación manual en GitHub

    steps:
      - uses: actions/checkout@v4

      - name: Build imagen Docker
        run: docker build -t laesquina-backend:${{ github.sha }} .

      - name: Push a Registry
        run: |
          echo <span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>s</mi><mi>e</mi><mi>c</mi><mi>r</mi><mi>e</mi><mi>t</mi><mi>s</mi><mi mathvariant="normal">.</mi><mi>R</mi><mi>E</mi><mi>G</mi><mi>I</mi><mi>S</mi><mi>T</mi><mi>R</mi><msub><mi>Y</mi><mi>P</mi></msub><mi>A</mi><mi>S</mi><mi>S</mi><mi>W</mi><mi>O</mi><mi>R</mi><mi>D</mi></mrow><mi mathvariant="normal">∣</mi><mi>d</mi><mi>o</mi><mi>c</mi><mi>k</mi><mi>e</mi><mi>r</mi><mi>l</mi><mi>o</mi><mi>g</mi><mi>i</mi><mi>n</mi><mo>−</mo><mi>u</mi></mrow><annotation encoding="application/x-tex">{{ secrets.REGISTRY_PASSWORD }} | docker login -u </annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="base"><span class="strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.0278em;">secr</span><span class="mord mathnormal">e</span><span class="mord mathnormal">t</span><span class="mord mathnormal">s</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal" style="margin-right:0.0576em;">E</span><span class="mord mathnormal">G</span><span class="mord mathnormal" style="margin-right:0.0785em;">I</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.1389em;">T</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord"><span class="mord mathnormal" style="margin-right:0.2222em;">Y</span><span class="msupsub"><span class="vlist-t vlist-t2"><span class="vlist-r"><span class="vlist" style="height:0.3283em;"><span style="top:-2.55em;margin-left:-0.2222em;margin-right:0.05em;"><span class="pstrut" style="height:2.7em;"></span><span class="sizing reset-size6 size3 mtight"><span class="mord mathnormal mtight" style="margin-right:0.1389em;">P</span></span></span></span><span class="vlist-s">​</span></span><span class="vlist-r"><span class="vlist" style="height:0.15em;"><span></span></span></span></span></span></span><span class="mord mathnormal">A</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.0576em;">S</span><span class="mord mathnormal" style="margin-right:0.1389em;">W</span><span class="mord mathnormal" style="margin-right:0.0278em;">O</span><span class="mord mathnormal" style="margin-right:0.0077em;">R</span><span class="mord mathnormal" style="margin-right:0.0278em;">D</span></span></span><span class="mord">∣</span><span class="mord mathnormal">d</span><span class="mord mathnormal">oc</span><span class="mord mathnormal" style="margin-right:0.0315em;">k</span><span class="mord mathnormal" style="margin-right:0.0278em;">er</span><span class="mord mathnormal" style="margin-right:0.0197em;">l</span><span class="mord mathnormal">o</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">in</span><span class="mspace" style="margin-right:0.2222em;"></span><span class="mbin">−</span><span class="mspace" style="margin-right:0.2222em;"></span></span><span class="base"><span class="strut" style="height:0.4306em;"></span><span class="mord mathnormal">u</span></span></span></span>{{ secrets.REGISTRY_USER }} --password-stdin
          docker push laesquina-backend:${{ github.sha }}

      - name: Deploy al servidor
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_KEY }}
          script: |
            docker pull laesquina-backend:${{ github.sha }}
            docker stop laesquina-backend || true
            docker run -d --name laesquina-backend \
              -p 8000:8000 \
              --env-file /etc/laesquina/.env \
              laesquina-backend:${{ github.sha }}

Secrets en GitHub: Las credenciales (contraseñas, SSH keys) se guardan en Settings → Secrets, nunca en el código.

Estrategias de deploy:

Estrategia Descripción Downtime
Recreate Detener v1, iniciar v2
Rolling Reemplazar instancias gradualmente No
Blue-Green Dos entornos, swapping de tráfico No
Canary % del tráfico va a la nueva versión primero No

5.4 Docker para entornos reproducibles

💡 Intuición

"En mi máquina funciona" es la frase más famosa del desarrollo de software. El código funciona en el laptop del desarrollador, pero no en el servidor. O en el laptop de otro desarrollador. O en el entorno de pruebas.

Docker resuelve esto empaquetando el código junto con su entorno completo (sistema operativo base, dependencias, configuración) en una imagen. Esa imagen corre idéntica en cualquier máquina que tenga Docker instalado.

Una imagen Docker es como un USB con un sistema operativo portable.

📐 Fundamento

Conceptos clave:

  • Imagen: Plantilla inmutable que define el contenido del contenedor. Se construye desde un Dockerfile.
  • Contenedor: Instancia en ejecución de una imagen. Es efímero (se puede crear y destruir).
  • Registry: Repositorio de imágenes (Docker Hub, GitHub Container Registry).
  • Docker Compose: Herramienta para definir y correr múltiples contenedores juntos (ej: backend + BD + Redis).

Dockerfile para La Esquina (Python/FastAPI):

# Dockerfile
FROM python:3.11-slim

# Directorio de trabajo dentro del contenedor
WORKDIR /app

# Instalar dependencias primero (aprovecha el caché de Docker)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copiar el código
COPY src/ ./src/

# Variable de entorno para el modo de producción
ENV ENVIRONMENT=production

# Puerto que expone el contenedor
EXPOSE 8000

# Comando para iniciar la aplicación
CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]

docker-compose.yml para desarrollo:

# docker-compose.yml
version: "3.8"

services:
  backend:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=mysql://laesquina:password@db:3306/laesquina
    depends_on:
      - db
    volumes:
      - ./src:/app/src  # hot reload en desarrollo

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=rootpassword
      - MYSQL_DATABASE=laesquina
      - MYSQL_USER=laesquina
      - MYSQL_PASSWORD=password
    volumes:
      - mysql_data:/var/lib/mysql

volumes:
  mysql_data:

Comandos básicos:

# Construir imagen
docker build -t laesquina .

# Correr contenedor
docker run -p 8000:8000 laesquina

# Con Docker Compose (levanta backend + BD juntos)
docker compose up        # foreground
docker compose up -d     # background (daemon)
docker compose down      # parar y eliminar contenedores
docker compose logs -f   # ver logs en tiempo real

5.5 Cultura DevOps

💡 Intuición

DevOps es la fusión de Development y Operations. Históricamente, el equipo de desarrollo y el de operaciones (los que mantienen los servidores) estaban separados y a menudo en conflicto:

  • Dev quería desplegar cambios rápido.
  • Ops quería estabilidad — cada cambio era un riesgo.

DevOps rompe esa separación: el mismo equipo que desarrolla es responsable de operar. "You build it, you run it" (Werner Vogels, CTO de Amazon).

📐 Fundamento

Los tres caminos de DevOps (Gene Kim):

  1. Flow: Maximizar el flujo de trabajo de izquierda a derecha (Dev → Ops → Cliente). Eliminar cuellos de botella.
  2. Feedback: Crear loops de retroalimentación rápidos de derecha a izquierda (monitoreo → alerta → corrección).
  3. Continual Learning: Experimentación, aprendizaje de fallos, mejora continua.

Prácticas DevOps:

Práctica Herramientas comunes
Control de versiones Git, GitHub, GitLab
CI/CD GitHub Actions, Jenkins, GitLab CI
Containerización Docker, Kubernetes
Infrastructure as Code Terraform, Ansible, AWS CDK
Monitoreo Prometheus, Grafana, Datadog
Log management ELK Stack, Loki, Splunk
Alerting PagerDuty, OpsGenie, Slack

Métricas DORA (DevOps Research and Assessment):

Las cuatro métricas que predicen la performance de equipos de alto rendimiento:

Métrica Elite performers Low performers
Deployment frequency Múltiples deploys/día Mensual o menos
Lead time for changes < 1 hora 1-6 meses
Change failure rate 0–15% 46–60%
Time to restore service < 1 hora 1–6 meses

(Informe State of DevOps 2023 — Google Cloud / DORA)

🛠️ En la práctica

Pipeline completo de La Esquina:

Developer push → GitHub
        │
        ▼
GitHub Actions CI:
  1. Lint + type check
  2. Pruebas unitarias (100 tests, < 30s)
  3. Pruebas de integración (BD de prueba)
  4. Cobertura ≥ 80%
        │
  [¿Pasó todo?]
        │ Sí              │ No
        ▼                 ▼
   PR abierta        Notificación al dev
   → Code Review          └── Fix y push nuevamente
   → Aprobación
        │
        ▼
  Merge a main
        │
        ▼
GitHub Actions CD:
  1. Build imagen Docker
  2. Push a registry
  3. Deploy a staging (automático)
  4. Smoke tests en staging
  5. [Aprobación manual del PO]
  6. Deploy a producción
        │
        ▼
Monitoreo:
  - Health checks cada 30s
  - Alertas en Slack si error rate > 1%
  - Logs accesibles para debugging

¿Cuánto tiempo tarda? En un equipo bien configurado:

  • CI: 3-5 minutos
  • Deploy a staging: 2 minutos
  • Deploy a producción: 2 minutos

Total: un cambio puede estar en producción en < 15 minutos de haber sido mergeado.


5.6 Cierre de Ingeniería de Software

El sistema de La Esquina ahora tiene:

  1. Cultura y proceso: Scrum con sprints de 2 semanas, daily standups, retrospectivas.
  2. Calidad medible: Code coverage ≥80%, defect density monitoreada.
  3. Pruebas automáticas: Unitarias (TDD), integración, y smoke tests.
  4. Pipeline CI/CD: Cada push dispara pruebas; cada merge a main puede ir a producción.
  5. Entorno reproducible: Docker para desarrollo, staging y producción.

De "código que funciona en mi laptop" a "software profesional que se entrega continuamente con confianza". Eso es ingeniería de software.

5.7 Ejercicios

✏️ Ejercicio 5.1 — Git workflow

Describí paso a paso (comandos Git incluidos) el proceso completo para:

"El desarrollador Ernesto va a implementar la historia HU-014: notificaciones push cuando el pedido esté listo."

✏️ Ejercicio 5.2 — GitHub Actions

Escribí un workflow de GitHub Actions que:

  • Se active en push a cualquier rama y en PRs a main.
  • Configure Node.js 18.
  • Corra npm install.
  • Corra npm test.
  • Solo haga el deploy (a un servidor ficticio) cuando el push sea a main.

5.8 Para profundizar

5.X Mini-proyecto integrador

🏗️ Proyecto final — Pipeline CI/CD completo

Alcance: automatizar de cero el deploy de una app web a producción usando todo el libro: Scrum + tests + CI + CD + observabilidad.

Entregables (un repo público en GitHub):

  1. Backlog (cap. 2) — 5 historias en GitHub Projects con criterios INVEST.
  2. Tests (cap. 3-4) — pirámide: unit tests (>80 % coverage), 2-3 integration tests, 1 end-to-end con Playwright.
  3. GitHub Actions (cap. 5) — pipeline que en cada PR: lint → test → build → docker push → deploy a staging.
  4. Docker (cap. 5) — Dockerfile multi-stage, imagen final < 100 MB.
  5. Deploy (cap. 5) — staging en Fly.io / Railway / Render free tier. Producción solo se actualiza al mergear a main.
  6. DORA básico — instrumentar deployment frequency y lead time con un script simple sobre los logs de Actions. Mostrá los 4 KPIs en el README.
  7. Postmortem ficticio — simulá un incidente (/health falla 5xx) y escribí un postmortem blameless de 1 página.

Criterio de éxito: alguien forkea el repo, corre make setup, hace un PR trivial, y ve el pipeline correr verde en menos de 10 minutos. Si rompe un test, el deploy se bloquea.

Tiempo estimado: 2-3 semanas. Es proyecto portafolio: lo enseñás en entrevistas.


Definiciones nuevas: CI (Continuous Integration), CD (Continuous Delivery/Deployment), GitHub Actions, pipeline, Docker, imagen, contenedor, Docker Compose, Docker Registry, DevOps, Infrastructure as Code, métricas DORA, deployment frequency, lead time, change failure rate, time to restore.