$ ./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-spring-k8s
Spring on Kubernetes
What You Will Build
Kubernetes in Spring environments is maturing. According to the 2024 State of Spring Survey, 65% of respondents are using Kubernetes in their Spring environment.
Before you can run a Spring Boot application on Kubernetes, you first must generate a container image. Spring Boot supports using Cloud Native Buildpacks to easily generate a Docker image from your Maven or Gradle plugin.
The goal of this guide is to show you how you can run a Spring Boot application on Kubernetes and take advantage of several of the platform features that let you build cloud-native applications.
In this guide you will build two Spring Boot web applications. You will package each web application into a Docker image using Cloud Native Buildpacks, create a kubernetes deployment based on that image, and create a service for access to the deployment.
What You Need
-
A favorite text editor or IDE
-
Java 17 or later
-
A Docker environment
-
A Kubernetes environment
Docker Desktop provides both the Docker and Kubernetes environments necessary to follow along in this guide. |
How to Complete This Guide
This guide focuses on creating the necessary artifacts to run Spring Boot apps on kubernetes. As such, the best way to follow along is to use the code provided in this repository.
This repository provides two services that we will use:
-
hello-spring-k8s
is a basic Spring Boot REST application that will echo a Hello World message. -
hello-caller
will call the Spring Boot REST applicationhello-spring-k8s
. Thehello-caller
service is to demonstrate how service discovery works in a kubernetes environment.
Both of these applications are Spring Boot REST applications and can be created from scratch using this guide. The code specific to this guide is called out below as the lessons progress.
This guide is separated into distinct sections.
In the solution repository, you will find the kubernetes artifacts have already been created. This guide walks you through creating these objects step by step, but you can refer to the solutions at any time for a working example.
Generate a Docker Image
First, generate a Docker Image of the hello-spring-k8s
project using Cloud Native Buildpacks. In the hello-spring-k8s
directory, run the command:
This will generate a Docker Image with the name spring-k8s/hello-spring-k8s
. After the build finishes, we should now have a Docker image for our application, which we can check with the following command:
$ docker images spring-k8s/hello-spring-k8s
REPOSITORY TAG IMAGE ID CREATED SIZE
spring-k8s/hello-spring-k8s latest <ID> 44 years ago 325MB
Now we can start the container image and make sure it works:
$ docker run -p 8080:8080 --name hello-spring-k8s -t spring-k8s/hello-spring-k8s
We can test that everything is working by making an HTTP request to the actuator/health endpoint:
$ curl http://localhost:8080/actuator/health
{"status":"UP"}
Before moving on be sure to stop the running container.
$ docker stop hello-spring-k8s
Kubernetes Requirements
With a container image for our application (with nothing more than a visit to start.spring.io!) we are ready to get our application running on Kubernetes. To do this, we need two things:
-
The Kubernetes CLI (kubectl)
-
A Kubernetes cluster to which to deploy our application
Follow these instructions to install the Kubernetes CLI.
Any Kubernetes cluster can work, but, for the purpose of this post, we spin one up locally to make it as simple as possible. The easiest way to run a Kubernetes cluster locally is with Docker Desktop.
There are some common kubernetes flags used throughout the tutorial that are worth noting. The --dry-run=client
flag tells kubernetes to only print the object that would be sent, without sending it. The -o yaml
flag specifies that the output of the command should be yaml. These two flags are used in conjunction with the output redirection >
so that the kubernetes commands can be captured in a file. This is useful for editing objects prior to creation as well as creating a repeatable process.
Deploying To Kubernetes
The solution to this section is defined in k8s-artifacts/basic/*
.
To deploy our hello-spring-k8s
application to Kubernetes, we need to generate some YAML that Kubernetes can use to deploy, run, and manage our application as well as expose that application to the rest of the cluster.
If you are choosing to build the yaml yourself instead of running the provided solution, first create a directory for your YAML. It does not matter where this folder resides, as the yaml files we generate will not be dependent on the path.
$ mkdir k8s
$ cd k8s
Now we can use kubectl to generate the basic YAML we need:
$ kubectl create deployment gs-spring-boot-k8s --image spring-k8s/spring-k8s/hello-spring-k8s:latest -o yaml --dry-run=client > deployment.yaml
Because the image we are using is local, we need to change the imagePullPolicy
for the container in our deployment. The containers:
spec of the yaml should now be:
spec:
containers:
- image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
If you attempt to run the deployment without modifying the imagePullPolicy
, your pod will have a status of ErrImagePull
.
The deployment.yaml
file tells Kubernetes how to deploy and manage our application, but it does not let our application be a network service to other applications. To do that, we need a service resource. Kubectl can help us generate the YAML for the service resource:
$ kubectl create service clusterip gs-spring-boot-k8s --tcp 80:8080 -o yaml --dry-run=client > service.yaml
Now we are ready to apply the YAML files to Kubernetes:
$ kubectl apply -f deployment.yaml
$ kubectl apply -f service.yaml
Then you can run:
$ kubectl get all
You should see our newly created deployment, service, and pod running:
NAME READY STATUS RESTARTS AGE
pod/gs-spring-boot-k8s-779d4fcb4d-xlt9g 1/1 Running 0 3m40s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/gs-spring-boot-k8s ClusterIP 10.96.142.74 <none> 80/TCP 3m40s
service/kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 4h55m
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/gs-spring-boot-k8s 1/1 1 1 3m40s
NAME DESIRED CURRENT READY AGE
replicaset.apps/gs-spring-boot-k8s-779d4fcb4d 1 1 1 3m40s
Unfortunately, we cannot make an HTTP request to the service in Kubernetes directly, because it is not exposed outside of the cluster network. With the help of kubectl we can forward HTTP traffic from our local machine to the service running in the cluster:
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
With the port-forward command running, we can now make an HTTP request to localhost:9090, and it is forwarded to the service running in Kubernetes:
$ curl http://localhost:9090/helloWorld
Hello World!!
Before moving on be sure to stop the port-forward
command above.
Best Practices
The solution to this section is defined in k8s-artifacts/best_practice/*
.
Our application runs on Kubernetes, but, in order for our application to run optimally, we recommend implementing the best practices:
Open deployment.yaml
in a text editor and add the readiness, and liveness properties to your file:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 1
selector:
matchLabels:
app: gs-spring-boot-k8s
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
spec:
containers:
- image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
status: {}
This will address the first best practice. Additionally, we need to add a property to our application configuration. Since we run our application on Kubernetes, we can take advantage of Kubernetes ConfigMaps to externalize this property, as a good cloud developer should. We now take a look at how to do that.
Using ConfigMaps To Externalize Configuration
The solution to this section is defined in k8s-artifacts/config_map/*
.
To enable graceful shutdown in a Spring Boot application, we could set server.shutdown=graceful
in application.properties
. Rather than adding this line directly to our code, let’s use a ConfigMap. We can use the Actuator endpoints as a way of verifying that our application is adding the properties file from our ConfigMap to the list of PropertySources.
We can create a properties file that enables graceful shutdown and also exposes all of the Actuator endpoints. We can use the Actuator endpoints as a way of verifying that our application is adding the properties file from our ConfigMap to the list of PropertySources.
Create a new file called application.properties
where you are keeping your yaml files. In that file add the following properties.
server.shutdown=graceful
management.endpoints.web.exposure.include=*
Alternatively you can do this in one easy step from the command line by running the following command.
$ cat <<EOF >./application.properties
server.shutdown=graceful
management.endpoints.web.exposure.include=*
EOF
With our properties file created, we can now create a ConfigMap with kubectl.
$ kubectl create configmap gs-spring-boot-k8s --from-file=./application.properties
With our ConfigMap created, we can see what it looks like:
$ kubectl get configmap gs-spring-boot-k8s -o yaml
apiVersion: v1
data:
application.properties: |
server.shutdown=graceful
management.endpoints.web.exposure.include=*
kind: ConfigMap
metadata:
creationTimestamp: "2020-09-10T21:09:34Z"
name: gs-spring-boot-k8s
namespace: default
resourceVersion: "178779"
selfLink: /api/v1/namespaces/default/configmaps/gs-spring-boot-k8s
uid: 9be36768-5fbd-460d-93d3-4ad8bc6d4dd9
The last step is to mount this ConfigMap as a volume in the container.
To do this, we need to modify our deployment YAML to first create the volume and then mount that volume in the container:
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 1
selector:
matchLabels:
app: gs-spring-boot-k8s
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
spec:
containers:
- image: spring-k8s/hello-spring-k8s
imagePullPolicy: Never
name: hello-spring-k8s
resources: {}
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
volumeMounts:
- name: config-volume
mountPath: /workspace/config
volumes:
- name: config-volume
configMap:
name: gs-spring-boot-k8s
status: {}
With all of our best practices implemented, we can apply the new deployment to Kubernetes. This deploys another Pod and stops the old one (as long as the new one starts successfully).
$ kubectl apply -f deployment.yaml
If your liveness and readiness probes are configured correctly, the Pod starts successfully and transitions to a ready state. If the Pod never reaches the ready state, go back and check your readiness probe configuration. If your Pod reaches the ready state but Kubernetes constantly restarts the Pod, your liveness probe is not configured properly. If the pod starts and stays up, everything is working fine.
You can verify that the ConfigMap volume is mounted and that the application is using the properties file by hitting the /actuator/env
endpoint.
$ kubectl port-forward svc/gs-spring-boot-k8s 9090:80
Now if you visit http://localhost:9090/actuator/env you will see property sources contributed from our mounted volume.
curl http://localhost:9090/actuator/env | jq
{
"name":"applicationConfig: [file:./config/application.properties]",
"properties":{
"server.shutdown":{
"value":"graceful",
"origin":"URL [file:./config/application.properties]:1:17"
},
"management.endpoints.web.exposure.include":{
"value":"*",
"origin":"URL [file:./config/application.properties]:2:43"
}
}
}
Before continuing, be sure to stop the port-forward
command.
Service Discovery and Load Balancing
This part of the guide adds the hello-caller
application. The solution to this section is defined in k8s-artifacts/service_discovery/*
.
To demonstrate load balancing, let’s first scale our existing hello-spring-k8s
service to 3 replicas. This can be done by adding the replicas
configuration to your deployment.
...
metadata:
creationTimestamp: null
labels:
app: gs-spring-boot-k8s
name: gs-spring-boot-k8s
spec:
replicas: 3
selector:
...
Update the deployment by running the command:
kubectl apply -f deployment.yaml
Now we should see 3 pods running:
$ kubectl get pod --selector=app=gs-spring-boot-k8s
NAME READY STATUS RESTARTS AGE
gs-spring-boot-k8s-76477c6c99-2psl4 1/1 Running 0 15m
gs-spring-boot-k8s-76477c6c99-ss6jt 1/1 Running 0 3m28s
gs-spring-boot-k8s-76477c6c99-wjbhr 1/1 Running 0 3m28s
We need to run a second service for this section, so let’s turn our attention to hello-caller
. This application has a single endpoint that in turn calls hello-spring-k8s
. Note that the URL is the same as the service name in kubernetes.
@GetMapping
public Mono<String> index() {
return webClient.get().uri("http://gs-spring-boot-k8s/name")
.retrieve()
.toEntity(String.class)
.map(entity -> {
String host = entity.getHeaders().get("k8s-host").get(0);
return "Hello " + entity.getBody() + " from " + host;
});
}
Kubernetes sets up DNS entries so that we can use the service ID for the hello-spring-k8s
to make an HTTP request to the service without knowing the IP address of the pods. The Kubernetes service also load balances these requests between all the pods.
We now need to package the hello-caller
application as a Docker image and run it as a kubernetes resource. To generate a Docker image we will once again use Cloud Native Buildpacks. In the hello-caller
folder, run the command:
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=spring-k8s/hello-caller
When the Docker image is created, you can create a new deployment similar to the one that we have already seen. A completed configuration is provided in the caller_deployment.yaml
file. Run this file:
kubectl apply -f caller_deployment.yaml
We can verify that the app is running with the command:
$ kubectl get pod --selector=app=gs-hello-caller
NAME READY STATUS RESTARTS AGE
gs-hello-caller-774469758b-qdtsx 1/1 Running 0 2m34s
We also need to create a service, as defined in the provided file caller_service.yaml
. This file can be run with the command:
kubectl apply -f caller_service.yaml
Now that you have two deployments and two services running, you are ready to test the application.
$ kubectl port-forward svc/gs-hello-caller 9090:80
$ curl http://localhost:9090 -i; echo
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 4
Date: Mon, 14 Sep 2020 15:37:51 GMT
Hello Paul from gs-spring-boot-k8s-76477c6c99-5xdq8
If you make multiple requests, you should see different names returned. The pod name is also listed in the request. This value will also change if you submit multiple requests. While waiting for the kubernetes load balancer to select a different pod, you can speed up the process by deleting the pod that returned the most recent request.
$ kubectl delete pod gs-spring-boot-k8s-76477c6c99-5xdq8
Summary
Getting a Spring Boot application running on Kubernetes requires nothing more than a visit to start.spring.io. The goal of Spring Boot has always been to make building and running Java applications as easy as possible, and we try to enable that, no matter how you choose to run your application. Building cloud-native applications with Kubernetes involves nothing more than creating an image that uses Spring Boot’s built-in image builder and taking advantage of the capabilities of the Kubernetes platform.