Overview
Pipeline as Code is the watershed moment when Jenkins evolved from “drag-and-drop configuration” to “code-driven.” Defining pipelines in a Jenkinsfile under Git version control means every pipeline change has a diff to review, a history to trace, and a branch to roll back. This article systematically covers Pipeline as Code practices, from Jenkinsfile syntax to production-grade pipeline design.
Reference: Jenkins Pipeline Official Documentation
I. Jenkinsfile Syntax
1.1 Declarative vs. Scripted
Jenkins Pipeline has two syntax styles:
| Dimension | Declarative | Scripted |
|---|---|---|
| Syntax | Structured DSL | Groovy code |
| Readability | High, close to config files | Low, requires Groovy knowledge |
| Flexibility | Constrained by DSL | Fully free |
| Input validation | Built-in post, when, etc. | Manual implementation |
| Recommended for | Standard pipelines, team collaboration | Complex logic, many conditional branches |
// === Declarative Pipeline ===
pipeline {
agent any
stages {
stage('Build') {
steps {
sh 'make build'
}
}
}
}
// === Scripted Pipeline ===
node {
stage('Build') {
sh 'make build'
}
}
Best practice: Prefer declarative syntax. Only use
script {}blocks for complex logic that declarative can’t express.
1.2 Declarative Pipeline Structure
pipeline {
// === Agent: Execution Environment ===
agent {
label 'linux && docker'
// Or use Docker
// docker { image 'golang:1.22' }
// Or K8s
// kubernetes { ... }
}
// === Environment Variables ===
environment {
APP_NAME = 'myapp'
VERSION = "${env.BUILD_ID}"
DOCKER_REGISTRY = 'registry.example.com'
// Read from Credentials
DOCKER_CREDENTIALS = credentials('docker-registry')
// Read from config file
DEPLOY_CONFIG = 'deploy-config'
}
// === Options ===
options {
timeout(time: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
retry(3)
ansiColor('xterm')
}
// === Triggers ===
triggers {
cron('H 2 * * *') // Daily at 2 AM
pollSCM('H/5 * * * *') // Check SCM every 5 minutes
upstream(upstreamProjects: 'base-library', threshold: hudson.model.Result.SUCCESS)
}
// === Parameters ===
parameters {
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Deploy environment')
string(name: 'VERSION', defaultValue: '', description: 'Deploy version (empty for latest)')
booleanParam(name: 'SKIP_TESTS', defaultValue: false, description: 'Skip tests')
password(name: 'DEPLOY_TOKEN', defaultValue: '', description: 'Deploy token')
}
// === Stages ===
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
when {
expression { !params.SKIP_TESTS }
}
steps {
sh 'make test'
}
}
}
// === Post Actions ===
post {
always {
junit 'reports/**/*.xml'
archiveArtifacts artifacts: 'dist/**', fingerprint: true
cleanWs()
}
success {
echo 'Pipeline succeeded'
}
failure {
echo 'Pipeline failed'
// Send notification
emailext(
subject: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: "Check: ${env.BUILD_URL}",
to: 'team@example.com'
)
}
unstable {
echo 'Build is unstable'
}
changed {
echo 'Build status changed'
}
}
}
II. Multibranch Pipelines
2.1 Configuring Multibranch Pipelines
Multibranch pipelines automatically discover branches and PRs in a Git repository, creating an independent Jenkins job for each:
// Jenkinsfile - Multibranch pipeline adaptation
pipeline {
agent any
environment {
// Dynamically set environment based on branch name
BRANCH_NAME = "${env.BRANCH_NAME ?: 'unknown'}"
IS_MAIN = "${env.BRANCH_NAME == 'main'}"
IS_PR = "${env.CHANGE_ID != null}"
}
stages {
stage('Detect Environment') {
steps {
script {
echo "Branch: ${env.BRANCH_NAME}"
echo "Is PR: ${env.CHANGE_ID != null}"
if (env.CHANGE_ID) {
echo "PR number: ${env.CHANGE_ID}"
echo "PR target branch: ${env.CHANGE_TARGET}"
}
}
}
}
stage('Build') {
steps {
sh 'make build'
}
}
stage('Test') {
steps {
sh 'make test'
}
}
stage('Deploy') {
when {
anyOf {
branch 'main'
branch 'release/*'
}
expression { env.CHANGE_ID == null } // Not a PR
}
steps {
script {
if (env.BRANCH_NAME == 'main') {
echo 'Deploying to staging'
sh 'make deploy-staging'
} else if (env.BRANCH_NAME?.startsWith('release/')) {
input message: 'Confirm deployment to production?', ok: 'Deploy'
echo 'Deploying to production'
sh 'make deploy-production'
}
}
}
}
}
}
2.2 Branch Strategy
main → Auto-deploy to staging
release/* → Deploy to production after manual confirmation
develop → Build and test, no deploy
feature/* → Build (quick validation)
PR → Build + test + code quality check
// Branch-specific when conditions
stage('Deploy Staging') {
when {
branch 'main'
}
steps {
sh 'make deploy-staging'
}
}
stage('Deploy Production') {
when {
branch 'release/*'
beforeInput true // Evaluate when before input
}
input {
message "Confirm deployment to production?"
ok "Deploy"
submitter "release-managers"
}
steps {
sh 'make deploy-production'
}
}
stage('PR Check') {
when {
changeRequest target: 'main'
}
steps {
sh 'make lint security-scan'
}
}
III. Shared Libraries
3.1 Creating a Shared Library
When multiple projects share similar pipeline logic, extract it into a shared library for unified maintenance:
jenkins-shared-library/
├── vars/
│ ├── standardPipeline.groovy # Global variables (directly callable)
│ ├── deploy.groovy
│ ├── notify.groovy
│ └── buildDocker.groovy
├── src/
│ └── com/
│ └── example/
│ ├── Deployer.groovy # Class library
│ └── Config.groovy
└── resources/
└── templates/
└── deployment.yaml # Resource files
// vars/standardPipeline.groovy
// Standard pipeline template, reused across projects
def call(Map config = [:]) {
pipeline {
agent {
label config.agent ?: 'linux'
}
environment {
APP_NAME = config.appName ?: error('appName is required')
DOCKER_REGISTRY = config.registry ?: 'registry.example.com'
}
options {
timeout(time: config.timeout ?: 30, unit: 'MINUTES')
timestamps()
buildDiscarder(logRotator(numToKeepStr: '20'))
disableConcurrentBuilds()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh config.buildCmd ?: 'make build'
}
}
stage('Test') {
steps {
sh config.testCmd ?: 'make test'
}
post {
always {
junit testResults: config.testResults ?: 'reports/**/*.xml',
allowEmptyResults: true
}
}
}
stage('Code Quality') {
when {
expression { config.qualityCheck != false }
}
steps {
sh config.qualityCmd ?: 'make lint'
}
}
stage('Build Image') {
when {
expression { config.dockerBuild != false }
}
steps {
script {
buildDocker(
image: "${env.APP_NAME}",
tag: "${env.BUILD_NUMBER}"
)
}
}
}
stage('Deploy') {
when {
anyOf {
branch config.deployBranch ?: 'main'
expression { env.BRANCH_NAME?.startsWith('release/') }
}
}
steps {
script {
deploy(
appName: env.APP_NAME,
version: env.BUILD_NUMBER,
env: env.BRANCH_NAME == 'main' ? 'staging' : 'production'
)
}
}
}
}
post {
failure {
notify.slack(
channel: config.slackChannel ?: '#alerts',
message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
}
}
}
3.2 Using the Shared Library
// Project Jenkinsfile - just a few lines
@Library('jenkins-shared-library@v1.2') _
standardPipeline(
appName: 'payment-service',
agent: 'golang',
buildCmd: 'go build -o bin/app ./cmd/',
testCmd: 'go test -v -race ./...',
qualityCmd: 'golangci-lint run',
dockerBuild: true,
deployBranch: 'main',
slackChannel: '#payments'
)
3.3 Shared Library Utility Functions
// vars/buildDocker.groovy
def call(Map params) {
def image = params.image
def tag = params.tag
def dockerfile = params.dockerfile ?: 'Dockerfile'
def context = params.context ?: '.'
sh """
docker build -t ${image}:${tag} \
-f ${dockerfile} \
--build-arg VERSION=${tag} \
${context}
"""
// Push image
if (params.push != false) {
withCredentials([usernamePassword(
credentialsId: 'docker-registry',
usernameVariable: 'DOCKER_USER',
passwordVariable: 'DOCKER_PASS'
)]) {
sh """
echo \$DOCKER_PASS | docker login registry.example.com -u \$DOCKER_USER --password-stdin
docker push ${image}:${tag}
docker tag ${image}:${tag} ${image}:latest
docker push ${image}:latest
docker logout registry.example.com
"""
}
}
}
// vars/notify.groovy
def slack(Map params) {
def color = params.color ?: 'danger'
def channel = params.channel ?: '#general'
def message = params.message ?: 'No message'
slackSend(
channel: channel,
color: color,
message: message
)
}
def dingtalk(Map params) {
def message = params.message ?: 'No message'
def mobiles = params.mobiles ?: []
dingtalk(
robot: 'jenkins-robot',
type: 'MARKDOWN',
title: 'Jenkins Notification',
text: message,
at: mobiles
)
}
def email(Map params) {
emailext(
subject: params.subject ?: "Jenkins: ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: params.body ?: "Check: ${env.BUILD_URL}",
to: params.to ?: 'team@example.com',
mimeType: 'text/html',
attachmentsPattern: params.attachments ?: ''
)
}
IV. Parallel Stages
4.1 Parallel Test Execution
pipeline {
agent any
stages {
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'go test -v -short ./... 2>&1 | tee reports/unit-tests.xml'
}
post {
always {
junit 'reports/unit-tests.xml'
}
}
}
stage('Integration Tests') {
steps {
sh 'make test-integration'
}
}
stage('Lint') {
steps {
sh 'golangci-lint run --out-format=junit > reports/lint.xml'
}
post {
always {
junit 'reports/lint.xml'
}
}
}
stage('Security Scan') {
steps {
sh 'trivy fs --severity HIGH,CRITICAL .'
}
}
stage('License Check') {
steps {
sh 'go-licenses check ./...'
}
}
}
}
}
}
4.2 Matrix Build
pipeline {
agent any
stages {
stage('Matrix Build') {
matrix {
axes {
axis {
name 'GO_VERSION'
values '1.21', '1.22', '1.23'
}
axis {
name 'GOOS'
values 'linux', 'darwin', 'windows'
}
axis {
name 'GOARCH'
values 'amd64', 'arm64'
}
}
excludes {
// Exclude unsupported combinations
exclude {
axis {
name 'GOOS'
values 'windows'
}
axis {
name 'GOARCH'
values 'arm64'
}
}
}
agent {
label "golang-${GO_VERSION}"
}
stages {
stage('Build') {
steps {
sh """
GOOS=${GOOS} GOARCH=${GOARCH} \
go build -o bin/app-${GOOS}-${GOARCH} ./cmd/
"""
}
}
stage('Test') {
when {
expression { GOOS == 'linux' && GOARCH == 'amd64' }
}
steps {
sh 'go test -v ./...'
}
}
}
}
}
}
}
V. Conditional Triggers
5.1 When Conditions In Detail
pipeline {
agent any
stages {
// Based on branch
stage('Deploy Staging') {
when {
branch 'main'
}
steps { sh 'make deploy-staging' }
}
// Based on PR
stage('PR Validation') {
when {
changeRequest target: 'main', branch: 'feature/*'
}
steps { sh 'make validate' }
}
// Based on expression
stage('Production Deploy') {
when {
expression {
env.BRANCH_NAME ==~ /release\/.*/ &&
env.BUILD_NUMBER?.toInteger() > 0
}
}
steps { sh 'make deploy-production' }
}
// Based on changed file paths
stage('Build Frontend') {
when {
changeset 'frontend/**'
}
steps { sh 'cd frontend && npm run build' }
}
stage('Build Backend') {
when {
changeset 'backend/**'
}
steps { sh 'cd backend && go build' }
}
// Based on commit message
stage('Skip on [skip ci]') {
when {
expression {
!env.GIT_COMMIT_MESSAGE?.contains('[skip ci]')
}
}
steps { echo 'Running because no skip ci' }
}
// Combined conditions
stage('Complex Condition') {
when {
allOf {
branch 'main'
changeset 'deploy/**'
expression { params.FORCE_DEPLOY || env.GIT_MESSAGE?.contains('[deploy]') }
}
}
steps { sh 'make deploy' }
}
// Exclusion condition
stage('Except Docs') {
when {
not {
changeset 'docs/**'
}
}
steps { sh 'make build' }
}
}
}
5.2 Getting Commit Info
stage('Get Commit Info') {
steps {
script {
// Get latest commit
def commitMsg = sh(
script: 'git log -1 --pretty=%B',
returnStdout: true
).trim()
def commitAuthor = sh(
script: 'git log -1 --pretty=%an',
returnStdout: true
).trim()
def commitHash = sh(
script: 'git rev-parse --short HEAD',
returnStdout: true
).trim()
// Set environment variables for later use
env.GIT_MESSAGE = commitMsg
env.GIT_AUTHOR = commitAuthor
env.GIT_HASH = commitHash
echo "Commit: ${commitHash} by ${commitAuthor}"
echo "Message: ${commitMsg}"
// Check for specific markers
if (commitMsg.contains('[skip deploy]')) {
echo 'Skipping deploy stage'
env.SKIP_DEPLOY = 'true'
}
}
}
}
VI. Artifact Management
6.1 Artifact Upload and Download
pipeline {
agent any
environment {
ARTIFACT_NAME = "myapp-${env.BUILD_NUMBER}.tar.gz"
}
stages {
stage('Package') {
steps {
sh """
tar czf ${ARTIFACT_NAME} \
bin/ \
configs/ \
migrations/
"""
}
}
stage('Upload Artifact') {
steps {
// Option 1: Using Artifactory plugin
rtUpload(
serverId: 'artifactory',
spec: '''{
"files": [
{
"pattern": "*.tar.gz",
"target": "libs-release-local/myapp/${BUILD_NUMBER}/"
}
]
}'''
)
// Option 2: Using Nexus
nexusArtifactUploader(
nexusVersion: 'nexus3',
protocol: 'https',
nexusUrl: 'nexus.example.com',
groupId: 'com.example',
version: "${env.BUILD_NUMBER}",
repository: 'releases',
credentialsId: 'nexus-credentials',
artifacts: [
[artifactId: 'myapp',
classifier: '',
file: "${ARTIFACT_NAME}",
type: 'tar.gz']
]
)
// Option 3: Using AWS S3
withAWS(credentials: 'aws-credentials', region: 'cn-north-1') {
s3Upload(
bucket: 'my-artifacts',
path: "myapp/${env.BUILD_NUMBER}/",
includePathPattern: '**/*.tar.gz'
)
}
}
}
stage('Download Artifact') {
steps {
script {
// Download from artifact repository
rtDownload(
serverId: 'artifactory',
spec: '''{
"files": [{
"pattern": "libs-release-local/myapp/${BUILD_NUMBER}/*.tar.gz",
"target": "downloads/"
}]
}'''
)
}
}
}
}
}
6.2 Artifact Version Management
// vars/publishArtifact.groovy
def call(Map params) {
def appName = params.appName
def version = params.version
def files = params.files ?: []
def repository = params.repository ?: 'releases'
// Determine if SNAPSHOT or Release
def isRelease = !version.contains('SNAPSHOT') && !version.contains('dev')
if (!isRelease) {
repository = 'snapshots'
}
echo "Uploading artifact to ${repository}: ${appName}:${version}"
files.each { file ->
echo " Uploading: ${file}"
}
// Build artifact metadata
def metadata = [
appName: appName,
version: version,
buildNumber: env.BUILD_NUMBER,
buildUrl: env.BUILD_URL,
gitCommit: env.GIT_COMMIT,
timestamp: new Date().format("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone('UTC'))
]
// Write metadata file
writeJSON file: 'artifact-metadata.json', json: metadata, pretty: 2
// Upload metadata
files << 'artifact-metadata.json'
return metadata
}
VII. Kubernetes Integration
7.1 K8s Pod Agent
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: golang
image: golang:1.22-alpine
command: ['sleep', 'infinity']
volumeMounts:
- mountPath: /go/pkg
name: gopkg
- mountPath: /root/.cache/go-build
name: gobuild
resources:
requests:
memory: 1Gi
cpu: 500m
limits:
memory: 2Gi
cpu: 1000m
- name: docker
image: docker:24
command: ['sleep', 'infinity']
securityContext:
privileged: true
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
- name: kubectl
image: bitnami/kubectl:latest
command: ['sleep', 'infinity']
volumes:
- name: gopkg
emptyDir: {}
- name: gobuild
emptyDir: {}
- name: docker-sock
hostPath:
path: /var/run/docker.sock
'''
defaultContainer: 'golang'
}
}
stages {
stage('Build') {
steps {
container('golang') {
sh 'go build -o bin/app ./cmd/'
}
}
}
stage('Docker Build') {
steps {
container('docker') {
sh """
docker build -t myapp:${env.BUILD_NUMBER} .
docker tag myapp:${env.BUILD_NUMBER} registry.example.com/myapp:${env.BUILD_NUMBER}
docker push registry.example.com/myapp:${env.BUILD_NUMBER}
"""
}
}
}
stage('Deploy') {
steps {
container('kubectl') {
sh """
kubectl set image deployment/myapp \
app=registry.example.com/myapp:${env.BUILD_NUMBER} \
-n production
kubectl rollout status deployment/myapp -n production --timeout=300s
"""
}
}
}
}
}
7.2 Dynamic Agent Templates
// Select different Pod templates based on project type
def getPodTemplate(String projectType) {
def templates = [
golang: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: golang
image: golang:1.22-alpine
command: ['sleep', 'infinity']
resources:
requests: { memory: 1Gi, cpu: 500m }
limits: { memory: 2Gi, cpu: 2000m }
- name: docker
image: docker:24
securityContext: { privileged: true }
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
volumes:
- name: docker-sock
hostPath: { path: /var/run/docker.sock }
''',
nodejs: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: node
image: node:20-alpine
command: ['sleep', 'infinity']
resources:
requests: { memory: 2Gi, cpu: 500m }
limits: { memory: 4Gi, cpu: 2000m }
- name: docker
image: docker:24
securityContext: { privileged: true }
volumeMounts:
- mountPath: /var/run/docker.sock
name: docker-sock
volumes:
- name: docker-sock
hostPath: { path: /var/run/docker.sock }
''',
python: '''
apiVersion: v1
kind: Pod
spec:
containers:
- name: python
image: python:3.12-slim
command: ['sleep', 'infinity']
resources:
requests: { memory: 1Gi, cpu: 500m }
limits: { memory: 2Gi, cpu: 1000m }
'''
]
return templates[projectType] ?: templates.golang
}
pipeline {
agent none
stages {
stage('Build & Test') {
steps {
script {
def template = getPodTemplate('golang')
podTemplate(yaml: template) {
node(POD_LABEL) {
container('golang') {
sh 'go test -v ./...'
sh 'go build -o bin/app ./cmd/'
}
}
}
}
}
}
}
}
VIII. Blue-Green Deployment Pipeline
8.1 Complete Blue-Green Deployment
pipeline {
agent any
environment {
APP_NAME = 'myapp'
VERSION = "${env.BUILD_NUMBER}"
NAMESPACE = 'production'
DOCKER_IMAGE = "registry.example.com/${APP_NAME}:${VERSION}"
}
stages {
stage('Build & Push') {
steps {
sh "docker build -t ${DOCKER_IMAGE} ."
sh "docker push ${DOCKER_IMAGE}"
}
}
stage('Deploy to Blue') {
steps {
sh """
# Deploy to Blue environment
envsubst < deploy/blue-green.yaml | kubectl apply -f - -n ${NAMESPACE}
# Wait for Blue to be ready
kubectl rollout status deployment/${APP_NAME}-blue -n ${NAMESPACE} --timeout=300s
"""
}
}
stage('Smoke Test Blue') {
steps {
sh """
# Smoke test via Blue service ClusterIP
BLUE_IP=\$(kubectl get svc ${APP_NAME}-blue -n ${NAMESPACE} -o jsonpath='{.spec.clusterIP}')
# Wait for service to be available
for i in \$(seq 1 30); do
if curl -sf http://\${BLUE_IP}:8080/health; then
echo "Blue service is ready"
break
fi
sleep 2
done
# Smoke tests
curl -sf http://\${BLUE_IP}:8080/health || exit 1
curl -sf http://\${BLUE_IP}:8080/api/version | grep "${VERSION}" || exit 1
curl -sf http://\${BLUE_IP}:8080/api/users?limit=1 || exit 1
echo "Smoke tests passed"
"""
}
}
stage('Switch Traffic') {
input {
message "Blue environment smoke tests passed, switch traffic?"
ok "Switch to Blue"
submitter "release-managers"
}
steps {
sh """
# Update service selector to route traffic to Blue
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"blue"}}}'
echo "Traffic switched to Blue"
"""
}
}
stage('Verify Production') {
steps {
sh """
# Verify production traffic is healthy
sleep 10
# Check error rate
ERROR_RATE=\$(curl -sf 'http://prometheus:9090/api/v1/query' \\
--data-urlencode 'query=rate(http_requests_total{app="${APP_NAME}",status=~"5.."}[1m]) / rate(http_requests_total{app="${APP_NAME}"}[1m])' \\
| jq -r '.data.result[0].value[1] // "0"')
echo "Current error rate: \${ERROR_RATE}"
if (( \$(echo "\${ERROR_RATE} > 0.05" | bc -l) )); then
echo "Error rate too high, auto-rollback!"
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"green"}}}'
exit 1
fi
echo "Production verification passed"
"""
}
}
stage('Cleanup Green') {
steps {
sh """
# Scale down old environment after confirming stability
kubectl scale deployment ${APP_NAME}-green -n ${NAMESPACE} --replicas=0
echo "Green environment scaled down"
"""
}
}
}
post {
failure {
sh """
# Auto-rollback to Green on failure
kubectl patch svc ${APP_NAME} -n ${NAMESPACE} -p \\
'{"spec":{"selector":{"version":"green"}}}' || true
echo "Rolled back to Green environment"
"""
slackSend(
channel: '#alerts',
color: 'danger',
message: "Blue-green deployment failed, rolled back: ${env.JOB_NAME} #${env.BUILD_NUMBER}"
)
}
success {
slackSend(
channel: '#deployments',
color: 'good',
message: "Blue-green deployment succeeded: ${env.APP_NAME} v${env.VERSION}"
)
}
}
}
8.2 Canary Deployment
// Canary deployment: gradual traffic shifting
stage('Canary Deploy') {
steps {
sh """
# Deploy canary version (1 replica)
kubectl set image deployment/${APP_NAME}-canary \
app=${DOCKER_IMAGE} -n ${NAMESPACE} || \
kubectl create deployment ${APP_NAME}-canary \
--image=${DOCKER_IMAGE} -n ${NAMESPACE}
kubectl scale deployment ${APP_NAME}-canary -n ${NAMESPACE} --replicas=1
"""
}
}
stage('Canary 10% Traffic') {
steps {
sh """
# Adjust traffic ratio via Istio VirtualService
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":90},{"op":"replace","path":"/spec/http/0/route/1/weight","value":10}]'
echo "10% traffic to canary version"
sleep 60 # Observe for one minute
"""
}
}
stage('Canary 50% Traffic') {
input {
message "10% canary test looks good, shift to 50%?"
ok "Shift to 50%"
}
steps {
sh """
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":50},{"op":"replace","path":"/spec/http/0/route/1/weight","value":50}]'
sleep 120 # Observe for two minutes
"""
}
}
stage('Canary 100% Traffic') {
input {
message "50% canary test looks good, go full?"
ok "Full release"
submitter "release-managers"
}
steps {
sh """
kubectl patch virtualservice ${APP_NAME} -n ${NAMESPACE} --type='json' \\
-p='[{"op":"replace","path":"/spec/http/0/route/0/weight","value":0},{"op":"replace","path":"/spec/http/0/route/1/weight","value":100}]'
# Update main deployment
kubectl set image deployment/${APP_NAME} \\
app=${DOCKER_IMAGE} -n ${NAMESPACE}
kubectl rollout status deployment/${APP_NAME} -n ${NAMESPACE}
# Clean up canary
kubectl delete deployment ${APP_NAME}-canary -n ${NAMESPACE}
"""
}
}
IX. Pipeline Best Practices
9.1 Pipeline Performance Optimization
pipeline {
agent any
options {
// Timeout control
timeout(time: 30, unit: 'MINUTES')
// No concurrent builds
disableConcurrentBuilds()
// Keep history
buildDiscarder(logRotator(numToKeepStr: '30'))
// Timestamps
timestamps()
}
stages {
// Skip stages when nothing changed
stage('Build') {
when {
changeset '**/*.go'
changeset 'go.mod'
}
steps {
sh 'make build'
}
}
// Run independent tasks in parallel
stage('Test') {
parallel {
stage('Unit Test') {
steps { sh 'make test-unit' }
}
stage('Lint') {
steps { sh 'make lint' }
}
stage('Security Scan') {
steps { sh 'make security-scan' }
}
}
}
// Cache dependencies
stage('Restore Cache') {
steps {
sh '''
if [ -d /go/pkg/mod ]; then
cp -r /go/pkg/mod ./go-mod-cache || true
fi
'''
}
}
}
}
9.2 Notifications and Monitoring
// vars/pipelineNotify.groovy
def call(Map config = [:]) {
def status = config.status ?: currentBuild.currentResult
def channel = config.channel ?: '#ci-cd'
def color = 'good'
def emoji = ':white_check_mark:'
if (status == 'FAILURE') {
color = 'danger'
emoji = ':x:'
} else if (status == 'UNSTABLE') {
color = 'warning'
emoji = ':warning:'
}
def duration = currentBuild.durationString ?: 'unknown'
def message = """
${emoji} Pipeline ${status}
*Job:* ${env.JOB_NAME}
*Build:* #${env.BUILD_NUMBER}
*Branch:* ${env.BRANCH_NAME ?: 'N/A'}
*Duration:* ${duration}
*URL:* ${env.BUILD_URL}
""".stripIndent().trim()
// Slack notification
slackSend(channel: channel, color: color, message: message)
// DingTalk notification
if (config.dingtalk) {
dingtalk(
robot: config.dingtalkRobot ?: 'jenkins',
type: 'MARKDOWN',
title: "Pipeline ${status}",
text: message
)
}
// Email notification (only on failure)
if (status == 'FAILURE' && config.emailOnFailure != false) {
emailext(
subject: "[${status}] ${env.JOB_NAME} #${env.BUILD_NUMBER}",
body: message,
to: config.emailTo ?: 'team@example.com',
mimeType: 'text/html'
)
}
}
Summary
The core value of Jenkins Pipeline as Code is: transforming CI/CD processes from “configuration” to “code.” This means pipelines can be version-controlled, code-reviewed, reused, and tested. Key takeaways:
- Declarative first: Declarative Pipeline has clear structure and built-in validation, suitable for team collaboration. Use
script {}blocks for complex logic rather than going fully scripted - Multibranch pipelines are standard: Automatically create independent jobs for branches and PRs. Combined with
whenconditions, implement a branch strategy of “main deploys to staging, release deploys to production, PRs only run checks” - Shared libraries eliminate duplication: Extract standard pipeline logic into
vars/standardPipeline.groovy; each project needs only a few lines of config. Pin shared library versions to prevent upstream changes from affecting all projects - Parallelization brings significant speedup: Tests, lint, and security scans run in parallel, cutting build time from 20 minutes (sequential) to 5 minutes. Matrix builds cover multiple platforms and versions
- Artifact management is essential: Upload each build’s artifacts to a repository (Artifactory/Nexus/S3) with version, build number, Git hash metadata. Deploy from the artifact repository, never rebuild from source
- K8s integration provides elasticity: Use K8s Pods as Jenkins Agents, created and destroyed on demand. Different projects use different container images for complete environment isolation
- Blue-green/canary must be automated: Deploy to standby → smoke test → switch traffic → verify → cleanup — the entire chain is automated. Auto-rollback on failure; human
inputonly at key decision points
Pipeline as Code isn’t just about moving pipeline configuration into a code file — it’s about managing CI/CD processes with engineering rigor. When each project’s Jenkinsfile is just a dozen lines (calling a shared library), and all complex logic is centrally maintained and versioned in the shared library, CI/CD truly achieves “code-driven governance.”
References & Acknowledgments
This article referenced the following materials during writing. We thank the original authors for their contributions:
- Jenkins Pipeline Official Documentation — Jenkins Project, referenced for Jenkins Pipeline Official Documentation