DevOps y CI/CD: Pipeline Moderno con GitHub Actions
Un pipeline de CI/CD bien diseñado es fundamental para el desarrollo moderno. Aprende a crear workflows automatizados que mejoren la calidad y velocidad de entrega.
Arquitectura del Pipeline
Estructura Básica
name: CI/CD Pipelineon:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: test_db
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'yarn'
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Run linting
run: yarn lint
- name: Run type checking
run: yarn type-check
- name: Run tests
run: yarn test:coverage
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test_db
- name: Upload coverage
uses: codecov/codecov-action@v3
Testing Automatizado
Configuración de Tests
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/test/setup.ts'],
testMatch: [
'/src/**/__tests__/**/*.{js,jsx,ts,tsx}',
'/src/**/*.{test,spec}.{js,jsx,ts,tsx}'
],
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts',
'!src/test/**',
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
Tests de Integración
// src/test/integration/api.test.ts
import { createMocks } from 'node-mocks-http';
import handler from '@/pages/api/users';
import { prisma } from '@/lib/prisma';
describe('/api/users', () => {
beforeEach(async () => {
await prisma.user.deleteMany();
});
it('should create a new user', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
email: 'test@example.com',
name: 'Test User',
},
});
await handler(req, res);
expect(res._getStatusCode()).toBe(201);
const user = await prisma.user.findUnique({
where: { email: 'test@example.com' }
});
expect(user).toBeTruthy();
expect(user?.name).toBe('Test User');
});
});
Deployment Automatizado
Build y Deploy
build-and-deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login 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=ref,event=branch
type=ref,event=pr
type=sha,prefix={{branch}}-
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Deploy to production
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp -p 3000:3000 ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main
Monitoreo y Alertas
Health Checks
// src/pages/api/health.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { prisma } from '@/lib/prisma';
import { redis } from '@/lib/redis';
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (req.method !== 'GET') {
return res.status(405).json({ error: 'Method not allowed' });
}
const checks = {
database: false,
redis: false,
timestamp: new Date().toISOString(),
};
try {
// Check database
await prisma.$queryRaw`SELECT 1`;
checks.database = true;
} catch (error) {
console.error('Database health check failed:', error);
}
try {
// Check Redis
await redis.ping();
checks.redis = true;
} catch (error) {
console.error('Redis health check failed:', error);
}
const isHealthy = checks.database && checks.redis;
const statusCode = isHealthy ? 200 : 503;
res.status(statusCode).json({
status: isHealthy ? 'healthy' : 'unhealthy',
checks,
});
}
Monitoring con Prometheus
// src/lib/metrics.ts
import { register, Counter, Histogram, Gauge } from 'prom-client';
export const httpRequestsTotal = new Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});
export const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route'],
buckets: [0.1, 0.5, 1, 2, 5],
});
export const activeConnections = new Gauge({
name: 'active_connections',
help: 'Number of active connections',
});
// Middleware para métricas
export function metricsMiddleware(req: any, res: any, next: any) {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
httpRequestsTotal
.labels(req.method, req.route?.path || req.url, res.statusCode)
.inc();
httpRequestDuration
.labels(req.method, req.route?.path || req.url)
.observe(duration);
});
next();
}
Seguridad en CI/CD
Secrets Management
- name: Deploy with secrets
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
run: |
echo "Deploying with secure environment variables"
# Deploy commands here
Security Scanning
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Mejores Prácticas
1. **Fail Fast**: Ejecutar tests rápidos primero
2. **Paralelización**: Ejecutar jobs en paralelo cuando sea posible
3. **Caching**: Usar cache para dependencias y builds
4. **Rollback**: Implementar estrategias de rollback automático
5. **Monitoring**: Monitorear métricas de deployment
Conclusión
Un pipeline de CI/CD bien implementado es la base de un desarrollo ágil y confiable. Invierte tiempo en configurarlo correctamente desde el inicio.