Chapter 66: Docker and Kubernetes Together: Dockerfile to Registry to Cluster, Jenkins and Kompose
66.4 Jenkins pipeline: build, test, push and deploy to Kubernetes
Why: the problem
Doing 66.1 to 66.3 by hand is fine once. On a team, every merge to main must repeat the same steps the same way: run tests, build the image, tag it with the commit, scan it, push it, update the Deployment and wait for the rollout. Jenkins is still the most common CI server in Indian enterprises, banks and IT services companies, so knowing how a Jenkinsfile drives Docker and kubectl is a very practical skill even if your next job uses GitHub Actions or GitLab CI. A declarative Jenkinsfile lives in the repository next to the Dockerfile. It has an agent, an environment block, stages and post actions. Credentials never appear in the file: they live in the Jenkins credentials store and are injected only inside withCredentials or the credentials() helper, which also masks them in the console log. The registry login uses a username and token credential; the cluster access uses a kubeconfig file credential for a ServiceAccount that can only update Deployments in one namespace, never cluster-admin. Where do builds run? The classic setup is a Jenkins agent VM with Docker installed. When Jenkins itself runs on Kubernetes, the Kubernetes plugin starts a fresh agent pod per build, with containers for python, kaniko or buildkit and kubectl. Mounting the host Docker socket into a pod gives that build root on the node, so in-cluster builds use rootless BuildKit or Kaniko instead. In this lesson we write a Jenkinsfile for the orders app, set up a least-privilege deployer ServiceAccount and kubeconfig, add credentials, and then write a Kubernetes-agent version that builds with Kaniko. Pushing to main produces a new tag, a rollout and an automatic rollback if the rollout fails.
What: every term in plain English
- Jenkinsfile — Pipeline as code stored in the repository.
- Declarative pipeline — pipeline { agent; environment; stages; post } syntax.
- Agent — Machine or pod that runs the build steps.
- Credentials store — Encrypted Jenkins store for tokens, passwords and kubeconfig files.
- withCredentials — Injects a credential into a block as environment variables or a file, masked in logs.
- GIT_COMMIT — Environment variable Jenkins sets to the commit being built.
- Kubernetes plugin — Runs each build in a temporary agent pod.
- Kaniko — Builds images inside a container without a Docker daemon.
- Deployer ServiceAccount — Kubernetes identity allowed only to patch Deployments in one namespace.
- post { failure } — Steps that run when the pipeline fails, such as rollback and alerts.
How: step by step
- Create a least-privilege deployer ServiceAccount. RBAC ready.
- Build a kubeconfig for it. File credential.
- Add Jenkins credentials. registry-creds and kubeconfig-orders.
- Write the Jenkinsfile stages. Test, build, push, deploy.
- Add rollout wait and rollback. Safe deploy.
- Run the pipeline. Green build.
- Switch to a Kubernetes agent with Kaniko. No Docker socket.
- Review logs for masked secrets. Asterisks only.
Example 1: Least-privilege deployer and its kubeconfig
Goal. Least-privilege deployer and its kubeconfig.
deployer.yaml
apiVersion: v1
kind: ServiceAccount
metadata: {name: jenkins-deployer, namespace: orders}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata: {name: deployer, namespace: orders}
rules:
- apiGroups: [apps]
resources: [deployments]
verbs: [get, list, watch, patch, update]
- apiGroups: [apps]
resources: [replicasets]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [pods]
verbs: [get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata: {name: jenkins-deployer, namespace: orders}
subjects: [{kind: ServiceAccount, name: jenkins-deployer, namespace: orders}]
roleRef: {kind: Role, name: deployer, apiGroup: rbac.authorization.k8s.io}
kubeconfig.sh
kubectl apply -f deployer.yaml
TOKEN=$(kubectl -n orders create token jenkins-deployer --duration=24h)
SERVER=$(kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}')
kubectl config view --minify --raw -o jsonpath='{.clusters[0].cluster.certificate-authority-data}' | base64 -d > ca.crt
kubectl --kubeconfig=jenkins.kubeconfig config set-cluster ch66 --server=$SERVER --certificate-authority=ca.crt --embed-certs=true
kubectl --kubeconfig=jenkins.kubeconfig config set-credentials deployer --token=$TOKEN
kubectl --kubeconfig=jenkins.kubeconfig config set-context ci --cluster=ch66 --user=deployer --namespace=orders
kubectl --kubeconfig=jenkins.kubeconfig config use-context ci
kubectl --kubeconfig=jenkins.kubeconfig auth can-i patch deployments
kubectl --kubeconfig=jenkins.kubeconfig auth can-i delete namespaces
chmod 600 jenkins.kubeconfig; rm -f ca.crt
What each line does
Role— Only patch Deployments and read pods in orders.create token --duration— Short-lived token; rotate it, do not create long-lived Secret tokens.auth can-i— Proves the limits before Jenkins uses the file.chmod 600— File readable only by you until it is uploaded as a Jenkins file credential.rm— Delete local copies after upload.
Green output
$ bash kubeconfig.sh
serviceaccount/jenkins-deployer created
role.rbac.authorization.k8s.io/deployer created
rolebinding.rbac.authorization.k8s.io/jenkins-deployer created
Switched to context "ci".
yes
no
Expected result. Jenkins gets a kubeconfig that can update orders Deployments and nothing else. Upload it under Manage Jenkins, Credentials, as a Secret file with ID kubeconfig-orders.
Example 2: The Jenkinsfile
Goal. The Jenkinsfile.
Jenkinsfile
pipeline {
agent { label 'docker' }
options { timeout(time: 20, unit: 'MINUTES'); disableConcurrentBuilds() }
environment {
REGISTRY = 'registry.example.com'
IMAGE = "${REGISTRY}/orders"
TAG = "1.0.${BUILD_NUMBER}-${GIT_COMMIT.take(7)}"
}
stages {
stage('Test') {
steps { sh 'python3 -m venv .v && . .v/bin/activate && pip install -q -r requirements.txt pytest && pytest -q' }
}
stage('Build') {
steps { sh 'docker build --pull -t $IMAGE:$TAG .' }
}
stage('Scan') {
steps { sh 'trivy image --exit-code 1 --severity CRITICAL $IMAGE:$TAG' }
}
stage('Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'RU', passwordVariable: 'RP')]) {
sh 'echo "$RP" | docker login $REGISTRY -u "$RU" --password-stdin && docker push $IMAGE:$TAG'
}
}
}
stage('Deploy') {
when { branch 'main' }
steps {
withCredentials([file(credentialsId: 'kubeconfig-orders', variable: 'KUBECONFIG')]) {
sh 'kubectl set image deploy/orders orders=$IMAGE:$TAG'
sh 'kubectl annotate deploy/orders kubernetes.io/change-cause="jenkins $TAG" --overwrite'
sh 'kubectl rollout status deploy/orders --timeout=180s'
}
}
}
}
post {
failure {
withCredentials([file(credentialsId: 'kubeconfig-orders', variable: 'KUBECONFIG')]) {
sh 'kubectl rollout undo deploy/orders || true'
}
}
always { sh 'docker logout $REGISTRY || true; docker image rm $IMAGE:$TAG || true' }
}
}
What each line does
TAG— Build number plus short commit: unique and traceable.Single quotes in sh— The shell expands secrets, not Groovy, so they are not interpolated into the log.--password-stdin— Token never appears on the command line.when branch main— Feature branches test and push but do not deploy.post failure— Automatic rollback if rollout status times out.
Green output
[Pipeline] stage (Test)
4 passed in 0.31s
[Pipeline] stage (Build)
naming to registry.example.com/orders:1.0.42-a1b2c3d
[Pipeline] stage (Scan)
Total: 0 (CRITICAL: 0)
[Pipeline] stage (Push)
+ docker login registry.example.com -u **** --password-stdin
Login Succeeded
[Pipeline] stage (Deploy)
deployment.apps/orders image updated
deployment "orders" successfully rolled out
Finished: SUCCESS
Expected result. A merge to main tests, builds, scans, pushes and rolls out with masked credentials and an automatic rollback path.
Example 3: Kubernetes agent with Kaniko, no Docker socket
Goal. Kubernetes agent with Kaniko, no Docker socket.
Jenkinsfile.k8s
pipeline {
agent {
kubernetes {
yaml '''
apiVersion: v1
kind: Pod
spec:
serviceAccountName: jenkins-agent
containers:
- name: python
image: python:3.12-slim
command: [sleep]
args: [infinity]
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
command: [sleep]
args: [infinity]
volumeMounts: [{name: regcred, mountPath: /kaniko/.docker}]
- name: kubectl
image: bitnami/kubectl:1.34
command: [sleep]
args: [infinity]
volumes:
- name: regcred
secret:
secretName: regcred
items: [{key: .dockerconfigjson, path: config.json}]
'''
}
}
environment { IMAGE = 'registry.example.com/orders'; TAG = "1.0.${BUILD_NUMBER}-${GIT_COMMIT.take(7)}" }
stages {
stage('Test') { steps { container('python') { sh 'pip install -q -r requirements.txt pytest && pytest -q' } } }
stage('Build and push') { steps { container('kaniko') { sh '/kaniko/executor --context . --destination $IMAGE:$TAG' } } }
stage('Deploy') { steps { container('kubectl') { sh 'kubectl -n orders set image deploy/orders orders=$IMAGE:$TAG && kubectl -n orders rollout status deploy/orders' } } }
}
}
What each line does
kubernetes agent— A fresh pod per build, deleted afterwards.kaniko— Builds and pushes without a Docker daemon or privileged mode.regcred volume— The docker-registry Secret from 66.3 mounted as Kaniko's config.json.jenkins-agent SA— Bound to the same deployer Role, so the pod needs no kubeconfig file.container()— Runs each step in the right container of the agent pod.
Green output
Created Pod: jenkins/orders-main-42-xk2lp
[Pipeline] container (python)
4 passed
[Pipeline] container (kaniko)
INFO Pushing image to registry.example.com/orders:1.0.42-a1b2c3d
INFO Pushed registry.example.com/orders@sha256:3e9a...
[Pipeline] container (kubectl)
deployment "orders" successfully rolled out
Finished: SUCCESS
Expected result. The same pipeline runs inside the cluster with no Docker socket, no privileged pod and no kubeconfig file.
Every command, step by step (kind lab with kubectl)
-
RBAC
kubectl apply -f deployer.yamlWhat it does: Deployer.
role.rbac.authorization.k8s.io/deployer created -
Token
kubectl -n orders create token jenkins-deployer --duration=24hWhat it does: Short-lived.
eyJhbGciOi... (redacted) -
can-i
kubectl --kubeconfig=jenkins.kubeconfig auth can-i patch deploymentsWhat it does: Allowed.
yes -
can-i no
kubectl --kubeconfig=jenkins.kubeconfig auth can-i delete namespacesWhat it does: Denied.
no -
Credential
Manage Jenkins, Credentials, Secret file kubeconfig-ordersWhat it does: Stored.
Credential created -
Registry
Username with password registry-credsWhat it does: Stored.
Credential created -
Pipeline
New Item, Multibranch Pipeline, point to repoWhat it does: Job.
Scan Multibranch Pipeline Triggered -
Build
Build Now on mainWhat it does: Green.
Finished: SUCCESS -
History
kubectl -n orders rollout history deploy/ordersWhat it does: Revision.
3 jenkins 1.0.42-a1b2c3d -
Fail test
Push a broken readiness pathWhat it does: Rollback.
error: timed out waiting ... rolled back
When it is red 1
docker: permission denied while trying to connect to the Docker daemon socket
Cause. Jenkins user not allowed to use Docker on the agent.
Fix, step by step
- Add the jenkins user to the docker group on that agent VM, or use Kaniko or BuildKit.
When it is red 2
Error from server (Forbidden): deployments.apps "orders" is forbidden
Cause. Deployer Role missing a verb or wrong namespace.
Fix, step by step
- kubectl auth can-i with the same kubeconfig; fix the Role.
When it is red 3
Warning: A secret was passed to "sh" using Groovy String interpolation
Cause. Double quotes put the secret into the Groovy string.
Fix, step by step
- Use single quotes so the shell expands it.
When it is red 4
error: timed out waiting for the condition
Cause. New pods never became ready.
Fix, step by step
- Check probes and logs; post failure rolls back.
When it is red 5
kaniko: UNAUTHORIZED
Cause. config.json missing or for the wrong registry.
Fix, step by step
- Mount the docker-registry Secret as /kaniko/.docker/config.json.
Practice tasks
Easy Task 1: RBAC
Why: Deployer.
What to do: Apply deployer.yaml.
- Role and binding.
Solution
kubectl -n orders get role,rolebindingEasy Task 2: Kubeconfig
Why: Build.
What to do: Run kubeconfig.sh.
- can-i yes/no.
Solution
kubectl --kubeconfig=jenkins.kubeconfig get deployEasy Task 3: Credentials
Why: Store.
What to do: Add both credentials.
- IDs match.
Solution
Manage Jenkins, CredentialsMedium Task 4: Jenkinsfile
Why: Write.
What to do: Five stages.
- when main.
Solution
Pipeline Syntax, ValidateMedium Task 5: Run
Why: Build.
What to do: Build main.
- Masked.
Solution
Console OutputMedium Task 6: Rollback
Why: Fail.
What to do: Break readiness.
- post failure.
Solution
kubectl -n orders rollout history deploy/ordersHard Task 7: Kaniko
Why: Agent pod.
What to do: Jenkinsfile.k8s.
- regcred.
Solution
kubectl -n jenkins get podsHard Task 8: HDFC
Why: Main task.
What to do: statements pipeline.
- Least privilege.
Solution
kubectl -n statements rollout history deploy/statementsHomework: try at home
For HDFC's statements service, create a deployer ServiceAccount limited to namespace statements, store its kubeconfig and the registry token as Jenkins credentials, and write a Jenkinsfile that tests, builds, scans, pushes a build-number-plus-commit tag, deploys only from main and rolls back on failure.
Recap
- A Jenkinsfile in the repo repeats test, build, scan, push and deploy identically on every merge.
- Credentials stay in Jenkins and are injected and masked with withCredentials.
- The deployer identity can only update Deployments in one namespace.
- Wait for rollout status and roll back in post failure.
- Inside Kubernetes, build with Kaniko or BuildKit instead of the Docker socket.
FAQ
Q. Should Jenkins deploy directly or update Git for Argo CD?
Both work. Many teams let Jenkins build and push, then commit the new tag to a GitOps repo so Argo CD deploys, as in ch64.
Q. How do I trigger builds on push?
A webhook from your Git server to the Multibranch Pipeline job, or polling as a fallback.
Q. Can Jenkins use IAM instead of tokens on EKS?
Yes. Run agents on EKS with EKS Pod Identity or IRSA so they get ECR and EKS access without stored keys.
Q. How do I keep the Jenkins controller safe?
Run no builds on the controller, keep plugins updated, use RBAC in Jenkins, and back up JENKINS_HOME.
Interview questions
-
Walk through your CI/CD pipeline for a containerised app.
Model answer: Test, build, scan, push immutable tag, deploy with rollout wait, roll back on failure.
-
How do you secure credentials in Jenkins?
Model answer: Credentials store, withCredentials, single-quoted shell, least-privilege identities, short-lived tokens.
-
Why not mount docker.sock in a build pod?
Model answer: It gives root on the node; use Kaniko or rootless BuildKit.
-
How do you trace a running pod to a build?
Model answer: Tag and change-cause contain build number and commit.
Quick quiz
Q1. Where do credentials live?
Jenkins credentials store, injected with withCredentials.
Q2. Why single quotes in sh steps?
So secrets are not interpolated into Groovy and logged.
Q3. What should the deployer be allowed to do?
Patch Deployments in one namespace only.
Q4. Why Kaniko inside Kubernetes?
No Docker socket or privileged pod.
Q5. What runs in post failure?
kubectl rollout undo.
Samajla ka?