Volver
Módulo 11 - DevOps Avanzado EXPERTO

Bash en Pipelines CI/CD

Integra Bash en tus procesos de integración y despliegue continuo para automatización robusta y escalable

Los scripts Bash son fundamentales en los pipelines CI/CD modernos, proporcionando flexibilidad, portabilidad y control granular sobre los procesos de construcción, pruebas y despliegue. En este módulo aprenderás a crear pipelines robustos y escalables.

CI/CD y Bash: Una Combinación Poderosa

Los scripts Bash son fundamentales en los pipelines CI/CD modernos, proporcionando flexibilidad, portabilidad y control granular sobre los procesos de construcción, pruebas y despliegue.

Código
Git Push
Build
Compilar
Test
Verificar
Deploy
Desplegar
Monitor
Observar
Ventajas de Bash en CI/CD
  • Universalidad: Funciona en cualquier sistema Unix/Linux
  • Flexibilidad: Fácil integración con herramientas existentes
  • Control: Manejo detallado de procesos y errores
  • Portabilidad: Scripts reutilizables entre diferentes plataformas CI/CD
  • Debugging: Fácil depuración y logging

Scripts de Build Inteligentes

Build script universal

Script de construcción inteligente
bash
#!/bin/bash
# build.sh - Script de construcción inteligente

set -euo pipefail

# Configuración
PROJECT_NAME="${PROJECT_NAME:-$(basename "$(pwd)")}"
BUILD_ID="${BUILD_ID:-$(date +%Y%m%d_%H%M%S)}"
NODE_VERSION="${NODE_VERSION:-18}"
DOCKER_REGISTRY="${DOCKER_REGISTRY:-registry.company.com}"
SKIP_TESTS="${SKIP_TESTS:-false}"
PARALLEL_JOBS="${PARALLEL_JOBS:-$(nproc)}"

# Colores para output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# Logging mejorado
log() {
    echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $*"
}

log_success() {
    echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] ✓${NC} $*"
}

log_error() {
    echo -e "${RED}[$(date '+%Y-%m-%d %H:%M:%S')] ✗${NC} $*" >&2
}

log_warning() {
    echo -e "${YELLOW}[$(date '+%Y-%m-%d %H:%M:%S')] ⚠${NC} $*"
}

# Detección automática del tipo de proyecto
detect_project_type() {
    if [[ -f "package.json" ]]; then
        echo "node"
    elif [[ -f "pom.xml" ]]; then
        echo "maven"
    elif [[ -f "build.gradle" ]]; then
        echo "gradle"
    elif [[ -f "Cargo.toml" ]]; then
        echo "rust"
    elif [[ -f "go.mod" ]]; then
        echo "go"
    elif [[ -f "requirements.txt" ]] || [[ -f "pyproject.toml" ]]; then
        echo "python"
    elif [[ -f "Dockerfile" ]]; then
        echo "docker"
    else
        echo "unknown"
    fi
}

# Preparación del entorno
prepare_environment() {
    local project_type="$1"
    
    log "Preparando entorno para proyecto $project_type"
    
    case "$project_type" in
        "node")
            # Verificar versión de Node
            if ! node --version | grep -q "v${NODE_VERSION}"; then
                log_warning "Versión de Node diferente a la esperada (${NODE_VERSION})"
            fi
            
            # Cache de node_modules
            if [[ -f "package-lock.json" ]]; then
                npm ci --prefer-offline --no-audit
            else
                npm install
            fi
            ;;
            
        "python")
            # Crear entorno virtual si no existe
            if [[ ! -d "venv" ]]; then
                python3 -m venv venv
            fi
            
            source venv/bin/activate
            pip install -r requirements.txt
            ;;
            
        "go")
            go mod download
            ;;
            
        "rust")
            cargo fetch
            ;;
            
        *)
            log_warning "Tipo de proyecto no reconocido: $project_type"
            ;;
    esac
}

