Salta el contingut

Pràctica 1: Case Study CI/CD Complet

Context de l'Aplicació

Escenari: Una botiga online desenvolupada per un equip de 5 desenvolupadors. L'aplicació té: - Frontend: React (TypeScript) - Backend: Node.js amb Express i PostgreSQL - Infraestructura: AWS (EC2, RDS, S3) - Repositori: GitHub - CI/CD: GitHub Actions

Arquitectura del Sistema

┌─────────────────────────────────────────────────┐
│                                                 │
│  USUARIS                                        │
│                                                 │
└────────────┬────────────────────────────────────┘
    ┌────────────────┐
    │   CloudFront   │ ← CDN per assets estàtics
    │      (CDN)     │
    └────────┬───────┘
    ┌────────────────┐
    │   S3 Bucket    │ ← Frontend (React build)
    └────────────────┘

    ┌────────────────┐
    │  Load Balancer │
    └────────┬───────┘
      ┌──────┴──────┐
      │             │
      ▼             ▼
┌──────────┐  ┌──────────┐
│   EC2    │  │   EC2    │  ← Backend (Node.js)
│ Instance │  │ Instance │
└────┬─────┘  └────┬─────┘
     │             │
     └──────┬──────┘
    ┌────────────────┐
    │   RDS          │  ← PostgreSQL Database
    │  (PostgreSQL)  │
    └────────────────┘

Estructura del Repositori

ecommerce-app/
├── .github/
│   └── workflows/
│       ├── frontend-ci.yml
│       ├── backend-ci.yml
│       └── deploy.yml
├── frontend/
│   ├── src/
│   ├── public/
│   ├── package.json
│   ├── tsconfig.json
│   └── Dockerfile
├── backend/
│   ├── src/
│   ├── tests/
│   ├── package.json
│   ├── tsconfig.json
│   └── Dockerfile
├── infrastructure/
│   ├── terraform/
│   └── docker-compose.yml
├── docs/
└── README.md

Pipeline Frontend (.github/workflows/frontend-ci.yml)

name: Frontend CI/CD

on:
  push:
    branches: [main, develop]
    paths:
      - 'frontend/**'
      - '.github/workflows/frontend-ci.yml'
  pull_request:
    branches: [main]
    paths:
      - 'frontend/**'

env:
  NODE_VERSION: '18.x'

jobs:
  # Job 1: Lint i Type Check
  lint-and-typecheck:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Install dependencies
      run: npm ci

    - name: Run ESLint
      run: npm run lint

    - name: Run TypeScript check
      run: npm run type-check

  # Job 2: Unit Tests
  unit-tests:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Install dependencies
      run: npm ci

    - name: Run tests with coverage
      run: npm run test:coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        files: ./frontend/coverage/coverage-final.json
        flags: frontend

  # Job 3: E2E Tests amb Playwright
  e2e-tests:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./frontend

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Install dependencies
      run: npm ci

    - name: Install Playwright browsers
      run: npx playwright install --with-deps

    - name: Build application
      run: npm run build

    - name: Run Playwright tests
      run: npm run test:e2e

    - uses: actions/upload-artifact@v3
      if: always()
      with:
        name: playwright-report
        path: frontend/playwright-report/
        retention-days: 30

  # Job 4: Build i Deploy a S3
  build-and-deploy:
    runs-on: ubuntu-latest
    needs: [lint-and-typecheck, unit-tests, e2e-tests]
    if: github.ref == 'refs/heads/main'
    defaults:
      run:
        working-directory: ./frontend

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        cache-dependency-path: frontend/package-lock.json

    - name: Install dependencies
      run: npm ci

    - name: Build for production
      env:
        REACT_APP_API_URL: ${{ secrets.PROD_API_URL }}
        REACT_APP_ENV: production
      run: npm run build

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: eu-west-1

    - name: Deploy to S3
      run: |
        aws s3 sync build/ s3://${{ secrets.S3_BUCKET }} --delete

    - name: Invalidate CloudFront cache
      run: |
        aws cloudfront create-invalidation \
          --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} \
          --paths "/*"

    - name: Verify deployment
      run: |
        sleep 30
        curl --fail https://www.myshop.com || exit 1

  # Job 5: Lighthouse Performance Test
  lighthouse:
    runs-on: ubuntu-latest
    needs: build-and-deploy
    if: github.ref == 'refs/heads/main'

    steps:
    - uses: actions/checkout@v4

    - name: Run Lighthouse CI
      uses: treosh/lighthouse-ci-action@v10
      with:
        urls: |
          https://www.myshop.com
          https://www.myshop.com/products
        uploadArtifacts: true
        temporaryPublicStorage: true

