Jenkins Declarative Pipeline: Better CI/CD Syntax
We migrated our Jenkins pipelines from Scripted to Declarative syntax. Much better.
Scripted vs Declarative
Scripted (old):
node {
stage('Build') {
checkout scm
sh 'mvn clean package'
}
stage('Test') {
sh 'mvn test'
}
stage('Deploy') {
sh 'kubectl apply -f deployment.yaml'
}
}
Declarative (new):
pipeline {
agent any
stages {
stage('Build') {
steps {
checkout scm
sh 'mvn clean package'
}
}
stage('Test') {
steps {
sh 'mvn test'
}
}
stage('Deploy') {
steps {
sh 'kubectl apply -f deployment.yaml'
}
}
}
}
Benefits
1. Better Structure
Declarative enforces structure. Easier to read and maintain.
2. Built-in Features
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
}
options {
timeout(time: 1, unit: 'HOURS')
timestamps()
}
stages {
stage('Build') {
steps {
sh 'docker build -t ${DOCKER_REGISTRY}/myapp:${BUILD_NUMBER} .'
}
}
}
post {
success {
slackSend color: 'good', message: "Build ${BUILD_NUMBER} succeeded"
}
failure {
slackSend color: 'danger', message: "Build ${BUILD_NUMBER} failed"
}
}
}
3. Parallel Execution
pipeline {
agent any
stages {
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify'
}
}
stage('Lint') {
steps {
sh 'npm run lint'
}
}
}
}
}
}
Real Example
Our production pipeline:
pipeline {
agent any
environment {
DOCKER_REGISTRY = 'registry.example.com'
IMAGE_NAME = 'myapp'
K8S_NAMESPACE = 'production'
}
options {
buildDiscarder(logRotator(numToKeepStr: '10'))
timeout(time: 1, unit: 'HOURS')
timestamps()
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
sh 'mvn clean package -DskipTests'
}
}
stage('Test') {
parallel {
stage('Unit Tests') {
steps {
sh 'mvn test'
}
}
stage('Integration Tests') {
steps {
sh 'mvn verify -Pintegration'
}
}
}
}
stage('Docker Build') {
steps {
script {
docker.build("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}")
}
}
}
stage('Docker Push') {
steps {
script {
docker.withRegistry("https://${DOCKER_REGISTRY}", 'docker-credentials') {
docker.image("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}").push()
docker.image("${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER}").push('latest')
}
}
}
}
stage('Deploy to Staging') {
steps {
sh """
kubectl set image deployment/${IMAGE_NAME} \
${IMAGE_NAME}=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER} \
-n staging
kubectl rollout status deployment/${IMAGE_NAME} -n staging
"""
}
}
stage('Approval') {
steps {
input message: 'Deploy to production?', ok: 'Deploy'
}
}
stage('Deploy to Production') {
steps {
sh """
kubectl set image deployment/${IMAGE_NAME} \
${IMAGE_NAME}=${DOCKER_REGISTRY}/${IMAGE_NAME}:${BUILD_NUMBER} \
-n ${K8S_NAMESPACE}
kubectl rollout status deployment/${IMAGE_NAME} -n ${K8S_NAMESPACE}
"""
}
}
}
post {
success {
slackSend color: 'good', message: "Deployed ${IMAGE_NAME}:${BUILD_NUMBER} to production"
}
failure {
slackSend color: 'danger', message: "Build ${BUILD_NUMBER} failed"
}
always {
junit '**/target/surefire-reports/*.xml'
archiveArtifacts artifacts: '**/target/*.jar', fingerprint: true
}
}
}
The Verdict
Declarative Pipeline is much better than Scripted. More readable, more maintainable, better features.
Migrate your pipelines. It’s worth it.
Questions? Let me know!