CI/CD Pipeline Setup - Complete Guide
Published: September 25, 2024 | Reading time: 25 minutes
CI/CD Pipeline Overview
Continuous Integration and Continuous Deployment automate software delivery:
CI/CD Benefits
# Key Benefits
- Automated testing
- Faster deployments
- Reduced human error
- Consistent environments
- Quick feedback loops
- Rollback capabilities
- Team collaboration
GitHub Actions Setup
Basic Workflow
Node.js CI/CD Pipeline
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
NODE_VERSION: '18'
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm test
- name: Run security audit
run: npm audit --audit-level moderate
- name: Generate coverage report
run: npm run test:coverage
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
with:
file: ./coverage/lcov.info
build:
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login 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=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
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
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- name: Deploy to production
uses: appleboy/ssh-action@v0.1.5
with:
host: ${{ secrets.HOST }}
username: ${{ secrets.USERNAME }}
key: ${{ secrets.SSH_KEY }}
script: |
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
docker-compose down
docker-compose up -d
docker system prune -f
Advanced GitHub Actions
Multi-Environment Deployment
# .github/workflows/deploy.yml
name: Multi-Environment Deployment
on:
push:
branches: [ main, staging, develop ]
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
steps:
- name: Checkout code
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: Build application
run: npm run build
env:
NODE_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
- name: Deploy to staging
if: github.ref == 'refs/heads/develop'
run: |
echo "Deploying to staging environment"
# Add staging deployment commands
- name: Deploy to production
if: github.ref == 'refs/heads/main'
run: |
echo "Deploying to production environment"
# Add production deployment commands
- name: Notify deployment
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
channel: '#deployments'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
Jenkins Setup
Jenkins Pipeline
Jenkinsfile
// Jenkinsfile
pipeline {
agent any
environment {
NODE_VERSION = '18'
DOCKER_REGISTRY = 'your-registry.com'
IMAGE_NAME = 'myapp'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Install Dependencies') {
steps {
sh 'npm ci'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
stage('Test') {
steps {
sh 'npm test'
}
post {
always {
publishTestResults testResultsPattern: 'test-results.xml'
publishCoverage adapters: [coberturaAdapter('coverage/cobertura-coverage.xml')]
}
}
}
stage('Security Scan') {
steps {
sh 'npm audit --audit-level moderate'
}
}
stage('Build Docker Image') {
steps {
script {
def image = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${env.BUILD_NUMBER}")
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
image.push()
image.push('latest')
}
}
}
}
stage('Deploy to Staging') {
when {
branch 'develop'
}
steps {
sh '''
docker pull ${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
docker-compose -f docker-compose.staging.yml up -d
'''
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
steps {
input message: 'Deploy to production?', ok: 'Deploy'
sh '''
docker pull ${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}
docker-compose -f docker-compose.prod.yml up -d
'''
}
}
}
post {
always {
cleanWs()
}
success {
slackSend channel: '#deployments', message: "✅ Deployment successful: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
}
failure {
slackSend channel: '#deployments', message: "⌠Deployment failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}"
}
}
}
GitLab CI/CD
GitLab Pipeline
.gitlab-ci.yml
# .gitlab-ci.yml
stages:
- test
- build
- deploy
variables:
NODE_VERSION: "18"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
# Test stage
test:
stage: test
image: node:18-alpine
before_script:
- npm ci
script:
- npm run lint
- npm test
- npm run test:coverage
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- coverage/
expire_in: 1 week
# Security scan
security_scan:
stage: test
image: node:18-alpine
script:
- npm audit --audit-level moderate
allow_failure: true
# Build Docker image
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker build -t $CI_REGISTRY_IMAGE:latest .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
- docker push $CI_REGISTRY_IMAGE:latest
only:
- main
- develop
# Deploy to staging
deploy_staging:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $STAGING_HOST >> ~/.ssh/known_hosts
script:
- ssh $STAGING_USER@$STAGING_HOST "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
- ssh $STAGING_USER@$STAGING_HOST "docker-compose -f docker-compose.staging.yml up -d"
environment:
name: staging
url: https://staging.example.com
only:
- develop
# Deploy to production
deploy_production:
stage: deploy
image: alpine:latest
before_script:
- apk add --no-cache openssh-client
- eval $(ssh-agent -s)
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
- mkdir -p ~/.ssh
- chmod 700 ~/.ssh
- ssh-keyscan $PRODUCTION_HOST >> ~/.ssh/known_hosts
script:
- ssh $PRODUCTION_USER@$PRODUCTION_HOST "docker pull $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
- ssh $PRODUCTION_USER@$PRODUCTION_HOST "docker-compose -f docker-compose.prod.yml up -d"
environment:
name: production
url: https://example.com
only:
- main
when: manual
Testing Strategies
Test Automation
Testing Pipeline
# Test configuration
# package.json
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"test:e2e": "cypress run",
"test:integration": "jest --config jest.integration.config.js",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"type-check": "tsc --noEmit"
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,ts}",
"!src/**/*.d.ts",
"!src/**/*.test.{js,ts}",
"!src/**/*.spec.{js,ts}"
],
"coverageThreshold": {
"global": {
"branches": 80,
"functions": 80,
"lines": 80,
"statements": 80
}
}
}
}
# Jest configuration
# jest.config.js
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
roots: ['/src', '/tests'],
testMatch: [
'**/__tests__/**/*.+(ts|tsx|js)',
'**/*.(test|spec).+(ts|tsx|js)'
],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest'
},
collectCoverageFrom: [
'src/**/*.{ts,tsx}',
'!src/**/*.d.ts'
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov', 'html']
};
# Cypress E2E tests
# cypress.config.js
const { defineConfig } = require('cypress');
module.exports = defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
supportFile: 'cypress/support/e2e.js',
specPattern: 'cypress/e2e/**/*.cy.{js,jsx,ts,tsx}',
video: true,
screenshotOnRunFailure: true
}
});
Deployment Strategies
Blue-Green Deployment
Blue-Green Script
#!/bin/bash
# blue-green-deploy.sh
set -e
APP_NAME="myapp"
BLUE_PORT="3000"
GREEN_PORT="3001"
NGINX_CONFIG="/etc/nginx/sites-available/myapp"
# Function to check if service is healthy
check_health() {
local port=$1
local max_attempts=30
local attempt=1
while [ $attempt -le $max_attempts ]; do
if curl -f http://localhost:$port/health > /dev/null 2>&1; then
echo "Service on port $port is healthy"
return 0
fi
echo "Attempt $attempt: Service on port $port not ready yet..."
sleep 2
attempt=$((attempt + 1))
done
echo "Service on port $port failed health check"
return 1
}
# Function to get current active color
get_active_color() {
if grep -q "proxy_pass http://localhost:$BLUE_PORT" $NGINX_CONFIG; then
echo "blue"
else
echo "green"
fi
}
# Function to switch traffic
switch_traffic() {
local target_port=$1
local target_color=$2
echo "Switching traffic to $target_color (port $target_port)"
# Update nginx configuration
sed -i "s/proxy_pass http:\/\/localhost:[0-9]*/proxy_pass http:\/\/localhost:$target_port/" $NGINX_CONFIG
# Test nginx configuration
nginx -t
# Reload nginx
systemctl reload nginx
echo "Traffic switched to $target_color"
}
# Main deployment logic
deploy() {
local current_color=$(get_active_color)
local new_color
if [ "$current_color" = "blue" ]; then
new_color="green"
new_port=$GREEN_PORT
old_port=$BLUE_PORT
else
new_color="blue"
new_port=$BLUE_PORT
old_port=$GREEN_PORT
fi
echo "Current active: $current_color, Deploying to: $new_color"
# Deploy new version
echo "Deploying new version to $new_color..."
docker run -d --name ${APP_NAME}-${new_color} -p $new_port:3000 ${APP_NAME}:latest
# Wait for service to be healthy
if check_health $new_port; then
echo "New version is healthy, switching traffic..."
switch_traffic $new_port $new_color
# Wait a bit, then stop old version
sleep 10
echo "Stopping old version ($current_color)..."
docker stop ${APP_NAME}-${current_color} || true
docker rm ${APP_NAME}-${current_color} || true
echo "Deployment completed successfully"
else
echo "New version failed health check, rolling back..."
docker stop ${APP_NAME}-${new_color} || true
docker rm ${APP_NAME}-${new_color} || true
exit 1
fi
}
# Run deployment
deploy
Monitoring and Alerting
Pipeline Monitoring
Monitoring Setup
# GitHub Actions with monitoring
- name: Deploy with monitoring
run: |
# Deploy application
docker-compose up -d
# Wait for deployment
sleep 30
# Health check
if curl -f http://localhost:3000/health; then
echo "Deployment successful"
# Send success notification
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"✅ Deployment successful: '${{ github.sha }}'"}' \
${{ secrets.SLACK_WEBHOOK }}
else
echo "Deployment failed"
# Send failure notification
curl -X POST -H 'Content-type: application/json' \
--data '{"text":"⌠Deployment failed: '${{ github.sha }}'"}' \
${{ secrets.SLACK_WEBHOOK }}
# Rollback
docker-compose down
docker-compose -f docker-compose.previous.yml up -d
exit 1
fi
# Prometheus monitoring
# prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'node-app'
static_configs:
- targets: ['localhost:3000']
metrics_path: '/metrics'
scrape_interval: 5s
- job_name: 'nginx'
static_configs:
- targets: ['localhost:9113']
# Grafana dashboard for CI/CD metrics
# dashboard.json
{
"dashboard": {
"title": "CI/CD Pipeline Metrics",
"panels": [
{
"title": "Deployment Success Rate",
"type": "stat",
"targets": [
{
"expr": "rate(deployment_success_total[5m]) / rate(deployment_total[5m]) * 100"
}
]
},
{
"title": "Build Duration",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(build_duration_seconds_bucket[5m]))"
}
]
}
]
}
}
Security in CI/CD
Security Scanning
Security Pipeline
# Security scanning workflow
name: Security Scan
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- 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'
- name: Run Snyk security scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: --severity-threshold=high
- name: Run CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
languages: javascript
- name: Run OWASP ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'http://localhost:3000'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
# Docker security scanning
# Dockerfile with security best practices
FROM node:18-alpine
# Install security updates
RUN apk update && apk upgrade
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies with security audit
RUN npm ci --only=production && npm audit --audit-level moderate
# Copy application code
COPY --chown=nextjs:nodejs . .
# Switch to non-root user
USER nextjs
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start application
CMD ["node", "index.js"]
Best Practices
CI/CD Checklist
Pipeline Design
- Fast feedback loops
- Parallel job execution
- Fail fast on errors
- Comprehensive testing
- Security scanning
- Environment parity
- Rollback capabilities
Deployment Strategy
- Blue-green deployments
- Canary releases
- Feature flags
- Database migrations
- Health checks
- Monitoring alerts
- Automated rollbacks
Summary
CI/CD pipeline setup involves several key components:
- Platform Selection: GitHub Actions, Jenkins, GitLab CI
- Testing Strategy: Unit, integration, E2E tests
- Build Process: Docker images, artifacts
- Deployment: Blue-green, canary, rolling updates
- Security: Vulnerability scanning, secrets management
- Monitoring: Health checks, alerts, metrics
- Best Practices: Fast feedback, environment parity
Need More Help?
Struggling with CI/CD pipeline setup or need help automating your deployment process? Our DevOps experts can help you implement robust CI/CD solutions.
Get CI/CD Help