CI/CD Patterns
A comprehensive guide to Continuous Integration and Continuous Deployment patterns, branching strategies, and deployment techniques for modern software delivery.
Overview
CI/CD (Continuous Integration/Continuous Deployment) patterns define how code flows from development to production. These patterns establish workflows for integrating changes, automating tests, managing artifacts, and deploying applications safely and reliably.
flowchart LR
subgraph CI ["Continuous Integration"]
A[Code Commit] --> B[Build]
B --> C[Unit Tests]
C --> D[Integration Tests]
D --> E[Code Analysis]
end
subgraph CD ["Continuous Deployment"]
E --> F[Artifact Creation]
F --> G[Deploy to Staging]
G --> H[Acceptance Tests]
H --> I[Deploy to Production]
end
I --> J[Monitoring]
J --> K[Feedback]
K --> A
Trunk-Based Development
Key Concepts
| Concept | Description |
|---|---|
| Single Branch | All developers commit to a single main branch (trunk) |
| Short-Lived Branches | Feature branches last hours to days, not weeks |
| Feature Flags | Toggle incomplete features off in production |
| Continuous Integration | Multiple integrations per day per developer |
| Release Branches | Optional short-lived branches for releases |
Common Patterns
gitGraph
commit id: "main-1"
branch feature-a
commit id: "feat-1"
checkout main
merge feature-a
commit id: "main-2"
branch feature-b
commit id: "feat-2"
commit id: "feat-3"
checkout main
merge feature-b
commit id: "main-3"
branch release-1.0
commit id: "rel-1" tag: "v1.0"
Examples
Feature Flag Implementation:
# Feature flag configuration
features:
new_checkout_flow:
enabled: false
rollout_percentage: 0
allowed_users:
- beta_testers
enhanced_search:
enabled: true
rollout_percentage: 100
Branch Protection Rules:
# GitHub branch protection
branches:
- name: main
protection:
required_status_checks:
strict: true
contexts:
- build
- test
- lint
required_pull_request_reviews:
required_approving_review_count: 1
enforce_admins: true
Trunk-Based CI Pipeline:
# .gitlab-ci.yml
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
test:
stage: test
script:
- npm run test:unit
- npm run test:integration
coverage: '/Coverage: \d+\.\d+%/'
deploy:
stage: deploy
script:
- ./deploy.sh
only:
- main
when: manual
GitFlow and GitHub Flow
Key Concepts
| Strategy | Branches | Best For |
|---|---|---|
| GitFlow | main, develop, feature/, release/, hotfix/* | Scheduled releases, multiple versions |
| GitHub Flow | main, feature/* | Continuous deployment, web apps |
| GitLab Flow | main, feature/*, environment branches | Environment-based deployments |
GitFlow Diagram
gitGraph
commit id: "init"
branch develop
commit id: "dev-1"
branch feature/login
commit id: "login-1"
commit id: "login-2"
checkout develop
merge feature/login
branch release/1.0
commit id: "rel-prep"
checkout main
merge release/1.0 tag: "v1.0"
checkout develop
merge release/1.0
commit id: "dev-2"
checkout main
branch hotfix/1.0.1
commit id: "fix-1"
checkout main
merge hotfix/1.0.1 tag: "v1.0.1"
checkout develop
merge hotfix/1.0.1
GitHub Flow Diagram
gitGraph
commit id: "main-1"
branch feature/new-api
commit id: "api-1"
commit id: "api-2"
checkout main
merge feature/new-api
commit id: "main-2" tag: "deploy"
branch feature/ui-update
commit id: "ui-1"
checkout main
merge feature/ui-update
commit id: "main-3" tag: "deploy"
Examples
GitFlow Branch Commands:
# Start a new feature
git checkout develop
git checkout -b feature/user-authentication
# Finish feature
git checkout develop
git merge --no-ff feature/user-authentication
git branch -d feature/user-authentication
# Start a release
git checkout develop
git checkout -b release/1.2.0
# Finish release
git checkout main
git merge --no-ff release/1.2.0
git tag -a v1.2.0 -m "Release 1.2.0"
git checkout develop
git merge --no-ff release/1.2.0
git branch -d release/1.2.0
# Hotfix
git checkout main
git checkout -b hotfix/1.2.1
# ... fix the issue ...
git checkout main
git merge --no-ff hotfix/1.2.1
git tag -a v1.2.1 -m "Hotfix 1.2.1"
git checkout develop
git merge --no-ff hotfix/1.2.1
git branch -d hotfix/1.2.1
GitHub Flow Workflow:
# .github/workflows/github-flow.yml
name: GitHub Flow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: npm ci && npm run build
- name: Test
run: npm test
deploy:
needs: build-and-test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Pipeline Stages (Build, Test, Deploy)
Key Concepts
| Stage | Purpose | Activities |
|---|---|---|
| Build | Compile and package | Dependency resolution, compilation, asset bundling |
| Test | Verify quality | Unit tests, integration tests, security scans |
| Deploy | Release to environment | Configuration, deployment, smoke tests |
Pipeline Architecture
flowchart TD
subgraph Build ["Build Stage"]
B1[Checkout Code] --> B2[Install Dependencies]
B2 --> B3[Compile/Transpile]
B3 --> B4[Create Artifacts]
end
subgraph Test ["Test Stage"]
T1[Unit Tests] --> T2[Integration Tests]
T2 --> T3[E2E Tests]
T3 --> T4[Security Scan]
T4 --> T5[Code Quality]
end
subgraph Deploy ["Deploy Stage"]
D1[Deploy to Dev] --> D2[Smoke Tests]
D2 --> D3[Deploy to Staging]
D3 --> D4[UAT]
D4 --> D5[Deploy to Prod]
end
Build --> Test
Test --> Deploy
Examples
Multi-Stage Jenkins Pipeline:
// Jenkinsfile
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
APP_NAME = 'myapp'
}
stages {
stage('Build') {
steps {
sh 'npm ci'
sh 'npm run build'
sh "docker build -t ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} ."
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'npm run test:unit'
}
}
stage('Integration Tests') {
steps {
sh 'npm run test:integration'
}
}
stage('Security Scan') {
steps {
sh 'npm audit --audit-level=high'
sh 'trivy image ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}'
}
}
}
}
stage('Push Artifact') {
steps {
sh "docker push ${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER}"
}
}
stage('Deploy to Staging') {
steps {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} -n staging"
}
}
stage('Deploy to Production') {
when {
branch 'main'
}
input {
message "Deploy to production?"
ok "Deploy"
}
steps {
sh "kubectl set image deployment/${APP_NAME} ${APP_NAME}=${DOCKER_REGISTRY}/${APP_NAME}:${BUILD_NUMBER} -n production"
}
}
}
post {
always {
junit '**/test-results/*.xml'
archiveArtifacts artifacts: 'dist/**/*', fingerprint: true
}
failure {
slackSend channel: '#deployments', color: 'danger', message: "Build failed: ${env.JOB_NAME} ${env.BUILD_NUMBER}"
}
}
}
GitHub Actions Multi-Stage Pipeline:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set version
id: version
run: echo "version=$(date +%Y%m%d)-${GITHUB_SHA::8}" >> $GITHUB_OUTPUT
- name: Build application
run: |
npm ci
npm run build
- name: Build Docker image
run: docker build -t myapp:${{ steps.version.outputs.version }} .
- name: Save Docker image
run: docker save myapp:${{ steps.version.outputs.version }} > myapp.tar
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: docker-image
path: myapp.tar
test:
needs: build
runs-on: ubuntu-latest
strategy:
matrix:
test-type: [unit, integration, e2e]
steps:
- uses: actions/checkout@v4
- name: Run ${{ matrix.test-type }} tests
run: npm run test:${{ matrix.test-type }}
security-scan:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run security audit
run: npm audit --audit-level=high
- name: Run SAST
uses: github/codeql-action/analyze@v2
deploy-staging:
needs: [test, security-scan]
runs-on: ubuntu-latest
environment: staging
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: docker-image
- name: Deploy to staging
run: ./scripts/deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: Download artifact
uses: actions/download-artifact@v4
with:
name: docker-image
- name: Deploy to production
run: ./scripts/deploy.sh production
Artifact Management
Key Concepts
| Concept | Description |
|---|---|
| Immutable Artifacts | Build once, deploy many times |
| Versioning | Semantic versioning or build numbers |
| Retention Policies | Automatic cleanup of old artifacts |
| Checksums | Verify artifact integrity |
| Metadata | Track build info, dependencies, provenance |
Artifact Flow
flowchart LR
subgraph Build ["Build Environment"]
A[Source Code] --> B[Build Process]
B --> C[Artifact]
end
subgraph Registry ["Artifact Registry"]
C --> D[(Container Registry)]
C --> E[(Package Registry)]
C --> F[(Binary Repository)]
end
subgraph Deploy ["Deployment"]
D --> G[Dev]
D --> H[Staging]
D --> I[Production]
end
Examples
Docker Image Management:
# Build and push with multiple tags
name: Build and Push
on:
push:
branches: [main]
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
Artifact Retention in GitLab:
# .gitlab-ci.yml
build:
stage: build
script:
- npm run build
artifacts:
paths:
- dist/
- coverage/
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
expire_in: 1 week
when: always
release:
stage: release
script:
- npm run build:release
artifacts:
paths:
- dist/
expire_in: never
only:
- tags
Nexus/Artifactory Upload:
#!/bin/bash
# Upload artifact to Nexus
NEXUS_URL="https://nexus.example.com/repository/maven-releases"
GROUP_ID="com.example"
ARTIFACT_ID="myapp"
VERSION="1.2.3"
PACKAGING="jar"
curl -v -u "${NEXUS_USER}:${NEXUS_PASS}" \
--upload-file "target/${ARTIFACT_ID}-${VERSION}.${PACKAGING}" \
"${NEXUS_URL}/${GROUP_ID//.//}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.${PACKAGING}"
# Verify checksum
sha256sum "target/${ARTIFACT_ID}-${VERSION}.${PACKAGING}" > checksum.txt
curl -v -u "${NEXUS_USER}:${NEXUS_PASS}" \
--upload-file checksum.txt \
"${NEXUS_URL}/${GROUP_ID//.//}/${ARTIFACT_ID}/${VERSION}/${ARTIFACT_ID}-${VERSION}.${PACKAGING}.sha256"
Helm Chart Repository:
# Chart.yaml
apiVersion: v2
name: myapp
description: My application Helm chart
type: application
version: 1.2.3
appVersion: "2.0.0"
# Package and push
# helm package ./myapp
# helm push myapp-1.2.3.tgz oci://registry.example.com/charts
Environment Promotion Strategies
Key Concepts
| Strategy | Description | Use Case |
|---|---|---|
| Sequential | Dev → Test → Staging → Prod | Traditional workflow |
| Parallel | Deploy to multiple envs simultaneously | Fast feedback |
| Ring-based | Gradual rollout to user segments | Large-scale systems |
| GitOps | Git as source of truth for env state | Kubernetes deployments |
Promotion Flow
flowchart TD
A[Artifact Created] --> B{Automated Tests}
B -->|Pass| C[Deploy to Dev]
B -->|Fail| Z[Notify Team]
C --> D{Dev Tests}
D -->|Pass| E[Deploy to QA]
D -->|Fail| Z
E --> F{QA Approval}
F -->|Approved| G[Deploy to Staging]
F -->|Rejected| Z
G --> H{UAT & Performance}
H -->|Pass| I{Manual Approval}
H -->|Fail| Z
I -->|Approved| J[Deploy to Production]
I -->|Rejected| Z
J --> K[Monitor & Validate]
Examples
GitOps with ArgoCD:
# Application manifest for ArgoCD
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp-production
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/org/k8s-manifests.git
targetRevision: HEAD
path: environments/production/myapp
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
---
# Promotion script
#!/bin/bash
# promote.sh - Promote image to next environment
SOURCE_ENV=$1
TARGET_ENV=$2
APP=$3
# Get current image from source
IMAGE=$(kubectl get deployment $APP -n $SOURCE_ENV -o jsonpath='{.spec.template.spec.containers[0].image}')
# Update target environment manifests
cd k8s-manifests/environments/$TARGET_ENV/$APP
kustomize edit set image app=$IMAGE
git add .
git commit -m "Promote $APP to $TARGET_ENV: $IMAGE"
git push
echo "Promotion triggered. ArgoCD will sync automatically."
Environment Configuration Management:
# kustomization.yaml for each environment
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- configmap.yaml
# environments/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: staging-
namespace: staging
patches:
- path: replica-patch.yaml
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=debug
- FEATURE_FLAGS=all
# environments/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namePrefix: prod-
namespace: production
patches:
- path: replica-patch.yaml
- path: resources-patch.yaml
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=info
- FEATURE_FLAGS=stable
Spinnaker Pipeline Configuration:
{
"name": "Production Deployment",
"stages": [
{
"type": "bake",
"name": "Bake AMI",
"baseOs": "ubuntu",
"package": "myapp",
"regions": ["eu-west-1"]
},
{
"type": "deploy",
"name": "Deploy to Staging",
"clusters": [{
"account": "staging",
"application": "myapp",
"strategy": "redblack",
"capacity": {"min": 2, "max": 4, "desired": 2}
}]
},
{
"type": "manualJudgment",
"name": "Approve Production",
"instructions": "Verify staging deployment before proceeding"
},
{
"type": "deploy",
"name": "Deploy to Production",
"clusters": [{
"account": "production",
"application": "myapp",
"strategy": "redblack",
"capacity": {"min": 4, "max": 10, "desired": 6}
}]
}
]
}
Rollback Procedures
Key Concepts
| Type | Description | Speed | Risk |
|---|---|---|---|
| Instant Rollback | Switch traffic to previous version | Seconds | Low |
| Redeploy Previous | Deploy previous artifact again | Minutes | Low |
| Database Rollback | Revert schema/data changes | Minutes-Hours | High |
| Feature Flag | Disable feature without deploy | Seconds | Low |
Rollback Decision Flow
flowchart TD
A[Issue Detected] --> B{Severity?}
B -->|Critical| C[Immediate Rollback]
B -->|High| D{Can fix quickly?}
B -->|Medium| E[Investigate]
D -->|Yes| F[Hot Fix]
D -->|No| C
E --> G{Root cause found?}
G -->|Yes| H{Fix complexity?}
G -->|No| I[Feature Flag Off]
H -->|Simple| F
H -->|Complex| I
C --> J[Verify Rollback]
F --> J
I --> J
J --> K[Post-Mortem]
Examples
Kubernetes Rollback:
#!/bin/bash
# rollback.sh - Kubernetes deployment rollback
NAMESPACE=$1
DEPLOYMENT=$2
REVISION=${3:-0} # 0 means previous version
# Check rollout history
kubectl rollout history deployment/$DEPLOYMENT -n $NAMESPACE
# Rollback to previous version
if [ "$REVISION" -eq 0 ]; then
kubectl rollout undo deployment/$DEPLOYMENT -n $NAMESPACE
else
kubectl rollout undo deployment/$DEPLOYMENT -n $NAMESPACE --to-revision=$REVISION
fi
# Wait for rollback to complete
kubectl rollout status deployment/$DEPLOYMENT -n $NAMESPACE --timeout=300s
# Verify pods are healthy
kubectl get pods -n $NAMESPACE -l app=$DEPLOYMENT
echo "Rollback completed successfully"
Blue/Green Rollback with Nginx:
#!/bin/bash
# blue-green-rollback.sh
CURRENT=$(readlink /etc/nginx/sites-enabled/app)
if [[ $CURRENT == *"blue"* ]]; then
NEW="green"
OLD="blue"
else
NEW="blue"
OLD="green"
fi
# Switch to previous version
ln -sfn /etc/nginx/sites-available/app-$NEW /etc/nginx/sites-enabled/app
nginx -t && nginx -s reload
echo "Rolled back from $OLD to $NEW"
Database Rollback with Flyway:
#!/bin/bash
# db-rollback.sh
# Check current version
flyway info
# Rollback last migration (requires undo scripts)
flyway undo
# Or rollback to specific version
# flyway undo -target=5
# Verify
flyway info
Automated Rollback in GitHub Actions:
name: Deploy with Auto-Rollback
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get current version
id: current
run: |
CURRENT=$(kubectl get deployment myapp -o jsonpath='{.spec.template.spec.containers[0].image}')
echo "version=$CURRENT" >> $GITHUB_OUTPUT
- name: Deploy new version
run: |
kubectl set image deployment/myapp myapp=myregistry/myapp:${{ github.sha }}
kubectl rollout status deployment/myapp --timeout=300s
- name: Run smoke tests
id: smoke
continue-on-error: true
run: |
./scripts/smoke-tests.sh
- name: Rollback on failure
if: steps.smoke.outcome == 'failure'
run: |
echo "Smoke tests failed, rolling back..."
kubectl set image deployment/myapp myapp=${{ steps.current.outputs.version }}
kubectl rollout status deployment/myapp --timeout=300s
exit 1
Blue/Green and Canary Deployments
Key Concepts
| Strategy | Description | Traffic Split | Rollback Time |
|---|---|---|---|
| Blue/Green | Two identical environments, instant switch | 0% or 100% | Instant |
| Canary | Gradual traffic shift to new version | Configurable % | Fast |
| Rolling | Replace instances incrementally | Gradual | Moderate |
| A/B Testing | Route based on user attributes | By segment | Fast |
Deployment Strategies Comparison
flowchart TD
subgraph BlueGreen ["Blue/Green Deployment"]
BG1[Blue v1 - Active] --> BG2[Green v2 - Idle]
BG2 --> BG3[Switch Traffic]
BG3 --> BG4[Green v2 - Active]
end
subgraph Canary ["Canary Deployment"]
C1[v1 - 100%] --> C2[v1 90% / v2 10%]
C2 --> C3[v1 50% / v2 50%]
C3 --> C4[v2 - 100%]
end
subgraph Rolling ["Rolling Deployment"]
R1[v1 v1 v1] --> R2[v2 v1 v1]
R2 --> R3[v2 v2 v1]
R3 --> R4[v2 v2 v2]
end
Examples
Blue/Green with Kubernetes Service:
# blue-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-blue
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: blue
template:
metadata:
labels:
app: myapp
version: blue
spec:
containers:
- name: myapp
image: myregistry/myapp:1.0.0
---
# green-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-green
spec:
replicas: 3
selector:
matchLabels:
app: myapp
version: green
template:
metadata:
labels:
app: myapp
version: green
spec:
containers:
- name: myapp
image: myregistry/myapp:2.0.0
---
# service.yaml - Switch by changing selector
apiVersion: v1
kind: Service
metadata:
name: myapp
spec:
selector:
app: myapp
version: blue # Change to 'green' to switch
ports:
- port: 80
targetPort: 8080
Canary with Istio:
# VirtualService for canary routing
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: myapp
spec:
hosts:
- myapp
http:
- match:
- headers:
canary:
exact: "true"
route:
- destination:
host: myapp
subset: canary
- route:
- destination:
host: myapp
subset: stable
weight: 90
- destination:
host: myapp
subset: canary
weight: 10
---
# DestinationRule
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
name: myapp
spec:
host: myapp
subsets:
- name: stable
labels:
version: stable
- name: canary
labels:
version: canary
Argo Rollouts Canary:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp
spec:
replicas: 10
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:2.0.0
strategy:
canary:
steps:
- setWeight: 10
- pause: {duration: 5m}
- setWeight: 30
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 80
- pause: {duration: 5m}
analysis:
templates:
- templateName: success-rate
startingStep: 1
args:
- name: service-name
value: myapp
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.95
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.*"}[5m])) /
sum(rate(http_requests_total{service="{{args.service-name}}"}[5m]))
AWS CodeDeploy Canary:
# appspec.yml
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: "arn:aws:ecs:region:account:task-definition/myapp:2"
LoadBalancerInfo:
ContainerName: "myapp"
ContainerPort: 8080
Hooks:
- BeforeInstall: "scripts/before_install.sh"
- AfterInstall: "scripts/after_install.sh"
- AfterAllowTestTraffic: "scripts/test_traffic.sh"
- AfterAllowTraffic: "scripts/validation.sh"
Best Practices for CI/CD Pipelines
Key Concepts
| Category | Best Practice | Benefit |
|---|---|---|
| Speed | Parallelise tests | Faster feedback |
| Reliability | Idempotent builds | Consistent results |
| Security | Scan dependencies | Prevent vulnerabilities |
| Observability | Comprehensive logging | Easier debugging |
| Efficiency | Cache dependencies | Reduced build time |
Pipeline Design Patterns
flowchart TD
subgraph Patterns ["Best Practice Patterns"]
P1[Fail Fast] --> P2[Run quick tests first]
P3[Cache Everything] --> P4[Dependencies, builds, images]
P5[Parallelise] --> P6[Independent tests run concurrently]
P7[Immutable Artifacts] --> P8[Build once, deploy many]
P9[Infrastructure as Code] --> P10[Version controlled configs]
end
Examples
Optimised Pipeline with Caching:
# .github/workflows/optimised.yml
name: Optimised CI/CD
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
strategy:
matrix:
shard: [1, 2, 3, 4]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test -- --shard=${{ matrix.shard }}/4
build:
needs: [lint, test]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build with cache
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
Security-Focused Pipeline:
name: Secure Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 0 * * *' # Daily security scan
jobs:
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Dependency audit
run: npm audit --audit-level=high
- name: SAST scan
uses: github/codeql-action/analyze@v2
- name: Secret detection
uses: trufflesecurity/trufflehog@main
with:
path: ./
- name: Container scan
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'
Pipeline Observability:
# Datadog CI visibility
name: Observable Pipeline
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Configure Datadog CI
run: |
npm install -g @datadog/datadog-ci
- name: Run tests with tracing
env:
DD_API_KEY: ${{ secrets.DD_API_KEY }}
DD_SITE: datadoghq.eu
run: |
datadog-ci junit upload --service myapp ./test-results/
- name: Tag pipeline
run: |
datadog-ci tag --level pipeline \
--tags "env:ci,version:${{ github.sha }}"
Clean Code Pipeline Configuration:
# Reusable workflow
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
version:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy to ${{ inputs.environment }}
run: |
echo "Deploying version ${{ inputs.version }} to ${{ inputs.environment }}"
./scripts/deploy.sh ${{ inputs.environment }} ${{ inputs.version }}
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
# Caller workflow
# .github/workflows/main.yml
name: Main Pipeline
on:
push:
branches: [main]
jobs:
build:
# ... build steps ...
outputs:
version: ${{ steps.version.outputs.version }}
deploy-staging:
needs: build
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
version: ${{ needs.build.outputs.version }}
secrets:
DEPLOY_TOKEN: ${{ secrets.STAGING_DEPLOY_TOKEN }}
deploy-production:
needs: [build, deploy-staging]
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
version: ${{ needs.build.outputs.version }}
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
Quick Reference
| Pattern | When to Use | Key Benefit |
|---|---|---|
| Trunk-based | Small teams, continuous deployment | Fast integration |
| GitFlow | Scheduled releases, multiple versions | Clear release process |
| GitHub Flow | Simple projects, web apps | Simplicity |
| Blue/Green | Zero-downtime requirements | Instant rollback |
| Canary | Risk mitigation, large user base | Gradual validation |
| Rolling | Resource constraints | Minimal extra resources |
| GitOps | Kubernetes environments | Declarative, auditable |
Common CI/CD Tools
| Category | Tools |
|---|---|
| CI Servers | Jenkins, GitLab CI, GitHub Actions, CircleCI, Travis CI |
| CD/GitOps | ArgoCD, Flux, Spinnaker, Harness |
| Artifact Storage | Nexus, Artifactory, GitHub Packages, ECR, GCR |
| Container Orchestration | Kubernetes, ECS, Cloud Run |
| Feature Flags | LaunchDarkly, Split, Unleash, Flagsmith |
Pipeline Stage Checklist
Build Stage:
[ ] Checkout code
[ ] Install dependencies (cached)
[ ] Compile/transpile
[ ] Create artifacts
[ ] Tag with version
Test Stage:
[ ] Unit tests
[ ] Integration tests
[ ] E2E tests
[ ] Security scan
[ ] Code quality analysis
[ ] Performance tests
Deploy Stage:
[ ] Environment configuration
[ ] Database migrations
[ ] Deploy application
[ ] Smoke tests
[ ] Health checks
[ ] Monitoring verification
Common Issues and Solutions
Pipeline Failures
| Issue | Cause | Solution |
|---|---|---|
| Flaky tests | Race conditions, external dependencies | Isolate tests, use mocks, retry logic |
| Slow builds | No caching, sequential execution | Enable caching, parallelise stages |
| OOM errors | Insufficient resources | Increase memory, optimise builds |
| Timeout failures | Long-running processes | Increase timeout, split into stages |
| Dependency conflicts | Version mismatches | Lock file, consistent environments |
Deployment Failures
| Issue | Cause | Solution |
|---|---|---|
| Failed health checks | Misconfigured probes | Adjust probe timing, check endpoints |
| Database migration errors | Schema conflicts | Test migrations, use versioning |
| Secret not found | Missing environment config | Verify secret exists in target env |
| Insufficient permissions | RBAC misconfiguration | Review service account permissions |
| Resource quota exceeded | Cluster limits | Request quota increase, optimise resources |
Rollback Issues
| Issue | Cause | Solution |
|---|---|---|
| Cannot rollback DB | No backward migrations | Always create undo scripts |
| Cached old version | CDN/browser cache | Cache invalidation strategy |
| Stale connections | Connection pooling | Graceful shutdown, connection draining |
| Lost transactions | Mid-flight requests | Queue-based processing, retry logic |
Example Troubleshooting
Debugging Failed Deployments:
#!/bin/bash
# diagnose-deployment.sh
NAMESPACE=$1
DEPLOYMENT=$2
echo "=== Deployment Status ==="
kubectl get deployment $DEPLOYMENT -n $NAMESPACE
echo "=== Recent Events ==="
kubectl get events -n $NAMESPACE --sort-by='.lastTimestamp' | grep $DEPLOYMENT
echo "=== Pod Status ==="
kubectl get pods -n $NAMESPACE -l app=$DEPLOYMENT
echo "=== Pod Logs ==="
for pod in $(kubectl get pods -n $NAMESPACE -l app=$DEPLOYMENT -o name); do
echo "--- $pod ---"
kubectl logs $pod -n $NAMESPACE --tail=50
done
echo "=== Describe Deployment ==="
kubectl describe deployment $DEPLOYMENT -n $NAMESPACE
Handling Flaky Tests:
# Jest configuration for retry
module.exports = {
// Retry failed tests
testRetries: 2,
// Increase timeout for slow tests
testTimeout: 30000,
// Run tests in band for better isolation
runInBand: true,
// Fail on first error in CI
bail: process.env.CI ? 1 : 0,
};
Pipeline Timeout Handling:
# GitHub Actions timeout configuration
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Long running task
timeout-minutes: 10
run: ./scripts/build.sh
- name: Continue on timeout
if: failure() && steps.previous.conclusion == 'cancelled'
run: echo "Previous step timed out"
Related Topics
The following topics would complement this CI/CD Patterns cheatsheet:
-
GitOps and ArgoCD - Deep dive into GitOps workflows, ArgoCD configuration, and declarative infrastructure management for Kubernetes environments.
-
Container Security and Scanning - Comprehensive coverage of container vulnerability scanning, image signing, SBOM generation, and supply chain security practices.
-
Infrastructure as Code (Terraform/Pulumi) - Patterns for managing infrastructure alongside application code, including state management and drift detection.
-
Observability and Monitoring - Metrics, logs, and traces for CI/CD pipelines, deployment tracking, and SLO/SLI monitoring with tools like Prometheus and Grafana.
-
Feature Flag Management - Strategies for implementing feature flags, progressive rollouts, and A/B testing in conjunction with deployment pipelines.
-
Kubernetes Deployment Strategies - Advanced Kubernetes patterns including operators, custom resources, service mesh integration, and multi-cluster deployments.