Spring Boot on Kubernetes with Eclipse JKube
This article will teach you how to use the Eclipse JKube project to build images and generate Kubernetes manifests for the Spring Boot application. Eclipse JKube is a collection of plugins and libraries that we can use to build container images using Docker, Jib, or source-2-image (S2I) build strategies. It also generates and deploys Kubernetes and OpenShift manifests at compile time. We can include it as the Maven or Gradle plugin to use it during our build process. On the other hand, Spring Boot doesn’t provide any built-in tools to simplify deployment to Kubernetes. It only provides the build-image
goal within the Spring Boot Maven and Gradle plugins dedicated to building container images with Cloud Native Buildpacks. Let’s check out how Eclipse JKube can simplify our interaction with Kubernetes. By the way, it also provides tools for watching, debugging, and logging.
You can find other interesting articles on my blog if you are interested in the tools for generating Kubernetes manifests. Here’s the article that shows how to use the Dekorate library to generate Kubernetes manifests for the Spring Boot app.
Source Code
If you would like to try this exercise by yourself, you may always take a look at my source code. First, you need to clone the following GitHub repository. It contains several sample Java applications for a Kubernetes showcase. You must go to the “inner-dev-loop” directory, to proceed with exercise. Then you should follow my further instructions.
Prerequisites
Before we start the development, we must install some tools on our laptops. Of course, we should have Maven and at least Java 21 installed. We must also have access to the container engine (like Docker or Podman) and a Kubernetes cluster. I have everything configured on my local machine using Podman Desktop and Minikube. Finally, we need to install the Helm CLI. It can be used to deploy the Postgres database on Kubernetes using the popular Bitnami Helm chart. In summary, we need to have:
- Maven
- OpenJDK 21+
- Podman or Docker
- Kubernetes
- Helm CLI
Once we have everything in place, we can proceed to the next steps.
Create Spring Boot Application
In this exercise, we create a typical Spring Boot application that connects to the relational database and exposes REST endpoints for the basic CRUD operations. Both the application and database will run on Kubernetes. We install the Postgres database using the Bitnami Helm chart. To build and deploy the application in the Kubernetes cluster, we will use Maven and Eclipse JKube features. First, let’s take a look at the source code of our application. Here’s the list of included dependencies. It’s worth noting that Spring Boot Actuator is responsible for generating Kubernetes liveness and readiness health checks. JKube will be able to detect it and generate the required elements in the Deployment
manifest.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
XMLHere’s our Person
entity class:
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String firstName;
private String lastName;
private int age;
@Enumerated(EnumType.STRING)
private Gender gender;
private Integer externalId;
// getters and setters
}
JavaWe use the well-known Spring Data repository pattern to implement the data access layer. Here’s our PersonRepository
interface. There are the additional method to search persons by age.
public interface PersonRepository extends CrudRepository<Person, Long> {
List<Person> findByAgeGreaterThan(int age);
}
JavaFinally, we can implement the REST controller using the previously created PersonRepository
to interact with the database.
@RestController
@RequestMapping("/persons")
public class PersonController {
private static final Logger LOG = LoggerFactory
.getLogger(PersonController.class);
private final PersonRepository repository;
public PersonController(PersonRepository repository) {
this.repository = repository;
}
@GetMapping
public List<Person> getAll() {
LOG.info("Get all persons");
return (List<Person>) repository.findAll();
}
@GetMapping("/{id}")
public Person getById(@PathVariable("id") Long id) {
LOG.info("Get person by id={}", id);
return repository.findById(id).orElseThrow();
}
@GetMapping("/age/{age}")
public List<Person> getByAgeGreaterThan(@PathVariable("age") int age) {
LOG.info("Get person by age={}", age);
return repository.findByAgeGreaterThan(age);
}
@DeleteMapping("/{id}")
public void deleteById(@PathVariable("id") Long id) {
LOG.info("Delete person by id={}", id);
repository.deleteById(id);
}
@PostMapping
public Person addNew(@RequestBody Person person) {
LOG.info("Add new person: {}", person);
return repository.save(person);
}
}
JavaHere’s the full list of configuration properties. The database name and connection credentials are configured through environment variables: DATABASE_NAME
, DATABASE_USER
, and DATABASE_PASS
. We should enable the exposure of the Kubernetes liveness and readiness health checks. After that, we include the database component status in the readiness probe.
spring:
application:
name: inner-dev-loop
datasource:
url: jdbc:postgresql://person-db-postgresql:5432/${DATABASE_NAME}
username: ${DATABASE_USER}
password: ${DATABASE_PASS}
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
show_sql: true
format_sql: true
management:
info.java.enabled: true
endpoints:
web:
exposure:
include: "*"
endpoint.health:
show-details: always
group:
readiness:
include: db
probes:
enabled: true
YAMLInstall Postgres on Kubernetes
We begin our interaction with Kubernetes from the database installation. We use the Bitnami Helm chart for that. In the first step, we must add the Bitnami repository:
helm repo add bitnami https://charts.bitnami.com/bitnami
ShellSessionThen, we can install the Postgres chart under the person-db
name. During the installation, we create the spring
user and the database under the same name.
helm install person-db bitnami/postgresql \
--set auth.username=spring \
--set auth.database=spring
ShellSessionPostgres is accessible inside the cluster under the person-db-postgresql
name.
$ kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
person-db-postgresql ClusterIP 10.96.115.19 <none> 5432/TCP 23s
person-db-postgresql-hl ClusterIP None <none> 5432/TCP 23s
ShellSessionHelm chart generates a Kubernetes Secret under the same name person-db-postgresql
. The password for the spring
user is automatically generated during installation. We can retrieve that password from the password
field.
$ kubectl get secret person-db-postgresql -o yaml
apiVersion: v1
data:
password: UkRaalNYU3o3cA==
postgres-password: a1pBMFFuOFl3cQ==
kind: Secret
metadata:
annotations:
meta.helm.sh/release-name: person-db
meta.helm.sh/release-namespace: demo
creationTimestamp: "2024-10-03T14:39:19Z"
labels:
app.kubernetes.io/instance: person-db
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: postgresql
app.kubernetes.io/version: 17.0.0
helm.sh/chart: postgresql-16.0.0
name: person-db-postgresql
namespace: demo
resourceVersion: "61646"
uid: 00b0cf7e-8521-4f53-9c69-cfd3c942004c
type: Opaque
ShellSessionJKube with Spring Boot in Action
With JKube, we can build a Spring Boot app image and deploy it on Kubernetes with a single command without creating any YAML or Dockerfile. To use Eclipse JKube, we must include the org.eclipse.jkube:kubernetes-maven-plugin
plugin to the Maven pom.xml
. The plugin configuration contains values for two environment variables DATABASE_USER
and DATABASE_NAME
required by our Spring Boot application. We also set the memory and CPU request for the Deployment
, which is obviusly a good practice.
<plugin>
<groupId>org.eclipse.jkube</groupId>
<artifactId>kubernetes-maven-plugin</artifactId>
<version>1.17.0</version>
<configuration>
<resources>
<controller>
<env>
<DATABASE_USER>spring</DATABASE_USER>
<DATABASE_NAME>spring</DATABASE_NAME>
</env>
<containerResources>
<requests>
<memory>256Mi</memory>
<cpu>200m</cpu>
</requests>
</containerResources>
</controller>
</resources>
</configuration>
</plugin>
XMLWe can use the resource fragments to generate a more advanced YAML manifest with properties not covered by the plugin’s XML fields. Such a fragment of the YAML manifest must be placed in the src/main/jkube
directory. In our case, the password to the database must be injected from the Kubernetes person-db-postgresql
Secret
generated by the Bitnami Helm chart. Here’s the fragment of Deployment
YAML in the deployment.yml
file:
spec:
template:
spec:
containers:
- env:
- name: DATABASE_PASS
valueFrom:
secretKeyRef:
key: password
name: person-db-postgresql
src/main/jkube/deployment.ymlIf we want to build the image and generate Kubernetes manifests without applying them to the cluster we can use the goals k8s:build
and k8s:resource
during the Maven build.
mvn clean package -DskipTests k8s:build k8s:resource
ShellSessionLet’s take a look at the logs from the k8s:build
phase. JKube reads the image group from the last part of the Maven group ID and replaces the version that contains the -SNAPSHOT
suffix with the latest
tag.
Here are the logs from k8s:resource
phase. As you see, JKube reads the Spring Boot management.health.probes.enabled
configuration property and includes /actuator/health/liveness
and /actuator/health/readiness
endpoints as the probes.
Here’s the Deployment
object generated by the JKube plugin for our Spring Boot application.
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
jkube.eclipse.org/scm-url: https://github.com/spring-projects/spring-boot/inner-dev-loop
jkube.eclipse.org/scm-tag: HEAD
jkube.eclipse.org/git-commit: 92b2e11f7ddb134323133aee0daa778135500113
jkube.eclipse.org/git-url: https://github.com/piomin/kubernetes-quickstart.git
jkube.eclipse.org/git-branch: master
labels:
app: inner-dev-loop
provider: jkube
version: 1.0-SNAPSHOT
group: pl.piomin
app.kubernetes.io/part-of: pl.piomin
app.kubernetes.io/managed-by: jkube
app.kubernetes.io/name: inner-dev-loop
app.kubernetes.io/version: 1.0-SNAPSHOT
name: inner-dev-loop
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app: inner-dev-loop
provider: jkube
group: pl.piomin
app.kubernetes.io/name: inner-dev-loop
app.kubernetes.io/part-of: pl.piomin
app.kubernetes.io/managed-by: jkube
template:
metadata:
annotations:
jkube.eclipse.org/scm-url: https://github.com/spring-projects/spring-boot/inner-dev-loop
jkube.eclipse.org/scm-tag: HEAD
jkube.eclipse.org/git-commit: 92b2e11f7ddb134323133aee0daa778135500113
jkube.eclipse.org/git-url: https://github.com/piomin/kubernetes-quickstart.git
jkube.eclipse.org/git-branch: master
labels:
app: inner-dev-loop
provider: jkube
version: 1.0-SNAPSHOT
group: pl.piomin
app.kubernetes.io/part-of: pl.piomin
app.kubernetes.io/managed-by: jkube
app.kubernetes.io/name: inner-dev-loop
app.kubernetes.io/version: 1.0-SNAPSHOT
spec:
containers:
- env:
- name: DATABASE_PASS
valueFrom:
secretKeyRef:
key: password
name: person-db-postgresql
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: DATABASE_NAME
value: spring
- name: DATABASE_USER
value: spring
- name: HOSTNAME
valueFrom:
fieldRef:
fieldPath: metadata.name
image: piomin/inner-dev-loop:latest
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health/liveness
port: 8080
scheme: HTTP
initialDelaySeconds: 180
successThreshold: 1
name: spring-boot
ports:
- containerPort: 8080
name: http
protocol: TCP
- containerPort: 9779
name: prometheus
protocol: TCP
- containerPort: 8778
name: jolokia
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /actuator/health/readiness
port: 8080
scheme: HTTP
initialDelaySeconds: 10
successThreshold: 1
securityContext:
privileged: false
YAMLIn order to deploy the application to Kubernetes, we need to add the k8s:apply
to the previously executed command.
mvn clean package -DskipTests k8s:build k8s:resource k8s:apply
ShellSessionAfter that, JKube applies the generated YAML manifests to the cluster.
We can verify a list of running applications by displaying a list of pods:
$ kubectl get pod
NAME READY STATUS RESTARTS AGE
inner-dev-loop-5cbcf7dfc6-wfdr6 1/1 Running 0 17s
person-db-postgresql-0 1/1 Running 0 106m
ShellSessionIt is also possible to display the application logs by executing the following command:
mvn k8s:log
ShellSessionHere’s the output after running the mvn k8s:log
command:
We can also do other things, like to undeploy the app from the Kubernetes cluster.
Final Thoughts
Eclipse JKube simplifies Spring Boot deployment on the Kubernetes cluster. Except for the presented features, it also provides mechanisms for the inner development loop with the k8s:watch
and k8s:remote-dev
goals.
2 COMMENTS