# Construcción por tipo de proyecto
build_project() {
    local project_type="$1"
    
    log "Iniciando construcción para $project_type"
    
    case "$project_type" in
        "node")
            # Linting
            if npm run lint --if-present; then
                log_success "Linting pasado"
            else
                log_error "Linting falló"
                return 1
            fi
            
            # Build
            if npm run build --if-present; then
                log_success "Build completado"
            else
                log_error "Build falló"
                return 1
            fi
            ;;
            
        "python")
            source venv/bin/activate
            
            # Linting con flake8
            if command -v flake8 >/dev/null 2>&1; then
                flake8 src/ --count --select=E9,F63,F7,F82 --show-source --statistics
            fi
            
            # Build del paquete
            python setup.py build
            ;;
            
        "go")
            # Linting
            if command -v golangci-lint >/dev/null 2>&1; then
                golangci-lint run
            fi
            
            # Build con optimizaciones
            CGO_ENABLED=0 go build -ldflags="-w -s" -o bin/ ./...
            ;;
            
        "rust")
            cargo build --release
            ;;
            
        "docker")
            # Build multi-stage optimizado
            docker build \
                --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
                --build-arg VCS_REF="$(git rev-parse --short HEAD)" \
                --build-arg VERSION="${BUILD_ID}" \
                --tag "${DOCKER_REGISTRY}/${PROJECT_NAME}:${BUILD_ID}" \
                --tag "${DOCKER_REGISTRY}/${PROJECT_NAME}:latest" \
                .
            ;;
            
        *)
            log_error "No se sabe cómo construir proyecto tipo: $project_type"
            return 1
            ;;
    esac
}

# Función principal
main() {
    local start_time=$(date +%s)
    
    log "Iniciando build de ${PROJECT_NAME} (${BUILD_ID})"
    
    # Detección automática del tipo de proyecto
    local project_type=$(detect_project_type)
    log "Tipo de proyecto detectado: $project_type"
    
    # Pipeline de construcción
    prepare_environment "$project_type" || { log_error "Falló preparación del entorno"; exit 1; }
    build_project "$project_type" || { log_error "Falló la construcción"; exit 1; }
    
    local end_time=$(date +%s)
    local duration=$((end_time - start_time))
    
    log_success "Build completado exitosamente en ${duration} segundos"
}

# Ejecutar si es llamado directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Pipelines de Despliegue Avanzados

Pipeline multi-entorno con estrategias de despliegue

Pipeline de despliegue multi-entorno
bash
#!/bin/bash
# deploy-pipeline.sh - Pipeline de despliegue multi-entorno

set -euo pipefail

# Configuración global
ENVIRONMENTS=("dev" "staging" "prod")
PROJECT_NAME="${PROJECT_NAME:-$(basename "$(pwd)")}"
DEPLOYMENT_STRATEGY="${DEPLOYMENT_STRATEGY:-rolling}"
SLACK_WEBHOOK="${SLACK_WEBHOOK:-}"

# Configuración por entorno
declare -A ENV_CONFIG
ENV_CONFIG[dev]="namespace=development replicas=1 resources=small"
ENV_CONFIG[staging]="namespace=staging replicas=2 resources=medium"
ENV_CONFIG[prod]="namespace=production replicas=5 resources=large"

# Funciones de utilidad
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}

send_notification() {
    local message="$1"
    local color="${2:-good}"
    
    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -X POST -H 'Content-type: application/json' \
            --data "{\"text\":\"$message\", \"color\":\"$color\"}" \
            "$SLACK_WEBHOOK" || true
    fi
    
    log "$message"
}

# Validación de pre-despliegue
validate_deployment() {
    local environment="$1"
    local image_tag="$2"
    
    log "Validando despliegue para $environment"
    
    # Verificar que la imagen existe
    if ! docker manifest inspect "$image_tag" >/dev/null 2>&1; then
        log "ERROR: Imagen $image_tag no encontrada"
        return 1
    fi
    
    # Verificar conectividad al cluster
    if ! kubectl cluster-info --context="$environment" >/dev/null 2>&1; then
        log "ERROR: No se puede conectar al cluster $environment"
        return 1
    fi
    
    log "Validación exitosa para $environment"
    return 0
}

