In this tutorial, we’ll walk through a complete CI/CD workflow that builds a Spring Boot application, runs tests, publishes a Docker image, and deploys it to a Kubernetes cluster using a Jenkins Pipeline. Our Git repo contains a Kubernetes manifest (k8s_deployment_service.yaml) defining both a Deployment and a Service.
Table of Contents
Jenkins Pipeline (Jenkinsfile)
This pipeline leverages Maven for build and test, Docker for image creation and pushing, and the Kubernetes CLI (kubectl) for deployment.
pipeline {
agent any
stages {
stage( 'Build Artifact - Maven' ) {
steps {
sh 'mvn clean package -DskipTests=true'
archiveArtifacts 'target/*.jar'
}
}
stage( 'Unit Tests - JUnit & JaCoCo' ) {
steps {
sh 'mvn test'
}
post {
always {
junit 'target/surefire-reports/*.xml'
jacoco execPattern : 'target/jacoco.exec'
}
}
}
stage( 'Docker Build & Push' ) {
steps {
withDockerRegistry([ credentialsId : 'docker-hub' , url : '' ]) {
sh 'docker build -t siddharth67/numeric-app:${GIT_COMMIT} .'
sh 'docker push siddharth67/numeric-app:${GIT_COMMIT}'
}
}
}
stage( 'Kubernetes Deployment - DEV' ) {
steps {
withKubeConfig([ credentialsId : 'kubeconfig' ]) {
sh 'sed -i "s#replace#siddharth67/numeric-app:${GIT_COMMIT}#g" k8s_deployment_service.yaml'
sh 'kubectl apply -f k8s_deployment_service.yaml'
}
}
}
}
}
Kubernetes Deployment & Service Manifest
The k8s_deployment_service.yaml file defines:
A Deployment named devsecops with 2 replicas.
A Service of type NodePort exposing port 8080.
apiVersion : apps/v1
kind : Deployment
metadata :
name : devsecops
labels :
app : devsecops
spec :
replicas : 2
selector :
matchLabels :
app : devsecops
template :
metadata :
labels :
app : devsecops
spec :
containers :
- name : devsecops-container
image : replace
---
apiVersion : v1
kind : Service
metadata :
name : devsecops-svc
labels :
app : devsecops
spec :
type : NodePort
selector :
app : devsecops
ports :
- port : 8080
targetPort : 8080
protocol : TCP
Resource Type Purpose Example Command Deployment Manages replicated Pods kubectl apply -f k8s_deployment_service.yamlService Exposes Pods internally/externally kubectl expose ...
Ensure you replace the placeholder replace with your Docker image tag (siddharth67/numeric-app:${GIT_COMMIT}) before applying the manifest.
Deploying the Node.js Service
We need a backend service that our Spring Boot app will call. Deploy a pre-built Node.js service (siddharth67/node-service:v1) in the default namespace:
# Create the Deployment
kubectl -n default create deployment node-app --image=siddharth67/node-service:v1
# Expose as ClusterIP on port 5000
kubectl -n default expose deployment node-app \
--name=node-service \
--port=5000 \
--target-port=5000
# Verify
kubectl -n default get all
Expected output:
NAME READY STATUS RESTARTS AGE
pod/node-app-6b8496465-tsrkf 1/1 Running 0 24s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT ( S ) AGE
service/node-service ClusterIP 10.96.198.94 < non e > 5000/TCP 4s
The Node.js service is now reachable at http://node-service:5000.
Numeric Spring Boot Application
Update your controller to call the Kubernetes service instead of localhost. Example:
package com.devsecops;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation. * ;
import org.springframework.web.client.RestTemplate;
@ RestController
public class NumericController {
private final Logger logger = LoggerFactory . getLogger ( NumericController . class );
private static final String BASE_URL = "http://node-service:5000/plussone" ;
private final RestTemplate restTemplate ;
public NumericController ( RestTemplateBuilder builder ) {
this . restTemplate = builder . build ();
}
@ GetMapping ( "/" )
public String welcome () {
return "Kubernetes DevSecOps" ;
}
@ GetMapping ( "/increment/{value}" )
public String increment (@ PathVariable int value ) {
ResponseEntity < String > response =
restTemplate . getForEntity (BASE_URL + "/" + value, String . class );
return response . getBody ();
}
@ GetMapping ( "/compare/{value}" )
public String compareToFifty (@ PathVariable int value ) {
return (value < 50 ) ? "Less than 50" : "Greater than 50" ;
}
}
Commit all changes to your main branch—Jenkins will automatically trigger the pipeline.
Verifying the Deployment
After the pipeline completes, verify both applications:
kubectl -n default get all
Sample output:
NAME READY STATUS RESTARTS AGE
pod/devsecops-xxxxx-xxxxx 1/1 Running 0 2m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT ( S ) AGE
service/devsecops-svc NodePort 10.96.xxx.xxx < non e > 8080:31933/TCP 2m
service/node-service ClusterIP 10.96.xxx.xxx < non e > 5000/TCP 5m
Access the Numeric application via NodePort:
http://<NODE_IP>:31933/ → Kubernetes DevSecOps
http://<NODE_IP>:31933/increment/77 → 78
http://<NODE_IP>:31933/compare/44 → Less than 50
http://<NODE_IP>:31933/compare/95 → Greater than 50
Both services communicate seamlessly within the Kubernetes cluster.
Links & References