Nội dung

DaiPhan

DaiPhan

Full-Stack Developer

Full-stack developer passionate about modern web technologies, best practices, and sharing knowledge with the community.

Skills & Expertise

JavaScript TypeScript React Node.js DevOps
150+
Articles
50k+
Readers
4.9
Rating

DevOps CI/CD Pipeline: Từ Code Đến Production

Xây dựng CI/CD pipeline hoàn chỉnh với GitHub Actions và Docker

DevOps CI/CD Pipeline: Từ Code Đến Production

CI/CD pipeline là backbone của modern software development. Một pipeline well-designed sẽ giúp bạn deliver features nhanh chóng và reliably.

CI/CD Overview

Pipeline Stages

Developer → Local Development → Push Code → CI/CD Pipeline → Production
    ↓              ↓                  ↓              ↓            ↓
Write Code → Run Tests Locally → Trigger Build → Deploy → Monitor

Benefits

✅ Faster Time to Market
✅ Reduced Manual Errors
✅ Consistent Quality
✅ Faster Feedback Loop
✅ Easier Rollbacks
✅ Better Collaboration

GitHub Actions Setup

Basic Workflow

# .github/workflows/ci.yml
name: CI/CD Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test
    
    - name: Run linting
      run: npm run lint
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage/lcov.info

Advanced Workflow

# .github/workflows/full-pipeline.yml
name: Full CI/CD Pipeline

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

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

jobs:
  # Code Quality
  quality:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
      with:
        fetch-depth: 0
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run tests
      run: npm test -- --coverage
    
    - name: Run linting
      run: npm run lint
    
    - name: Run type checking
      run: npm run type-check
    
    - name: SonarCloud Scan
      uses: SonarSource/sonarcloud-github-action@master
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
    
    - name: Upload test results
      uses: actions/upload-artifact@v3
      if: always()
      with:
        name: test-results
        path: |
          coverage/
          test-results/

  # Security Scanning
  security:
    runs-on: ubuntu-latest
    needs: quality
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      run: npm ci
    
    - name: Run security audit
      run: npm audit --audit-level high
    
    - name: OWASP Dependency Check
      uses: dependency-check/Dependency-Check_Action@main
      with:
        project: 'my-app'
        path: '.'
        format: 'HTML'
    
    - name: Upload security results
      uses: actions/upload-artifact@v3
      with:
        name: security-report
        path: reports/

  # Build và Package
  build:
    runs-on: ubuntu-latest
    needs: [quality, security]
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
    - uses: actions/checkout@v3
    
    - name: Setup Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Log in to Container Registry
      uses: docker/login-action@v2
      with:
        registry: ${{ env.REGISTRY }}
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}
    
    - name: Extract metadata
      id: meta
      uses: docker/metadata-action@v4
      with:
        images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
        tags: |
          type=ref,event=branch
          type=ref,event=pr
          type=semver,pattern={{version}}
          type=semver,pattern={{major}}.{{minor}}
          type=sha
    
    - name: Build and push Docker image
      id: build
      uses: docker/build-push-action@v4
      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: Generate SBOM
      uses: anchore/sbom-action@v0
      with:
        image: ${{ steps.meta.outputs.tags }}
        format: spdx-json
        output-file: sbom.spdx.json

  # Deploy to Staging
  deploy-staging:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    environment: staging
    steps:
    - uses: actions/checkout@v3
    
    - name: Deploy to staging
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.STAGING_HOST }}
        username: ${{ secrets.STAGING_USER }}
        key: ${{ secrets.STAGING_SSH_KEY }}
        script: |
          docker pull ${{ needs.build.outputs.image-tag }}
          docker stop my-app-staging || true
          docker rm my-app-staging || true
          docker run -d \
            --name my-app-staging \
            -p 80:3000 \
            -e NODE_ENV=staging \
            -e DATABASE_URL=${{ secrets.STAGING_DATABASE_URL }} \
            ${{ needs.build.outputs.image-tag }}
    
    - name: Run smoke tests
      run: |
        sleep 30
        curl -f http://${{ secrets.STAGING_HOST }}/health || exit 1
        curl -f http://${{ secrets.STAGING_HOST }}/api/status || exit 1
    
    - name: Run E2E tests
      run: |
        npm install
        npm run test:e2e:staging

  # Deploy to Production
  deploy-production:
    runs-on: ubuntu-latest
    needs: [build, deploy-staging]
    if: github.ref == 'refs/heads/main'
    environment: production
    steps:
    - uses: actions/checkout@v3
    
    - name: Create deployment
      uses: actions/github-script@v6
      with:
        script: |
          const deployment = await github.rest.repos.createDeployment({
            owner: context.repo.owner,
            repo: context.repo.repo,
            ref: context.sha,
            environment: 'production',
            auto_merge: false
          });
          
          core.setOutput('deployment_id', deployment.data.id);
    
    - name: Deploy to production
      uses: appleboy/ssh-action@v0.1.5
      with:
        host: ${{ secrets.PRODUCTION_HOST }}
        username: ${{ secrets.PRODUCTION_USER }}
        key: ${{ secrets.PRODUCTION_SSH_KEY }}
        script: |
          # Blue-green deployment
          docker pull ${{ needs.build.outputs.image-tag }}
          
          # Stop green instances
          docker stop my-app-green || true
          docker rm my-app-green || true
          
          # Start green instances with new version
          docker run -d \
            --name my-app-green \
            -p 3001:3000 \
            -e NODE_ENV=production \
            -e DATABASE_URL=${{ secrets.PRODUCTION_DATABASE_URL }} \
            ${{ needs.build.outputs.image-tag }}
          
          # Health check green instances
          sleep 30
          if curl -f http://localhost:3001/health; then
            # Switch traffic to green
            docker stop my-app-blue || true
            docker rm my-app-blue || true
            docker rename my-app-green my-app-blue
            
            echo "Deployment successful"
          else
            echo "Health check failed, rolling back"
            docker stop my-app-green || true
            docker rm my-app-green || true
            exit 1
          fi
    
    - name: Update deployment status
      if: always()
      uses: actions/github-script@v6
      with:
        script: |
          const deployment_id = '${{ steps.create-deployment.outputs.deployment_id }}';
          const state = '${{ job.status }}' === 'success' ? 'success' : 'failure';
          
          await github.rest.repos.createDeploymentStatus({
            owner: context.repo.owner,
            repo: context.repo.repo,
            deployment_id: deployment_id,
            state: state,
            environment_url: 'https://${{ secrets.PRODUCTION_HOST }}'
          });

