CI/CD Pipeline Setup
CI/CD Pipeline Setup
Step-by-step guide to setting up production-ready CI/CD pipelines from scratch with security, testing, and deployment automation.
Pipeline Architecture
A well-architected CI/CD pipeline provides fast feedback, ensures quality, and enables safe, frequent deployments.
Core Components
- Version Control: Git-based source control
- CI Server: Build and test automation
- Artifact Repository: Store build artifacts
- Container Registry: Docker image storage
- Deployment Platform: Target infrastructure
- Monitoring: Pipeline and application metrics
Initial Setup
Repository Structure
project-root/
├── .github/workflows/ # GitHub Actions
├── .gitlab-ci.yml # GitLab CI
├── Jenkinsfile # Jenkins Pipeline
├── src/ # Application source
├── tests/ # Test suites
├── scripts/ # Build/deploy scripts
├── k8s/ # Kubernetes manifests
├── terraform/ # Infrastructure code
├── docker/ # Dockerfiles
├── docs/ # Documentation
└── .gitignore
Branch Strategy
# Git Flow Configuration
git flow init
# Feature branch
git flow feature start new-feature
# Work on feature
git flow feature finish new-feature
# Release branch
git flow release start 1.0.0
# Prepare release
git flow release finish 1.0.0
# Hotfix
git flow hotfix start critical-fix
# Fix issue
git flow hotfix finish critical-fix
Jenkins Setup
Jenkins Installation
# Docker Compose for Jenkins
version: '3.8'
services:
jenkins:
image: jenkins/jenkins:lts-jdk11
privileged: true
user: root
ports:
- "8080:8080"
- "50000:50000"
volumes:
- jenkins_home:/var/jenkins_home
- /var/run/docker.sock:/var/run/docker.sock
environment:
- JENKINS_OPTS="--prefix=/jenkins"
restart: unless-stopped
jenkins-agent:
image: jenkins/inbound-agent
privileged: true
user: root
environment:
- JENKINS_URL=http://jenkins:8080/jenkins
- JENKINS_AGENT_NAME=docker-agent
- JENKINS_SECRET=${JENKINS_SECRET}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
volumes:
jenkins_home:
Jenkins Configuration as Code
# jenkins.yaml
jenkins:
systemMessage: "Jenkins CI/CD Server"
securityRealm:
local:
allowsSignup: false
users:
- id: "admin"
password: "${ADMIN_PASSWORD}"
authorizationStrategy:
globalMatrix:
permissions:
- "Overall/Read:anonymous"
- "Overall/Administer:admin"
clouds:
- kubernetes:
name: "kubernetes"
serverUrl: "https://kubernetes.default"
namespace: "jenkins"
jenkinsUrl: "http://jenkins:8080"
credentials:
system:
domainCredentials:
- credentials:
- string:
scope: GLOBAL
id: "github-token"
secret: "${GITHUB_TOKEN}"
- usernamePassword:
scope: GLOBAL
id: "docker-hub"
username: "${DOCKER_USERNAME}"
password: "${DOCKER_PASSWORD}"
unclassified:
location:
url: "https://jenkins.example.com/"
gitscm:
globalConfigName: "Jenkins"
globalConfigEmail: "[email protected]"
GitLab CI Setup
GitLab Runner Installation
# Install GitLab Runner on Kubernetes
helm repo add gitlab https://charts.gitlab.io
helm repo update
# Create values file
cat > gitlab-runner-values.yaml << EOF
gitlabUrl: https://gitlab.com/
runnerRegistrationToken: "YOUR_REGISTRATION_TOKEN"
concurrent: 10
checkInterval: 30
rbac:
create: true
serviceAccountName: gitlab-runner
runners:
config: |
[[runners]]
[runners.kubernetes]
image = "ubuntu:20.04"
privileged = true
[[runners.kubernetes.volumes.pvc]]
name = "cache"
mount_path = "/cache"
[runners.cache]
Type = "s3"
Shared = true
[runners.cache.s3]
ServerAddress = "s3.amazonaws.com"
BucketName = "gitlab-runner-cache"
BucketLocation = "us-east-1"
EOF
# Install runner
helm install gitlab-runner gitlab/gitlab-runner \
-f gitlab-runner-values.yaml \
--namespace gitlab-runner \
--create-namespace
Pipeline Templates
# .gitlab-ci.yml with reusable components
include:
- project: 'devops/ci-templates'
ref: main
file:
- '/templates/security.yml'
- '/templates/docker.yml'
- '/templates/deploy.yml'
variables:
DOCKER_DRIVER: overlay2
DOCKER_BUILDKIT: 1
.default_rules:
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS'
when: never
- if: '$CI_COMMIT_BRANCH'
stages:
- build
- test
- security
- package
- deploy
build:app:
extends: .default_rules
stage: build
image: node:18
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
policy: pull-push
script:
- npm ci
- npm run build
artifacts:
paths:
- dist/
expire_in: 1 day
GitHub Actions Setup
Self-Hosted Runners
# Kubernetes deployment for GitHub Actions runner
apiVersion: v1
kind: Namespace
metadata:
name: actions-runner-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: github-runner
namespace: actions-runner-system
spec:
replicas: 3
selector:
matchLabels:
app: github-runner
template:
metadata:
labels:
app: github-runner
spec:
containers:
- name: runner
image: summerwind/actions-runner:latest
env:
- name: RUNNER_NAME_PREFIX
value: "k8s-runner"
- name: RUNNER_TOKEN
valueFrom:
secretKeyRef:
name: github-runner
key: runner-token
- name: RUNNER_REPOSITORY_URL
value: "https://github.com/myorg/myrepo"
- name: RUNNER_LABELS
value: "self-hosted,linux,x64,kubernetes"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
Reusable Workflows
# .github/workflows/reusable-build.yml
name: Reusable Build Workflow
on:
workflow_call:
inputs:
node-version:
required: false
type: string
default: '18'
environment:
required: true
type: string
secrets:
npm-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Configure npm
run: |
echo "//registry.npmjs.org/:_authToken=${{ secrets.npm-token }}" > ~/.npmrc
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build:${{ inputs.environment }}
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: build-${{ inputs.environment }}
path: dist/
retention-days: 7
# Usage in main workflow
name: CI/CD
on: [push, pull_request]
jobs:
build-staging:
uses: ./.github/workflows/reusable-build.yml
with:
environment: staging
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}
Testing Pipeline
Test Stages Configuration
# Complete test pipeline
test:
stage: test
parallel:
matrix:
- TEST_SUITE: unit
TEST_COMMAND: "npm run test:unit"
- TEST_SUITE: integration
TEST_COMMAND: "npm run test:integration"
SERVICES: ["postgres:14", "redis:7"]
- TEST_SUITE: e2e
TEST_COMMAND: "npm run test:e2e"
BROWSER: ["chrome", "firefox", "safari"]
image: node:18
services: $SERVICES
before_script:
- npm ci
- |
if [[ "$TEST_SUITE" == "e2e" ]]; then
apt-get update && apt-get install -y chromium firefox-esr
npm run build
npm run preview &
npx wait-on http://localhost:4173
fi
script:
- $TEST_COMMAND
artifacts:
when: always
reports:
junit: test-results/junit.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
paths:
- test-results/
- coverage/
expire_in: 30 days
Performance Testing
# k6 performance test integration
performance:
stage: test
image: loadimpact/k6:latest
script:
- k6 run --out json=performance.json tests/performance/load-test.js
artifacts:
reports:
performance: performance.json
expose_as: 'performance-report'
rules:
- if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
- if: '$CI_PIPELINE_SOURCE == "schedule"'
# k6 test script
import http from 'k6/http';
import { check, sleep } from 'k6';
export let options = {
stages: [
{ duration: '5m', target: 100 },
{ duration: '10m', target: 100 },
{ duration: '5m', target: 200 },
{ duration: '10m', target: 200 },
{ duration: '5m', target: 0 },
],
thresholds: {
http_req_duration: ['p(95)<500'],
http_req_failed: ['rate<0.1'],
},
};
export default function () {
let response = http.get('https://staging.example.com/api/health');
check(response, {
'status is 200': (r) => r.status === 200,
'response time < 500ms': (r) => r.timings.duration < 500,
});
sleep(1);
}
Security Pipeline
Security Scanning Integration
# Security scanning job
security:scan:
stage: security
image: aquasec/trivy:latest
script:
# Scan dependencies
- trivy fs --security-checks vuln --severity HIGH,CRITICAL .
# Scan container image
- trivy image --severity HIGH,CRITICAL ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
# Generate reports
- trivy fs --format template --template "@/contrib/gitlab.tpl" -o gl-dependency-scanning-report.json .
- trivy image --format template --template "@/contrib/gitlab.tpl" -o gl-container-scanning-report.json ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}
artifacts:
reports:
dependency_scanning: gl-dependency-scanning-report.json
container_scanning: gl-container-scanning-report.json
# SAST with multiple tools
security:sast:
stage: security
image: returntocorp/semgrep:latest
script:
- semgrep ci --json --output=semgrep-results.json
- semgrep --config=auto --severity=ERROR --json --output=sast-report.json .
artifacts:
reports:
sast: sast-report.json
Artifact Management
Nexus Repository Setup
# Docker compose for Nexus
version: '3.8'
services:
nexus:
image: sonatype/nexus3:latest
ports:
- "8081:8081"
- "8082:8082" # Docker registry
volumes:
- nexus-data:/nexus-data
environment:
- NEXUS_CONTEXT=nexus
restart: unless-stopped
volumes:
nexus-data:
# Configure in CI/CD
publish:artifacts:
stage: package
script:
- mvn deploy -DaltDeploymentRepository=nexus::default::https://nexus.example.com/repository/maven-releases/
only:
- tags
Multi-Environment Deployment
Environment Configuration
# environments/staging/values.yaml
replicaCount: 2
image:
repository: myapp
pullPolicy: Always
tag: "" # Overridden by CI/CD
ingress:
enabled: true
hosts:
- host: staging.example.com
paths:
- path: /
pathType: Prefix
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
# environments/production/values.yaml
replicaCount: 5
image:
repository: myapp
pullPolicy: IfNotPresent
tag: "" # Overridden by CI/CD
ingress:
enabled: true
hosts:
- host: example.com
paths:
- path: /
pathType: Prefix
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
autoscaling:
enabled: true
minReplicas: 5
maxReplicas: 20
targetCPUUtilizationPercentage: 70
Progressive Deployment
# Argo Rollouts configuration
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: myapp-rollout
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 20
- pause: {duration: 10m}
- setWeight: 40
- pause: {duration: 10m}
- setWeight: 60
- pause: {duration: 10m}
- setWeight: 80
- pause: {duration: 10m}
analysis:
templates:
- templateName: success-rate
startingStep: 2
args:
- name: service-name
value: myapp-canary
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myapp:latest
ports:
- containerPort: 8080
Pipeline Monitoring
Metrics Collection
# Prometheus metrics for CI/CD
- job_name: 'jenkins'
static_configs:
- targets: ['jenkins:8080']
metrics_path: '/prometheus'
- job_name: 'gitlab-runner'
static_configs:
- targets: ['gitlab-runner:9252']
# Grafana dashboard queries
# Build success rate
sum(rate(jenkins_job_build_result_total{result="SUCCESS"}[1h])) / sum(rate(jenkins_job_build_result_total[1h]))
# Average build duration
avg(jenkins_job_build_duration_seconds)
# Deployment frequency
sum(increase(deployments_total[1d]))
Troubleshooting Guide
Common Issues
Issue | Cause | Solution |
---|---|---|
Build timeout | Resource constraints | Increase runner resources |
Flaky tests | Race conditions | Add retries, fix async issues |
Deploy fails | Permission issues | Check service account roles |
Slow pipeline | No caching | Implement dependency caching |
Best Practices Checklist
- □ Version control everything including pipeline code
- □ Use pipeline as code (Jenkinsfile, .gitlab-ci.yml)
- □ Implement comprehensive testing
- □ Automate security scanning
- □ Use artifact repositories
- □ Implement proper secret management
- □ Monitor pipeline metrics
- □ Document pipeline processes
- □ Regular pipeline maintenance
Related Resources
Note: This documentation is provided for reference purposes only. It reflects general best practices and industry-aligned guidelines, and any examples, claims, or recommendations are intended as illustrative—not definitive or binding.