# Estrategia de despliegue Rolling
deploy_rolling() {
    local environment="$1"
    local image_tag="$2"
    local config="${ENV_CONFIG[$environment]}"
    
    log "Iniciando despliegue rolling en $environment"
    
    # Parsear configuración
    local namespace=$(echo "$config" | grep -o 'namespace=[^ ]*' | cut -d'=' -f2)
    local replicas=$(echo "$config" | grep -o 'replicas=[^ ]*' | cut -d'=' -f2)
    
    # Crear namespace si no existe
    kubectl create namespace "$namespace" --dry-run=client -o yaml | kubectl apply -f - --context="$environment"
    
    # Aplicar deployment
    cat << EOF | kubectl apply -f - --context="$environment"
apiVersion: apps/v1
kind: Deployment
metadata:
  name: ${PROJECT_NAME}
  namespace: ${namespace}
  labels:
    app: ${PROJECT_NAME}
    version: $(echo "$image_tag" | cut -d':' -f2)
spec:
  replicas: ${replicas}
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 0
  selector:
    matchLabels:
      app: ${PROJECT_NAME}
  template:
    metadata:
      labels:
        app: ${PROJECT_NAME}
        version: $(echo "$image_tag" | cut -d':' -f2)
    spec:
      containers:
      - name: ${PROJECT_NAME}
        image: ${image_tag}
        ports:
        - containerPort: 8080
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /ready
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 5
EOF

    # Esperar que el despliegue se complete
    log "Esperando completar despliegue..."
    kubectl rollout status deployment/"$PROJECT_NAME" -n "$namespace" --context="$environment" --timeout=600s
    
    log "Despliegue rolling completado exitosamente en $environment"
    return 0
}

# Pipeline principal
main() {
    local environment="$1"
    local image_tag="$2"
    local strategy="${DEPLOYMENT_STRATEGY}"
    
    log "Iniciando pipeline de despliegue"
    log "Entorno: $environment"
    log "Imagen: $image_tag"
    log "Estrategia: $strategy"
    
    # Validar parámetros
    if [[ ! " ${ENVIRONMENTS[*]} " =~ " ${environment} " ]]; then
        log "ERROR: Entorno inválido: $environment"
        log "Entornos disponibles: ${ENVIRONMENTS[*]}"
        exit 1
    fi
    
    # Validación previa
    if ! validate_deployment "$environment" "$image_tag"; then
        send_notification "❌ Validación falló para $environment" "danger"
        exit 1
    fi
    
    # Notificar inicio
    send_notification "🚀 Iniciando despliegue en $environment con imagen $image_tag"
    
    # Ejecutar estrategia de despliegue
    if deploy_rolling "$environment" "$image_tag"; then
        send_notification "✅ Despliegue exitoso en $environment" "good"
    else
        send_notification "❌ Despliegue falló en $environment" "danger"
        exit 1
    fi
}

# Ejecutar si es llamado directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    if [[ $# -lt 2 ]]; then
        echo "Uso: $0   [strategy]"
        echo "Environments: ${ENVIRONMENTS[*]}"
        echo "Strategies: rolling, blue-green"
        exit 1
    fi
    
    DEPLOYMENT_STRATEGY="${3:-$DEPLOYMENT_STRATEGY}"
    main "$1" "$2"
fi

Integración con Plataformas CI/CD

GitHub Actions

