Overview and What You Will Learn
Jenkins is the dominant CI platform in enterprise environments -- banks, insurance companies, large Indian enterprises running on-premise infrastructure for compliance. Despite cloud-native alternatives, Jenkins remains the answer when you need complete control over your CI infrastructure, when your company policy prohibits cloud CI runners, or when you are working in an organisation with a decade of Jenkins investment.
By the end of this lab you will:
- Write a complete declarative Jenkinsfile from scratch
- Configure multi-stage pipelines with parallel execution
- Use Docker agents to run each stage in the correct container
- Bind Jenkins credentials to pipeline environment variables securely
- Write and use Jenkins shared libraries for reusable pipeline code
- Configure post-conditions for notifications and cleanup
Why This Matters in Production
Many Indian enterprises -- large banks, NBFCs, insurance companies -- run Jenkins on-premise because their compliance requirements prohibit sending source code or build artifacts to cloud CI providers. Engineers joining these teams need Jenkins proficiency from day one. Understanding declarative Jenkinsfiles is how you work effectively in these environments without fighting the tooling.
Core Principles
Jenkins architecture -- controller and agents:
+------------------------------------------+| Jenkins Controller || Web UI, job scheduling, credential store |+------------------------------------------+ | | | v v v+----------+ +----------+ +----------+| Agent 1 | | Agent 2 | | Agent 3 || static | | docker | | k8s pod || VM | | executor | | dynamic |+----------+ +----------+ +----------+ Each job runs on ONE agent.Parallel stages run on DIFFERENT agents simultaneously.Dynamic K8s agents spin up per build, terminate after.Declarative Jenkinsfile structure:
pipeline { agent none <- per-stage agents environment { } <- global env vars stages { stage('Build') { agent { docker { image '...' } } steps { sh '...' } } stage('Test') { parallel { <- parallel stages stage('Unit') { ... } stage('Lint') { ... } } } } post { <- always runs success { } failure { } always { cleanWs() } }}Detailed Step-by-Step Practical Lab
Milestone 1 -- First declarative Jenkinsfile
// Jenkinsfile -- store in repository rootpipeline { agent none environment { ECR_REGISTRY = '123456789.dkr.ecr.ap-south-1.amazonaws.com' IMAGE_NAME = 'payment-api' AWS_CREDS = credentials('aws-ecr-credentials') } options { timeout(time: 30, unit: 'MINUTES') buildDiscarder(logRotator(numToKeepStr: '20')) disableConcurrentBuilds() } stages { stage('Build') { agent { docker { image 'docker:24-cli' args '-v /var/run/docker.sock:/var/run/docker.sock' } } steps { script { def imageTag = "${ECR_REGISTRY}/${IMAGE_NAME}:${GIT_COMMIT}" sh """ docker build -t ${imageTag} . echo ${AWS_CREDS_PSW} | docker login \\ --username AWS --password-stdin ${ECR_REGISTRY} docker push ${imageTag} """ env.IMAGE_TAG = imageTag } } } }}Milestone 2 -- Parallel stages
stage('Validate') { parallel { stage('Unit Tests') { agent { docker { image 'node:20-alpine' } } steps { sh 'npm ci' sh 'npm test -- --reporter=junit --outputFile=junit.xml' } post { always { junit 'junit.xml' } } } stage('Lint') { agent { docker { image 'node:20-alpine' } } steps { sh 'npm ci && npm run lint' } } stage('Security Scan') { agent { docker { image 'aquasec/trivy:latest' args '--entrypoint=' } } steps { sh "trivy image --exit-code 1 --severity HIGH,CRITICAL ${env.IMAGE_TAG}" } } } }Milestone 3 -- Credential binding and secrets
stage('Deploy Staging') { agent { label 'k8s-deployer' } when { branch 'main' } steps { withCredentials([ string(credentialsId: 'kubeconfig-staging', variable: 'KUBECONFIG_DATA'), string(credentialsId: 'slack-webhook', variable: 'SLACK_WEBHOOK') ]) { sh ''' echo "$KUBECONFIG_DATA" > /tmp/kubeconfig export KUBECONFIG=/tmp/kubeconfig helm upgrade --install payment-api ./charts/payment-api \\ --namespace payment-api-staging \\ --set image.tag=$GIT_COMMIT \\ --atomic --timeout 5m --wait rm -f /tmp/kubeconfig ''' } } } stage('Deploy Production') { agent { label 'k8s-deployer' } when { branch 'main' } input { message 'Deploy to production?' ok 'Deploy Now' submitter 'tech-leads,senior-engineers' } steps { withCredentials([ string(credentialsId: 'kubeconfig-production', variable: 'KUBECONFIG_DATA') ]) { sh ''' echo "$KUBECONFIG_DATA" > /tmp/kubeconfig export KUBECONFIG=/tmp/kubeconfig helm upgrade --install payment-api ./charts/payment-api \\ --namespace payment-api-production \\ --set image.tag=$GIT_COMMIT \\ --atomic --timeout 10m --wait rm -f /tmp/kubeconfig ''' } } }Milestone 4 -- Post conditions for notifications
post { success { slackSend( channel: '#deployments', color: 'good', message: "SUCCESS: ${env.JOB_NAME} #${env.BUILD_NUMBER} | ${env.GIT_COMMIT[0..7]}" ) } failure { slackSend( channel: '#deployments', color: 'danger', message: "FAILED: ${env.JOB_NAME} #${env.BUILD_NUMBER} | ${env.BUILD_URL}console" ) } always { cleanWs() } }Milestone 5 -- Jenkins shared libraries
// Shared library repository structure:jenkins-shared-library/ vars/ deployToKubernetes.groovy notifySlack.groovy src/ com/company/HelmDeploy.groovy// vars/deployToKubernetes.groovydef call(Map config) { def namespace = config.namespace ?: 'default' def imageTag = config.imageTag ?: error('imageTag required') def chart = config.chart ?: './charts/app' def valuesFile = config.valuesFile ?: 'values.yaml' sh """ helm upgrade --install ${config.releaseName} ${chart} \\ --namespace ${namespace} \\ --values ${valuesFile} \\ --set image.tag=${imageTag} \\ --atomic --timeout 5m --wait """} // Using in Jenkinsfile:('jenkins-shared-library') _pipeline { stages { stage('Deploy') { steps { deployToKubernetes( releaseName: 'payment-api', namespace: 'payment-api-staging', imageTag: env.GIT_COMMIT, valuesFile: 'charts/values-staging.yaml' ) } } }}Milestone 6 -- Dynamic Kubernetes agents
pipeline { agent { kubernetes { yaml '''apiVersion: v1kind: Podspec: containers: - name: node image: node:20-alpine command: [sleep, infinity] - name: docker image: docker:24-cli command: [sleep, infinity] volumeMounts: - name: docker-sock mountPath: /var/run/docker.sock volumes: - name: docker-sock hostPath: path: /var/run/docker.sock''' } } stages { stage('Test') { steps { container('node') { sh 'npm ci && npm test' } } } stage('Build') { steps { container('docker') { sh 'docker build -t payment-api .' } } } }}Production Best Practices and Common Pitfalls
| Mistake | Problem | Fix |
|---|---|---|
| Using scripted pipeline | Hard to read, no validation | Always use declarative pipeline |
| Hardcoding credentials | Secrets in Git history | Use withCredentials() binding |
| Global agent declaration | All stages on same machine | Use agent none + per-stage agents |
| No workspace cleanup | Disk fills up on agents | Add cleanWs() in post { always {} } |
| No build timeout | Hanging build blocks queue | Set timeout(time: 30, unit: 'MINUTES') |
Quick Reference and Troubleshooting Commands
| Task | Command |
|---|---|
| Validate Jenkinsfile syntax | Jenkins UI > Pipeline Syntax > Declarative Linter |
| Replay last build | Build > Replay (edit and rerun without commit) |
| Trigger via CLI | java -jar jenkins-cli.jar build JOB_NAME |
| View console log | curl $JENKINS_URL/job/JOB/lastBuild/consoleText |
| Abort a build | java -jar jenkins-cli.jar stop-build JOB 42 |
| List all jobs | java -jar jenkins-cli.jar list-jobs |
PLACEMENT PRO TIP**Tip:** Use the Jenkins Pipeline Syntax generator at `http://your-jenkins/pipeline-syntax/`. Select any installed plugin step from the dropdown, fill in the form fields, and it generates the correct Groovy syntax. Far faster than reading plugin documentation for every step you need.
REMEMBER THIS**Remember:** The `input` step pauses the build and holds a Jenkins executor slot. Set a timeout to prevent builds waiting indefinitely: `timeout(time: 24, unit: 'HOURS') { input 'Deploy to production?' }`. Without this, a forgotten approval gate can block all subsequent builds in the queue.
COMMON MISTAKE / WARNING**Security:** Never use `echo "${PASSWORD}"` in a Jenkinsfile -- this prints the credential to the build log visible to anyone with read access. Jenkins masks credentials injected via `withCredentials()` but only for exact string matches. A base64-encoded version of the password will not be masked.
COMMON MISTAKE / WARNING**Common Mistake:** Declaring a global agent at the top (`agent { label 'linux' }`) instead of `agent none`. A global agent runs ALL stages on the same machine, even stages needing different tools. Use `agent none` at the top and declare the appropriate agent per stage -- this enables parallel stages on different machines and proper tool isolation.