Pipeline Backend (.github/workflows/backend-ci.yml)

name: Backend CI/CD

on:
  push:
    branches: [main, develop]
    paths:
      - 'backend/**'
      - '.github/workflows/backend-ci.yml'
  pull_request:
    branches: [main]
    paths:
      - 'backend/**'

env:
  NODE_VERSION: '18.x'
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}/backend

jobs:
  # Job 1: Lint i Tests
  test:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_DB: testdb
          POSTGRES_USER: testuser
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 5432:5432

      redis:
        image: redis:7
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
        ports:
          - 6379:6379

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
        cache-dependency-path: backend/package-lock.json

    - name: Install dependencies
      run: npm ci

    - name: Run linter
      run: npm run lint

    - name: Run TypeScript check
      run: npm run type-check

    - name: Run database migrations
      env:
        DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
      run: npm run migrate:up

    - name: Run unit tests
      env:
        DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
        REDIS_URL: redis://localhost:6379
        NODE_ENV: test
      run: npm run test:unit

    - name: Run integration tests
      env:
        DATABASE_URL: postgresql://testuser:testpass@localhost:5432/testdb
        REDIS_URL: redis://localhost:6379
        NODE_ENV: test
      run: npm run test:integration

    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        files: ./backend/coverage/coverage-final.json
        flags: backend

  # Job 2: Security Scan
  security:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./backend

    steps:
    - uses: actions/checkout@v4

    - name: Run npm audit
      run: npm audit --audit-level=moderate
      continue-on-error: true

    - name: Run Snyk scan
      uses: snyk/actions/node@master
      env:
        SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
      with:
        args: --severity-threshold=high

  # Job 3: Build Docker Image
  build:
    runs-on: ubuntu-latest
    needs: [test, security]
    if: github.ref == 'refs/heads/main'

    permissions:
      contents: read
      packages: write

    steps:
    - uses: actions/checkout@v4

    - name: Log in to Container Registry
      uses: docker/login-action@v3
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v5
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=sha,prefix={{branch}}-
          type=ref,event=branch
          latest

    - name: Build and push
      uses: docker/build-push-action@v5
      with:
        context: ./backend
        push: true
        tags: ${{ steps.meta.outputs.tags }}
        labels: ${{ steps.meta.outputs.labels }}
        cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
        cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max

  # Job 4: Deploy to EC2
  deploy:
    runs-on: ubuntu-latest
    needs: build
    environment:
      name: production
      url: https://api.myshop.com

    steps:
    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v4
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: eu-west-1

    - name: Deploy to EC2 instances
      env:
        PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
        HOSTNAME: ${{ secrets.EC2_HOST }}
        USER: ubuntu
      run: |
        echo "$PRIVATE_KEY" > private_key.pem
        chmod 600 private_key.pem

        ssh -o StrictHostKeyChecking=no -i private_key.pem ${USER}@${HOSTNAME} << 'ENDSSH'
          # Login to container registry
          echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin

          # Pull new image
          docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

          # Update docker-compose services
          cd /opt/myshop
          docker-compose pull backend
          docker-compose up -d backend

          # Run migrations
          docker-compose exec -T backend npm run migrate:up

          # Health check
          sleep 10
          curl --fail http://localhost:3000/health || exit 1
        ENDSSH

        rm -f private_key.pem

    - name: Verify deployment
      run: |
        sleep 30
        curl --fail https://api.myshop.com/health || exit 1

  # Job 5: Smoke Tests a Producció
  smoke-tests:
    runs-on: ubuntu-latest
    needs: deploy

    steps:
    - uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}

    - name: Install dependencies
      working-directory: ./backend
      run: npm ci --only=dev

    - name: Run smoke tests
      working-directory: ./backend
      env:
        API_URL: https://api.myshop.com
      run: npm run test:smoke

Configuració de Secrets

