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.