StackPractices
beginner Por Mathias Paulenko

Configuración de Pipelines CI/CD

Configura pipelines CI/CD automatizados para testing, building y deployment de aplicaciones con GitHub Actions y lo que funciona.

Temas: devops

Descripción general

Continuous Integration y Continuous Deployment (CI/CD) automatizan el viaje desde el commit de código hasta el deploy en producción. Un pipeline bien configurado ejecuta tests, construye artifacts, escanea vulnerabilidades y despliega a staging o producción sin intervención manual. Esto elimina errores humanos, acelera releases y provee feedback rápido a los desarrolladores.

Cuándo usarla

Usa este recurso cuando:

  • configures un nuevo proyecto y quieras testing automatizado desde el día uno. Consulta Unit Testing para fundamentos de testing.
  • Migres de deploys manuales a releases automatizados. Consulta GitHub Actions para automatización de workflows.
  • Agregues scanning de seguridad, linting o gates de calidad de código a tu workflow. Consulta Container Security Scanning para gates de seguridad en CI.
  • Construyas una estrategia de deploy multi-ambiente (dev → staging → prod). Consulta Blue-Green Deployment para releases sin downtime.

Solución

Workflow de GitHub Actions

name: CI/CD Pipeline

on:
  push:
    branches: [main]
  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 run lint
      - run: npm run test:ci
      - run: npm run build

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit --audit-level=moderate

  deploy:
    needs: [test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: |
          npm run build
          npm run deploy:staging

Configuración de GitLab CI

stages:
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "20"

test:
  stage: test
  image: node:$NODE_VERSION
  script:
    - npm ci
    - npm run test:ci

deploy_prod:
  stage: deploy
  script:
    - npm run deploy:production
  only:
    - main
  environment:
    name: production
    url: https://api.example.com

Explicación

flowchart diagram: Push / PR

Un pipeline de CI/CD de producción típicamente incluye:

  1. Trigger: Push, pull request o job programado por cron
  2. Build: Compila, bundlea y crea artifacts
  3. Test: Tests unitarios, tests de integración, linting, type checking
  4. Seguridad: Auditoría de dependencias, SAST, escaneo de secretos
  5. Deploy: Push a staging, smoke tests, promoción a producción
  6. Notificación: Slack, email o sistema de incident management

Estrategias de deploy:

  • Básico: Deploy directo a producción
  • Blue-Green: Dos ambientes idénticos; switch de tráfico instantáneo
  • Canary: Rutea 1% de tráfico a nueva versión; incrementa gradualmente
  • Rolling: Reemplaza instancias una a una con zero downtime

Variantes

PlataformaIdeal ParaNotas
GitHub ActionsOpen source, repos GitHubGratis para repos públicos; marketplace de actions
GitLab CIProyectos GitLab-hostedBuilt-in; excelente para monorepos
CircleCITesting paralelo rápidoExcelente soporte de Docker
JenkinsOn-premise, plugins customSelf-hosted; alto mantenimiento
ArgoCDKubernetes GitOpsDeclarativo; sincroniza cluster con estado Git

Lo que funciona

  • Fail fast: Ejecuta linting y tests unitarios rápidos antes de tests de integración costosos
  • Paraleliza jobs: Divide tests por archivo o módulo para reducir tiempo wall-clock
  • Cachea dependencias: Cachea node_modules, pip cache y capas Docker entre ejecuciones
  • Usa secrets management: Nunca commitees API keys; usa secrets de GitHub/GitLab o Vault
  • Requiere reviews para prod: Usa branch protection y CODEOWNERS

Errores comunes

  1. Sin promoción de artifacts: Reconstruir en cada stage introduce no-determinismo
  2. Testear solo en CI: Los desarrolladores hacen push de código roto y esperan feedback de CI
  3. Secrets en variables de entorno: Visibles en logs de jobs; usa secrets enmascarados
  4. Sin plan de rollback: Deploys fallidos necesitan revert instantáneo vía blue-green o imagen anterior
  5. Ignorar tests flaky: Fallos aleatorios erosionan la confianza en el pipeline

Ejemplos avanzados

Deploy Multi-Entorno con Gates

# .github/workflows/deploy.yml
name: CI/CD Pipeline

on:
  push:
    branches: [main, staging]
  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 run lint
      - run: npm run test:ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: build-output
          path: dist/

  security:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm audit --audit-level=moderate
      - name: Run SAST
        uses: github/codeql-action/init@v3
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@v3

  deploy-staging:
    needs: [test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/staging'
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy a staging
        run: |
          aws s3 sync dist/ s3://staging-bucket --delete
          aws cloudfront create-invalidation --distribution-id ${{ secrets.STAGING_DIST_ID }} --paths "/*"

  deploy-production:
    needs: [test, security]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://example.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: build-output
          path: dist/
      - name: Deploy a producción
        run: |
          aws s3 sync dist/ s3://prod-bucket --delete
          aws cloudfront create-invalidation --distribution-id ${{ secrets.PROD_DIST_ID }} --paths "/*"

Docker Build y Push en CI

# .github/workflows/docker.yml
name: Docker Build and Push

on:
  push:
    tags: ['v*']

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/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:${{ github.ref_name }}
            ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Deploy Blue-Green

# .github/workflows/blue-green.yml
deploy:
  needs: [test, security]
  runs-on: ubuntu-latest
  if: github.ref == 'refs/heads/main'
  steps:
    - uses: actions/checkout@v4
    - name: Determinar entorno activo
      id: active
      run: |
        ACTIVE=$(kubectl get service app -o jsonpath='{.spec.selector.color}')
        echo "active=$ACTIVE" >> $GITHUB_OUTPUT
        if [ "$ACTIVE" = "blue" ]; then
          echo "target=green" >> $GITHUB_OUTPUT
        else
          echo "target=blue" >> $GITHUB_OUTPUT
        fi
    - name: Deploy al entorno inactivo
      run: |
        kubectl set image deployment/app-${{ steps.active.outputs.target }} \
          app=ghcr.io/org/app:${{ github.sha }}
        kubectl rollout status deployment/app-${{ steps.active.outputs.target }}
    - name: Cambiar tráfico
      run: |
        kubectl patch service app -p \
          '{"spec":{"selector":{"color":"${{ steps.active.outputs.target }}"}}}'
    - name: Ejecutar smoke tests
      run: |
        ./scripts/smoke-test.sh https://example.com/health
        if [ $? -ne 0 ]; then
          kubectl patch service app -p \
            '{"spec":{"selector":{"color":"${{ steps.active.outputs.active }}"}}}'
          exit 1
        fi

Deploy Canary con Argo Rollouts

# rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: api-server
spec:
  replicas: 10
  strategy:
    canary:
      steps:
      - setWeight: 10
      - pause: { duration: 5m }
      - setWeight: 25
      - pause: { duration: 5m }
      - setWeight: 50
      - pause: { duration: 10m }
      - setWeight: 100
  selector:
    matchLabels:
      app: api-server
  template:
    metadata:
      labels:
        app: api-server
    spec:
      containers:
      - name: api
        image: ghcr.io/org/api:v2.0.0
        ports:
        - containerPort: 8080

Estrategia de Caching en Pipeline

# .github/workflows/cached-build.yml
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

  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          cache-from: type=gha
          cache-to: type=gha,mode=max

  pip:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
          cache: 'pip'
      - run: pip install -r requirements.txt
      - run: pytest

Tips de Rendimiento

  1. Cachea agresivamente. Cachea dependencias, Docker layers y outputs de build:
- uses: actions/cache@v4
  with:
    path: |
      node_modules
      .next/cache
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
  1. Usa jobs condicionales. Salta jobs innecesarios según archivos cambiados:
test:
  steps:
    - uses: dorny/paths-filter@v3
      id: changes
      with:
        filters: |
          src:
            - 'src/**'
          docs:
            - 'docs/**'
    - if: steps.changes.outputs.src == 'true'
      run: npm test
  1. Usa workflows reutilizables. DRY tus definiciones de pipeline:
# .github/workflows/test.yml (reutilizable)
on:
  workflow_call:
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - run: npm ci && npm test

# .github/workflows/main.yml
jobs:
  test:
    uses: ./.github/workflows/test.yml
  1. Setea timeouts de jobs. Previene jobs colgados que bloqueen el pipeline:
test:
  timeout-minutes: 15
  runs-on: ubuntu-latest

Lecturas recomendadas

Preguntas frecuentes

¿Debería desplegar en cada commit a main?

Sí para staging — ya pasó los gates. Para producción, añade una aprobación manual (environment: con reviewers obligatorios) o despliega solo en releases etiquetadas.

¿Cómo gestiono las migraciones de base de datos en CI/CD?

Correlas en un job separado antes del despliegue, y mantenlas compatibles hacia atrás para que la versión anterior de la app siga funcionando durante el rollout. Nunca las corras en el arranque de la app — en un despliegue de 20 pods son 20 cambios de esquema concurrentes.

¿Puedo usar el mismo pipeline para microservicios?

Sí — usa triggers por ruta (paths: o dorny/paths-filter) para que solo los servicios modificados se construyan y desplieguen. Herramientas de monorepo como Nx o Turborepo calculan el grafo afectado por ti.

¿Cómo depuro un pipeline que solo falla en CI?

Reprodúcelo en local primero con los mismos comandos y versiones. Si solo rompe en CI, casi siempre es el estado de caché, secretos enmascarados, la variable CI o diferencias del SO del runner — depura con logging por job o action-tmate en el job que falla.

¿Cuál es el pipeline mínimo que vale la pena?

Lint, tests unitarios y un gate de build en cada PR. Ese trío caza la mayoría de roturas y se monta en un día — todo lo demás se apoya encima.