Pipeline CI/CD con Docker

Lectura
25 min~8 min lectura
CONCEPTO CLAVE: Un pipeline CI/CD con Docker automatiza la construcción, pruebas y despliegue de aplicaciones en contenedores. Esto garantiza consistencia entre entornos, reduce errores humanos y acelera el ciclo de desarrollo.

¿Qué es CI/CD?

CI/CD significa Integración Continua (Continuous Integration) y Entrega/Despliegue Continuo (Continuous Delivery/Deployment). Cuando combinamos estas prácticas con Docker, obtenemos un flujo de trabajo donde cada cambio en el código pasa por un proceso automatizado que verifica su calidad antes de llegar a producción.

La magia de Docker en CI/CD radica en que el mismo contenedor que se construye y prueba en el pipeline es exactamente el que llega a producción, eliminando el famoso problema "funciona en mi máquina".

Arquitectura de un Pipeline CI/CD con Docker

Un pipeline típico con Docker sigue esta estructura:

  1. Commit: El desarrollador sube código al repositorio.
  2. Build: Se construye la imagen Docker con el código nuevo.
  3. Test: Se ejecutan pruebas unitarias e integración dentro del contenedor.
  4. Security Scan: Se analizan vulnerabilidades en la imagen.
  5. Push: La imagen se sube al registry (Docker Hub, GCR, ECR).
  6. Deploy: Se despliega el contenedor en el entorno objetivo.
📌 Nota: El orden de los pasos puede variar según la herramienta y las necesidades del proyecto, pero la filosofía general permanece.

Preparando nuestra Aplicación

Antes de crear el pipeline, necesitamos una aplicación lista para Docker. Usaremos una aplicación Node.js simple como ejemplo:

# Dockerfile para desarrollo (multi-stage)
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:18-alpine AS production
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
💡 Consejo: Usar multi-stage builds reduce significativamente el tamaño de la imagen final y elimina archivos innecesarios como dependencias de desarrollo.

GitHub Actions + Docker

GitHub Actions es una de las herramientas más populares para CI/CD. Veamos un ejemplo completo:

# .github/workflows/docker-pipeline.yml
name: Docker CI/CD Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Build image
        run: |
          docker build \
            --tag ${{ env.IMAGE_NAME }}:test \
            --target test \
            .
      
      - name: Run tests
        run: |
          docker run --rm ${{ env.IMAGE_NAME }}:test npm test

  security-scan:
    runs-on: ubuntu-latest
    needs: build-and-test
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.IMAGE_NAME }}:test
          format: 'sarif'
          output: 'trivy-results.sarif'
      
      - name: Upload results to GitHub Security
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  push:
    runs-on: ubuntu-latest
    needs: security-scan
    if: github.ref == 'refs/heads/main'
    permissions:
      contents: read
      packages: write
    steps:
      - name: Login to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
⚠️ Advertencia: Nunca guardes contraseñas o tokens en el código. Usa secretos de GitHub (Settings > Secrets and variables > Actions).

Dockerfile Multi-Stage para Testing

Para que el pipeline funcione correctamente, necesitamos un Dockerfile que soporte diferentes objetivos:

# Dockerfile completo con múltiples stages
FROM node:18-alpine AS base
WORKDIR /app
COPY package*.json ./

# Stage para desarrollo
FROM base AS development
RUN npm install
COPY . .
CMD ["npm", "run", "dev"]

# Stage para pruebas
FROM base AS test
RUN npm ci
COPY . .
# Ejecutar pruebas en el pipeline
CMD ["npm", "test"]

# Stage para producción
FROM base AS production
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
FROM node:18-alpine AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY --from=production /app/dist ./dist
COPY --from=production /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]

Optimizaciones para CI/CD

Existen varias técnicas para hacer que los pipelines con Docker sean más rápidos y eficientes:

TécnicaDescripciónBeneficio
BuildKit CacheActivar caché de construcción con --mount=type=cacheReduce tiempo de build hasta 80%
Dependency CachingCachear node_modules o pip packagesEvita descargas repetitivas
Multi-platform BuildsConstruir para múltiples arquitecturasCompatibilidad universal
Layer CachingReutilizar capas no modificadasBuilds incrementales rápidos
💡 Tip avanzado: En GitHub Actions, usa cache-from: type=gha para activar el caché de GitHub Actions, que puede reducir drásticamente los tiempos de construcción en pipelines subsequentes.

Ejemplo con GitLab CI

Para quienes usen GitLab, aquí está la configuración equivalente:

# .gitlab-ci.yml
stages:
  - build
  - test
  - security
  - push
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

build:
  stage: build
  image: docker:24-dind
  services:
    - docker:24-dind
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $IMAGE_TAG .
    - docker push $IMAGE_TAG

test:
  stage: test
  image: $IMAGE_TAG
  script:
    - npm install
    - npm test

trivy-scan:
  stage: security
  image:
    name: aquasecurity/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 0 --severity HIGH,CRITICAL $IMAGE_TAG
  allow_failure: true

deploy-staging:
  stage: deploy
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/app app=$IMAGE_TAG
  environment:
    name: staging
  only:
    - develop