Docker Configuration

Multi-stage Dockerfile

# Build stage
FROM node:18-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production && \
    npm cache clean --force

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM node:18-alpine AS production

# Create app directory
WORKDIR /app

# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nextjs -u 1001

# Copy built application
COPY --from=builder --chown=nextjs:nodejs /app/dist ./dist
COPY --from=builder --chown=nextjs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nextjs:nodejs /app/package.json ./package.json

# Switch to non-root user
USER nextjs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD node healthcheck.js

# Start application
CMD ["npm", "start"]

Docker Compose for Development

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      target: development
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
    depends_on:
      - db
      - redis
    
  db:
    image: postgres:15
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
    depends_on:
      - app

volumes:
  postgres_data:

Monitoring và Alerting

Application Monitoring

// monitoring.js
const prometheus = require('prom-client');
const express = require('express');

// Create a Registry to register the metrics
const register = new prometheus.Registry();

// Add a default label which is added to all metrics
register.setDefaultLabels({
  app: 'my-app',
  version: process.env.npm_package_version || '1.0.0'
});

// Enable the collection of default metrics
prometheus.collectDefaultMetrics({ register });

// Create custom metrics
const httpRequestDuration = new prometheus.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10]
});

const httpRequestTotal = new prometheus.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code']
});

register.registerMetric(httpRequestDuration);
register.registerMetric(httpRequestTotal);

// Middleware to collect metrics
function metricsMiddleware(req, res, next) {
  const start = Date.now();
  
  res.on('finish', () => {
    const duration = (Date.now() - start) / 1000;
    const labels = {
      method: req.method,
      route: req.route?.path || req.path,
      status_code: res.statusCode
    };
    
    httpRequestDuration.observe(labels, duration);
    httpRequestTotal.inc(labels);
  });
  
  next();
}

// Health check endpoint
function healthCheck(req, res) {
  const health = {
    status: 'healthy',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
    memory: process.memoryUsage(),
    version: process.env.npm_package_version || '1.0.0'
  };
  
  res.status(200).json(health);
}

// Metrics endpoint
function metricsEndpoint(req, res) {
  res.set('Content-Type', register.contentType);
  res.end(register.metrics());
}

module.exports = {
  metricsMiddleware,
  healthCheck,
  metricsEndpoint,
  register
};

Alerting Rules

# prometheus-alerts.yml
groups:
- name: application-alerts
  rules:
  - alert: HighErrorRate
    expr: rate(http_requests_total{status_code=~"5.."}[5m]) > 0.1
    for: 5m
    labels:
      severity: critical
    annotations:
      summary: "High error rate detected"
      description: "Error rate is above 10% for 5 minutes"
  
  - alert: HighLatency
    expr: histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1
    for: 5m
    labels:
      severity: warning
    annotations:
      summary: "High latency detected"
      description: "95th percentile latency is above 1 second"
  
  - alert: ServiceDown
    expr: up == 0
    for: 1m
    labels:
      severity: critical
    annotations:
      summary: "Service is down"
      description: "Service has been down for 1 minute"

Kết luận

Một CI/CD pipeline hiệu quả cần:

  • Automation: Minimize manual intervention
  • Quality Gates: Ensure code quality at every stage
  • Security: Scan for vulnerabilities throughout
  • Monitoring: Track performance and health
  • Rollback: Quick recovery from failures

Bài viết này là phần đầu tiên trong series về DevOps. Trong các bài tiếp theo, chúng ta sẽ explore Infrastructure as Code và Kubernetes deployment.