Kubernetes

Spring Boot Multimodule App to Kubernetes

Previously on the series (I will use same setup)

Create docker image

mvn -am -pl web spring-boot:build-image

Where web is subproject name. According to 12factor we have to use enviromnemt variables for connections, and we will propbably have at least jdbc connection to database, so it becomes:

DATASOURCE_URL="jdbc:mysql://localhost:3306/dbname" \
DATASOURCE_USERNAME="username" \
DATASOURCE_PASSWORD="password" \
mvn -am -pl web spring-boot:build-image

(If it’s not multimodule, just skip -am -pl web)

When build is finished, we should get in the output:

Successfully built image 'docker.io/library/projectName:1.0-SNAPSHOT'

Whis image name can be used to run docker container

Run in docker

docker run --network="host" \
    -e DATASOURCE_URL="jdbc:mysql://localhost:3306/dbname" \
    -e DATASOURCE_USERNAME="username" \
    -e DATASOURCE_PASSWORD="password" \
    -p 8080:8080 \
    projectName:1.0-SNAPSHOT

Now we have the image, but kubernetes can’t access it yet. Image has to be pushed to some registry or loaded into minikube directly like this:

Load image to minikube

minikube image load projectName:1.0-SNAPSHOT

Generate .yaml for kubernetes deployment

kubectl create deployment \
    --image=projectName:1.0-SNAPSHOT \
    --dry-run=client \
    -o=yaml projectName > projectName.yaml
echo --- >> projectName.yaml
kubectl create service clusterip projectName \
    --tcp=8080:8080 \
    --dry-run=client \
    -o=yaml >> projectName.yaml

Adjust spec/template/spec/containers

Disable image pull

As for we use local image we do not need kubernetes to pull it from Docker public registry.

imagePullPolicy: Never

Connection to external database on localhost

env:
    - name: DATASOURCE_URL
        value: "jdbc:mysql://host.minikube.internal:3306/projectName"
    - name: DATASOURCE_USERNAME
        value: "projectName"
    - name: DATASOURCE_PASSWORD
        value: "projectName"

Notice host.minikube.internal this is host name for your host machine, localhost will point to the pod itself (to guest vm).

Result should look like this

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: projectName
  name: projectName
spec:
  replicas: 1
  selector:
    matchLabels:
      app: projectName
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: projectName
    spec:
      containers:
      - image: projectName:1.0-SNAPSHOT
        imagePullPolicy: Never
        name: projectName
        env:
        - name: DATASOURCE_URL
          value: "jdbc:mysql://host.minikube.internal:3306/projectName"
        - name: DATASOURCE_USERNAME
          value: "projectName"
        - name: DATASOURCE_PASSWORD
          value: "projectName"
        resources: { }
status: {}
---
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: projectName
  name: projectName
spec:
  ports:
  - name: 8080-8080
    port: 8080
    protocol: TCP
    targetPort: 8080
  selector:
    app: projectName
  type: ClusterIP
status:
  loadBalancer: {}

Apply projectName.yaml

kubectl apply -f projectName.yaml

After that kubectl get pods should return list of pods with state Running next to projectName-XXXXXXX-XXX

View logs

kubectl logs POD_NAME

Will shouw you console output, like

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.4)

...
2022-11-20 06:00:24.718  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-20 06:00:24.730  INFO 1 --- [           main] org.Application       : Started Application in 4.718 seconds (JVM running for 4.967)

Or some errors if any.

Now pod is running, but it lives iside the cluster, so we need to get access from the outside.

Access service through proxy

kubectl port-forward svc/projectName 8080:8080

Now you should be able to connect to your spring-boot application using http://localhost:8080

Maven multimodule project with Spring Boot

I will try to be close to real task here. Lets say we have RestAPI web application, command line application for manupulating with secure data (installation, initial user creation, etc). Both will work with same data, so we need 3rd project which will be linked as dependency from cli and web.

Folder structure

rootProject
 -> datalib
 -> cli
 -> web

Project types

rootProject/pom.xml should refer to type pom. Type pom means that rootProject is just a place for references from the other projects. There is no target, no .jar files for the rooProject, it’s used by subprojects (aka modules) to find each other, plus if you have same dependensy in different subprojects you can only mention them once in rootProject/pom.xml.

All subprojects / modules will have type jar in our case. So build will produce .jar files.

Linking everything together

Package name

Package name aka namespace has to be the same for rootProject and submodules.

GroupID

project/groupId also has to be the same for rootProject and submodules, like this:

<project ..>
  ...
  <groupId>org.rootProject</groupId>
  ...
</project>

file: rootProject/pom.xml

<project ..>
  ...
  <!-- modules -->
  <modules>
    <module>web</module>
    <module>cli</module>
    <module>datalib</module>
  </modules>

files: rootProject/cli/pom.xml, rootProject/web/pom.xml, rootProject/datalib/pom.xml

<project>
    ...
    <parent>
        <artifactId>rootProject</artifactId>
        <groupId>org.rootProject</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    ...
</project>

But wait! Spring has to be parent project too!

Obviously, we can’t have 2 parants for the module. So we have to move spring reference to rootProject/pom.xml.

<project>
    <groupId>org.rootProject</groupId>
    <artifactId>rootProject</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <name>rootProject</name>
    
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    ...
</project>

Hierarhy is like this:

  • Spring Boot
    • rootProject
      • web
      • cli
      • datalib

Using one module from another

Right now, our modules know about rootProject but they do not know about each other. Datalib will be used in cli and web, so we need to add dependency:

files: rootProject/cli/pom.xml, rootProject/web/pom.xml

<project>
    ...
    <dependencies>
		<dependency>
			<groupId>org.rootProject</groupId>
			<artifactId>datalib</artifactId>
			<version>${project.version}</version>
			<scope>provided</scope>
			<type>jar</type>
		</dependency>
		...
    </dependencies>
    ...
</project>

Build commands

That’s a bit counterintuitive. You need 2 commands:

From rootProject/

mvn clean install

will build all projects in correct order and place relusting .jar files in local storage, where java will be able to find them. To run maven commands on subproject, you need to use -pl and -am, like this:

mvn -am -pl web spring-boot:run

or

mvn -am -pl cli test

-am means “take dependencies into consideration” without that you might get symbol not found error.

-pl just short for –project and thats just set context to module.

Spring is this loose coupled, not that loose coupled

You king of can use spring only for web module, but you will need to create wrapper components for datalib classes. And I don’t think it’s possible to use Spring based datalib in cli if cli is not Spring based. But I might be wrong.