Advanced CI/CD Pipelines
Advanced CI/CD Pipelines
Principles of a mature pipeline
An advanced CI/CD pipeline goes beyond just building and deploying. It integrates quality, security, testing, and progressive deployment.
graph LR
A[Commit] --> B[Lint & Format]
B --> C[Unit Tests]
C --> D[Build & Security Scan]
D --> E[Integration Tests]
E --> F[Deploy Staging]
F --> G[E2E Tests]
G --> H[Deploy Canary 5%]
H --> I{Metrics OK?}
I -->|Yes| J[Rollout 100%]
I -->|No| K[Automatic Rollback]
GitHub Actions: complete pipelines
Multi-environment deployment pipeline
name: Deploy Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
lint-and-test:
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
- run: npm run test -- --coverage
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
security-scan:
runs-on: ubuntu-latest
needs: lint-and-test
steps:
- uses: actions/checkout@v4
- name: Dependency audit
run: npm audit --audit-level=high
- name: SAST scan with Semgrep
uses: semgrep/semgrep-action@v1
with:
config: p/typescript
- name: Secret scanning
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
build-and-push:
runs-on: ubuntu-latest
needs: [lint-and-test, security-scan]
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
steps:
- uses: actions/checkout@v4
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Image vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
exit-code: 1
severity: CRITICAL,HIGH
deploy-staging:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: |
kubectl set image deployment/api \
api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n staging
kubectl rollout status deployment/api -n staging --timeout=300s
deploy-production:
runs-on: ubuntu-latest
needs: build-and-push
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Canary deployment (5%)
run: |
kubectl apply -f k8s/canary.yaml
kubectl set image deployment/api-canary \
api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n production
- name: Metrics verification (5 min)
run: |
sleep 300
ERROR_RATE=$(curl -s "http://prometheus:9090/api/v1/query?query=rate(http_requests_total{status=~'5..'}[5m])" | jq '.data.result[0].value[1]')
if (( $(echo "$ERROR_RATE > 0.01" | bc -l) )); then
echo "Error rate too high: $ERROR_RATE"
kubectl rollout undo deployment/api-canary -n production
exit 1
fi
- name: Full rollout
run: |
kubectl set image deployment/api \
api=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
-n production
kubectl rollout status deployment/api -n production --timeout=600s
kubectl delete -f k8s/canary.yaml
GitLab CI/CD: advanced pipeline
stages:
- validate
- test
- build
- deploy
- verify
variables:
DOCKER_TLS_CERTDIR: "/certs"
.node-cache: &node-cache
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
lint:
stage: validate
<<: *node-cache
script:
- npm ci
- npm run lint
- npm run type-check
test:
stage: test
<<: *node-cache
services:
- postgres:16-alpine
variables:
POSTGRES_DB: test
POSTGRES_PASSWORD: test
DATABASE_URL: postgres://postgres:test@postgres:5432/test
script:
- npm ci
- npm run test:ci
- npm run test:e2e
coverage: '/All files[^|]*\|[^|]*\s+([\d.]+)/'
artifacts:
reports:
junit: junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
build:
stage: build
image: docker:24
services:
- docker:24-dind
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
deploy_production:
stage: deploy
environment:
name: production
url: https://api.example.com
when: manual
only:
- main
script:
- helm upgrade --install api ./charts/api
--set image.tag=$CI_COMMIT_SHA
--namespace production
--wait --timeout 10m
smoke_test:
stage: verify
needs: [deploy_production]
script:
- curl -f https://api.example.com/health || exit 1
- npm run test:smoke
Deployment strategies
Blue/Green
Two identical environments: blue (current) and green (new). Traffic is switched all at once.
# Service pointing to the active environment
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector:
app: api
version: green # Switch between blue and green
ports:
- port: 3000
Canary
Progressive deployment: 5% -> 25% -> 50% -> 100%.
Feature Flags
Decouple deployment from feature activation:
// The code is deployed but the feature is disabled
if (await featureFlags.isEnabled('new-checkout', { userId })) {
return newCheckoutFlow(order);
}
return legacyCheckoutFlow(order);
CI/CD best practices
- Fail fast: run the fastest steps first (lint, types)
- Parallelize independent jobs
- Cache dependencies between runs
- Immutable artifacts: build once, deploy everywhere
- Ephemeral environments for PRs (preview deployments)
- Automatic rollback if metrics degrade
- Secrets managed by CI, never in code