GitHub Actions Workflow
yaml
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline

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

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

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Run build script
      run: |
        chmod +x ./scripts/build.sh
        ./scripts/build.sh
      env:
        PROJECT_NAME: ${{ github.repository }}
        BUILD_ID: ${{ github.sha }}
        NODE_VERSION: 18
    
    - name: Upload artifacts
      uses: actions/upload-artifact@v4
      with:
        name: build-artifacts
        path: artifacts/

  deploy-staging:
    needs: build
    if: github.ref == 'refs/heads/develop'
    runs-on: ubuntu-latest
    environment: staging
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
      
    - name: Configure kubectl
      run: |
        echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > kubeconfig
        export KUBECONFIG=kubeconfig
        
    - name: Deploy to staging
      run: |
        chmod +x ./scripts/deploy-pipeline.sh
        ./scripts/deploy-pipeline.sh staging ${{ needs.build.outputs.image-tag }}
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

  deploy-production:
    needs: [build, deploy-staging]
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    
    steps:
    - name: Manual approval
      uses: trstringer/manual-approval@v1
      with:
        secret: ${{ github.TOKEN }}
        approvers: devops-team,tech-leads
        
    - name: Deploy to production
      run: |
        chmod +x ./scripts/deploy-pipeline.sh
        ./scripts/deploy-pipeline.sh prod ${{ needs.build.outputs.image-tag }} blue-green
      env:
        SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

Mejores Prácticas

Scripts CI/CD Robustos
  • Fail Fast: Usa set -euo pipefail para fallar rápidamente
  • Idempotencia: Scripts que pueden ejecutarse múltiples veces
  • Logging: Output detallado con timestamps y niveles
  • Cleanup: Siempre limpia recursos temporales
  • Secretos: Nunca hardcodees credenciales
Errores Comunes
  • Paths relativos: Siempre usa paths absolutos o relativos al script
  • Variables no definidas: Siempre valida variables de entorno
  • Procesos huérfanos: Implementa limpieza con traps
  • Timeouts: Define timeouts para operaciones externas
  • Permisos: Ejecuta con los mínimos privilegios necesarios

Template de script CI/CD seguro

Template seguro para scripts CI/CD
bash
#!/bin/bash
# template-cicd-script.sh - Template seguro para scripts CI/CD

set -euo pipefail
IFS=$'\n\t'

# Configuración
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
readonly LOG_FILE="${LOG_FILE:-/dev/stdout}"
readonly DEBUG="${DEBUG:-false}"

# Variables requeridas
required_vars=(
    "PROJECT_NAME"
    "BUILD_ID"
    "ENVIRONMENT"
)

# Logging
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}

debug() {
    if [[ "$DEBUG" == "true" ]]; then
        log "DEBUG: $*"
    fi
}

error() {
    log "ERROR: $*" >&2
}

# Cleanup function
cleanup() {
    local exit_code=$?
    log "Cleaning up..."
    
    # Cleanup temp files
    rm -rf "${TMPDIR:-/tmp}/cicd-$$"
    
    # Kill background processes
    jobs -p | xargs -r kill 2>/dev/null || true
    
    exit $exit_code
}

trap cleanup EXIT

# Validar variables requeridas
validate_environment() {
    for var in "${required_vars[@]}"; do
        if [[ -z "${!var:-}" ]]; then
            error "Variable requerida no definida: $var"
            exit 1
        fi
    done
}

# Función principal
main() {
    log "Iniciando script CI/CD"
    
    validate_environment
    
    # Tu lógica aquí
    log "Ejecutando lógica principal..."
    
    log "Script completado exitosamente"
}

# Ejecutar solo si es llamado directamente
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

Ejercicios Prácticos

Ejercicio 1: Pipeline Completo

Crea un pipeline CI/CD completo que incluya:

  • Build automático con detección de tecnología
  • Tests unitarios y de integración
  • Análisis de seguridad y calidad
  • Despliegue multi-entorno
  • Rollback automático en caso de fallo
Ejercicio 2: Monitoring Pipeline

Implementa un sistema de monitoreo que:

  • Rastree métricas de cada stage del pipeline
  • Detecte patrones de fallo
  • Genere alertas proactivas
  • Proporcione dashboards de rendimiento
Ejercicio 3: Multi-Cloud Deployment

Desarrolla scripts para desplegar en múltiples clouds:

  • AWS EKS, Azure AKS, Google GKE
  • Configuración específica por proveedor
  • Rollback coordinado entre clouds
  • Health checks distribuidos