deploy-production:
  stage: deploy
  script:
    - kubectl config use-context production
    - kubectl set image deployment/app app=$IMAGE_TAG
  environment:
    name: production
  when: manual
  only:
    - main
Ver más: Ejemplo con Jenkinsfile
// Jenkinsfile (Jenkins Declarative Pipeline)
pipeline {
    agent {
        docker {
            image 'node:18-alpine'
            args '-v /root/.npm:/root/.npm'
        }
    }
    
    environment {
        DOCKER_IMAGE = 'myapp'
        REGISTRY = 'docker.io'
    }
    
    stages {
        stage('Build') {
            steps {
                sh 'npm install'
                sh 'npm run build'
            }
        }
        
        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    junit '**/test-results/*.xml'
                }
            }
        }
        
        stage('Build Docker Image') {
            steps {
                script {
                    def customImage = docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                }
            }
        }
        
        stage('Push to Registry') {
            steps {
                script {
                    docker.withRegistry('https://registry.hub.docker.com', 'docker-hub-cred') {
                        def customImage = docker.build("${DOCKER_IMAGE}:${env.BUILD_NUMBER}")
                        customImage.push()
                        customImage.push('latest')
                    }
                }
            }
        }
        
        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh 'kubectl apply -f k8s/'
            }
        }
    }
}

Mejores Prácticas de Seguridad

  1. No ejecutes como root: Usa USER en el Dockerfile.
  2. Escanea vulnerabilidades: Integra herramientas como Trivy, Snyk o Clair.
  3. Usa imágenes oficiales: Prefiere imágenes base con soporte activo.
  4. Minimiza capas: Combina instrucciones RUN cuando sea posible.
  5. No incluyas secretos: Usa build args solo para configuración no sensible.
  6. Firma tus imágenes: Implementa Docker Content Trust.
⚠️ Importante: El escaneo de seguridad debe ser parte del pipeline, no un paso opcional. Las vulnerabilidades en imágenes de producción son un vector de ataque común.

Integración con Kubernetes

El pipeline completo frecuentemente termina con un despliegue a Kubernetes. Aquí está cómo actualizar un deployment automáticamente:

# Script de despliegue (deploy.sh)
#!/bin/bash
set -e

NEW_IMAGE=$1
KUBECONFIG=${KUBECONFIG:-~/.kube/config}
NAMSPACE=${NAMESPACE:-default}
DEPLOYMENT=${DEPLOYMENT:-app}

echo "Desplegando imagen: $NEW_IMAGE"
echo "Namespace: $NAMSPACE"
echo "Deployment: $DEPLOYMENT"

# Actualizar la imagen en el deployment
kubectl set image deployment/$DEPLOYMENT \
  $DEPLOYMENT=$NEW_IMAGE \
  -n $NAMESPACE

# Esperar a que el rollout complete
kubectl rollout status deployment/$DEPLOYMENT \
  -n $NAMSPACE \
  --timeout=300s

echo "✅ Despliegue completado exitosamente"

# Mostrar los pods activos
kubectl get pods -n $NAMSPACE -l app=$DEPLOYMENT
📌 Integración CI/CD completa: Este script puede ejecutarse como último paso del pipeline después de que la imagen se haya construido y subido exitosamente al registry.

Monitoreo Post-Despliegue

Un pipeline CI/CD robusto debe incluir verificaciones post-despliegue:

  • Health checks: Verificar que el contenedor responde correctamente.
  • Smoke tests: Ejecutar pruebas mínimas en el entorno de producción.
  • Integración con APM: Monitorear métricas de rendimiento.
  • Rollback automático: Revertir si algo falla.
🧠 Quiz: Pipeline CI/CD con Docker

¿Por qué es importante usar multi-stage builds en un pipeline CI/CD?

  • A) Para que el código sea más legible
  • B) Para reducir el tamaño de la imagen final y separar dependencias de construcción de producción
  • C) Para ejecutar múltiples contenedores simultáneamente
  • D) Para evitar usar Dockerfile
✅ Respuesta correcta: B) Los multi-stage builds permiten tener un stage de construcción con todas las herramientas necesarias y un stage de producción minimalista, reduciendo significativamente el tamaño de la imagen final y la superficie de ataque.

Conclusión

Un pipeline CI/CD con Docker transforma radicalmente la forma en que desarrollamos y desplegamos software. La combinación de contenedores con automatización de pipelines nos permite:

  • Consistencia: Mismo entorno en todas las etapas.
  • Velocidad: Despliegues automatizados en minutos.
  • Confianza: Pruebas automatizadas en cada cambio.
  • Escalabilidad: Facilita el escalado horizontal.
  • Reversión: Rollback rápido ante problemas.

La inversión inicial en configurar un pipeline robusto se recupera rápidamente en forma de reducción de errores, tiempo de desarrollo y operaciones más fluidas.

CONCEPTO CLAVE: El mejor pipeline es el que se ejecuta automáticamente en cada commit, prueba exhaustivamente, despliega de forma segura y notifica al equipo ante cualquier problema. Docker es el pegamento que une todas estas partes de manera consistente.