Per a que aquest case study funcioni, cal configurar els següents secrets a GitHub:

AWS_ACCESS_KEY_ID          # Credencials AWS
AWS_SECRET_ACCESS_KEY      # Credencials AWS
S3_BUCKET                  # Nom del bucket S3
CLOUDFRONT_DISTRIBUTION_ID # ID de la distribució CloudFront
PROD_API_URL               # URL de la API de producció
EC2_SSH_KEY                # Clau privada SSH per EC2
EC2_HOST                   # IP o DNS de les instàncies EC2
SNYK_TOKEN                 # Token per Snyk security scan
CODECOV_TOKEN              # Token per Codecov

Monitorització Post-Desplegament

Després del desplegament, és crucial monitoritzar l'aplicació. Exemples d'eines:

Application Performance Monitoring (APM): - New Relic - DataDog - Elastic APM

Log Aggregation: - ELK Stack (Elasticsearch, Logstash, Kibana) - CloudWatch Logs - Splunk

Error Tracking: - Sentry - Rollbar - Bugsnag

Exemple d'integració amb Sentry:

// backend/src/app.ts
import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV,
  release: process.env.COMMIT_SHA,
  tracesSampleRate: 1.0,
});

app.use(Sentry.Handlers.requestHandler());
app.use(Sentry.Handlers.tracingHandler());

// Routes...

app.use(Sentry.Handlers.errorHandler());

Estratègia de Rollback

Si el desplegament falla o es detecten problemes, cal una estratègia de rollback:

Opció 1: Rollback Manual (GitHub Actions)

name: Rollback Production

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to rollback to (commit SHA or tag)'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production

    steps:
    - name: Rollback backend
      env:
        PRIVATE_KEY: ${{ secrets.EC2_SSH_KEY }}
        HOSTNAME: ${{ secrets.EC2_HOST }}
        VERSION: ${{ github.event.inputs.version }}
      run: |
        echo "$PRIVATE_KEY" > private_key.pem
        chmod 600 private_key.pem

        ssh -i private_key.pem ubuntu@${HOSTNAME} << ENDSSH
          cd /opt/myshop
          docker pull ghcr.io/mycompany/myshop/backend:${VERSION}
          docker-compose up -d backend
        ENDSSH

Opció 2: Blue-Green Deployment

Mantenir dues versions de l'aplicació (Blue i Green) i canviar el tràfic del Load Balancer:

# Script per canviar entre versions
aws elbv2 modify-listener \
  --listener-arn $LISTENER_ARN \
  --default-actions Type=forward,TargetGroupArn=$GREEN_TARGET_GROUP

Mètriques Clau (KPIs)

Per mesurar l'èxit del pipeline CI/CD:

Deployment Frequency: - Quantes vegades es desplega a producció per setmana/dia

Lead Time for Changes: - Temps des de commit fins a producció

Mean Time to Recovery (MTTR): - Temps per recuperar-se d'un error en producció

Change Failure Rate: - Percentatge de desplegaments que fallen

Exercicis Pràctics per Alumnes

Exercici 1: Pipeline Bàsic (Nivell Inicial)

Crea un pipeline de CI per una aplicació Node.js que: 1. Faci checkout del codi 2. Instal·li dependències 3. Executi linter 4. Executi tests unitaris 5. Generi un informe de cobertura

Exercici 2: Multi-Stage Pipeline (Nivell Intermedi)

Implementa un pipeline amb múltiples stages: 1. Build stage: compila l'aplicació 2. Test stage: executa diferents tipus de tests 3. Quality stage: anàlisi de codi amb SonarQube 4. Package stage: crea imatge Docker 5. Deploy stage: desplega a staging

Exercici 3: Pipeline Complet amb Múltiples Entorns (Nivell Avançat)

Crea un pipeline complet que: 1. Executi tests i quality checks 2. Desplega automàticament a staging en push a develop 3. Desplega a production només amb tags 4. Requereixi aprovació manual per producció 5. Implementi smoke tests post-desplegament 6. Tingui estratègia de rollback

Exercici 4: Case Study Personalitzat

Els alumnes hauran de: 1. Escollir un projecte propi o proposat 2. Dissenyar l'arquitectura CI/CD completa 3. Documentar decisions tècniques 4. Implementar el pipeline 5. Presentar el resultat a classe