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