CI/CD Pipelines: Complete Setup
Build robust CI/CD pipelines using GitHub Actions, Jenkins, and GitLab CI for automated deployment. Master continuous integration and deployment strategies.
CI/CD Pipelines: Complete Setup
Continuous Integration and Continuous Deployment (CI/CD) pipelines automate the process of building, testing, and deploying applications. This comprehensive guide covers setting up robust CI/CD pipelines using popular tools and platforms.
CI/CD Fundamentals
Core Concepts
Continuous Integration (CI)
- Definition: Automated building and testing of code changes
- Benefits: Early bug detection, faster feedback, reduced integration issues
- Process: Code commit → Build → Test → Package
- Tools: Jenkins, GitHub Actions, GitLab CI, CircleCI
Continuous Deployment (CD)
- Definition: Automated deployment of tested code to production
- Benefits: Faster releases, reduced deployment risk, consistent environments
- Process: Package → Deploy → Monitor → Rollback (if needed)
- Tools: Kubernetes, Docker, AWS, Azure, GCP
Pipeline Stages
Source Stage
- Code Repository: Git-based version control
- Branch Strategy: Feature branches, main branch protection
- Trigger Events: Push, pull request, scheduled builds
- Code Quality: Static analysis, security scanning
Build Stage
- Environment Setup: Dependencies, runtime environments
- Compilation: Source code compilation and packaging
- Artifact Creation: Build outputs, container images
- Quality Gates: Code coverage, test results
Test Stage
- Unit Tests: Individual component testing
- Integration Tests: Component interaction testing
- End-to-End Tests: Full application testing
- Performance Tests: Load and stress testing
Deploy Stage
- Environment Promotion: Dev → Staging → Production
- Configuration Management: Environment-specific settings
- Database Migrations: Schema and data updates
- Health Checks: Application and service validation
GitHub Actions
Basic Workflow
Workflow Configuration
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: Build application
run: npm run build
Multi-Environment Deployment
name: Deploy to Environments
on:
push:
branches: [ main, develop ]
jobs:
deploy-staging:
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v3
- name: Deploy to Staging
run: |
echo "Deploying to staging environment"
# Add your deployment commands here
deploy-production:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production
needs: deploy-staging
steps:
- uses: actions/checkout@v3
- name: Deploy to Production
run: |
echo "Deploying to production environment"
# Add your deployment commands here
Advanced GitHub Actions
Matrix Builds
name: Matrix Build
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [16, 18, 20]
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v3
- name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
Docker Build and Push
name: Build and Push Docker Image
on:
push:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- 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: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
Jenkins Pipeline
Jenkinsfile Setup
Declarative Pipeline
pipeline {
agent any
environment {
NODE_VERSION = '18'
DOCKER_REGISTRY = 'your-registry.com'
IMAGE_NAME = 'my-app'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
}
}
stage('Test') {
steps {
sh 'npm test'
sh 'npm run lint'
}
post {
always {
publishTestResults testResultsPattern: 'test-results.xml'
}
}
}
stage('Docker Build') {
steps {
script {
def image = docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}")
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-registry-credentials') {
image.push()
image.push('latest')
}
}
}
}
stage('Deploy') {
when {
branch 'main'
}
steps {
sh 'kubectl apply -f k8s/'
sh 'kubectl rollout status deployment/my-app'
}
}
}
post {
always {
cleanWs()
}
failure {
mail to: 'team@company.com',
subject: "Build Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "Build failed. Check console output for details."
}
}
}
Scripted Pipeline
node {
stage('Checkout') {
checkout scm
}
stage('Build') {
def buildTool = tool name: 'NodeJS-18', type: 'nodejs'
env.PATH = "${buildTool}/bin:${env.PATH}"
sh 'npm ci'
sh 'npm run build'
}
stage('Test') {
sh 'npm test'
sh 'npm run lint'
publishTestResults testResultsPattern: 'test-results.xml'
publishHTML([
allowMissing: false,
alwaysLinkToLastBuild: true,
keepAll: true,
reportDir: 'coverage',
reportFiles: 'index.html',
reportName: 'Coverage Report'
])
}
stage('Docker Build') {
def image = docker.build("my-app:${BUILD_NUMBER}")
docker.withRegistry('https://your-registry.com', 'docker-registry-credentials') {
image.push()
image.push('latest')
}
}
stage('Deploy') {
if (env.BRANCH_NAME == 'main') {
sh 'kubectl apply -f k8s/'
sh 'kubectl rollout status deployment/my-app'
}
}
}
Jenkins Configuration
Global Tool Configuration
// Global Tools Configuration
tools {
nodejs 'NodeJS-18'
dockerTool 'Docker'
git 'Git'
}
// Global Pipeline Libraries
libraries {
lib('shared-library@main')
}
Credentials Management
pipeline {
agent any
stages {
stage('Deploy') {
steps {
script {
withCredentials([
string(credentialsId: 'kubeconfig', variable: 'KUBECONFIG'),
string(credentialsId: 'docker-registry', variable: 'DOCKER_REGISTRY')
]) {
sh 'kubectl apply -f k8s/'
}
}
}
}
}
}
GitLab CI/CD
GitLab CI Configuration
Basic Pipeline
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
NODE_VERSION: "18"
DOCKER_IMAGE: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
build:
stage: build
image: node:18
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 hour
test:
stage: test
image: node:18
script:
- npm ci
- npm test
- npm run lint
coverage: '/Lines\s*:\s*(\d+\.\d+)%/'
artifacts:
reports:
junit: test-results.xml
paths:
- coverage/
expire_in: 1 week
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
script:
- docker build -t $DOCKER_IMAGE .
- docker push $DOCKER_IMAGE
only:
- main
- develop
deploy-staging:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl apply -f k8s/
- kubectl rollout status deployment/my-app
environment:
name: staging
url: https://staging.myapp.com
only:
- develop
deploy-production:
stage: deploy
image: bitnami/kubectl:latest
script:
- kubectl apply -f k8s/
- kubectl rollout status deployment/my-app
environment:
name: production
url: https://myapp.com
when: manual
only:
- main
Advanced GitLab CI
Multi-Project Pipeline
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build-frontend:
stage: build
script:
- cd frontend
- npm ci
- npm run build
artifacts:
paths:
- frontend/dist/
build-backend:
stage: build
script:
- cd backend
- npm ci
- npm run build
artifacts:
paths:
- backend/dist/
test-frontend:
stage: test
script:
- cd frontend
- npm ci
- npm test
dependencies:
- build-frontend
test-backend:
stage: test
script:
- cd backend
- npm ci
- npm test
dependencies:
- build-backend
deploy:
stage: deploy
script:
- echo "Deploying application"
dependencies:
- test-frontend
- test-backend
Docker Integration
Multi-Stage Dockerfile
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Development stage
FROM node:18-alpine AS development
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Production stage
FROM node:18-alpine AS production
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
COPY --from=builder /app/node_modules ./node_modules
COPY --chown=nextjs:nodejs . .
USER nextjs
EXPOSE 3000
CMD ["npm", "start"]
Docker Compose for CI
# docker-compose.ci.yml
version: '3.8'
services:
app:
build:
context: .
target: development
ports:
- "3000:3000"
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://test:test@db:5432/testdb
depends_on:
- db
- redis
db:
image: postgres:15
environment:
- POSTGRES_DB=testdb
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
test:
build:
context: .
target: development
command: npm test
environment:
- NODE_ENV=test
- DATABASE_URL=postgresql://test:test@db:5432/testdb
depends_on:
- db
- redis
Kubernetes Deployment
Deployment Manifests
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
labels:
app: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-app
image: my-registry.com/my-app:latest
ports:
- containerPort: 3000
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: my-app-secrets
key: database-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 3000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 3000
initialDelaySeconds: 5
periodSeconds: 5
Service and Ingress
# k8s/service.yaml
apiVersion: v1
kind: Service
metadata:
name: my-app-service
spec:
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 3000
type: ClusterIP
---
# k8s/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: myapp.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-app-service
port:
number: 80
Monitoring and Alerting
Health Checks
# health-check.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: health-check-script
data:
health-check.sh: |
#!/bin/bash
if curl -f http://localhost:3000/health; then
echo "Health check passed"
exit 0
else
echo "Health check failed"
exit 1
fi
Prometheus Monitoring
# monitoring.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: prometheus-config
data:
prometheus.yml: |
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'my-app'
static_configs:
- targets: ['my-app-service:80']
metrics_path: /metrics
scrape_interval: 30s
Security Best Practices
Secrets Management
# secrets.yaml
apiVersion: v1
kind: Secret
metadata:
name: my-app-secrets
type: Opaque
data:
database-url: <base64-encoded-url>
api-key: <base64-encoded-key>
Security Scanning
# security-scan.yml
name: Security Scan
on: [push, pull_request]
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- 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'
Conclusion
CI/CD pipelines are essential for modern software development, enabling teams to deliver high-quality software faster and more reliably. By implementing the strategies and tools covered in this guide, you can build robust, automated pipelines that improve your development workflow and reduce deployment risks.
The key to successful CI/CD implementation is starting simple and gradually adding complexity as your team becomes more comfortable with the tools and processes. With proper planning and execution, CI/CD pipelines can significantly improve your software delivery capabilities.