Deploy Kotlin Spring Boot App with MySQL on Kubernetes

In this article, we will learn how to deploy the Kotlin Spring Boot application with MySQL on Kubernetes cluster.
A featured image for category: Spring

1. Introduction

Last week, we’ve learned how to deploy the Spring Boot application to Google Kubernetes Engine and connect it to the CloudSQL instance of MySQL database.

This time, I want to show you a more universal approach- deploying the Kotlin Spring Boot App with MySQL directly on Kubernetes. This approach requires a little more setup, but thanks to that, we gain more independence- we can easily migrate our environment between Kubernetes managed services providers (or to self-hosted k8s).

2. Prerequisites

To follow the tutorial you need the following prerequisites:

  • kubectl (Kubernetes CLI to manage a cluster)
  • minikube (local Kubernetes, making it easy to learn and develop for Kubernetes)
  • docker (containerization tool)

3. Create Spring Boot Application

In this tutorial, I’d like to begin with developing our Spring Boot app first (but it’s also OK to start with the Kubernetes part).

3.1. Imports

As the first step, let’s add the following dependencies:

    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    runtimeOnly("mysql:mysql-connector-java")

Please remember, that you can see the full build.gradle configuration here.

3.2. Configure Application Properties

After we’ve successfully added the import, let’s head to the application.yaml file and configure it as follows:

spring:
  datasource:
    url: jdbc:mysql://mysql-svc:3306/${DB_NAME}?useSSL=false
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
  jpa:
    hibernate:
      ddl-auto: update
    databasePlatform: "org.hibernate.dialect.MySQL5InnoDBDialect"

As you might have noticed, we are setting the MySQL connection details here. Please remember, that the host (mysql-svc) has to match the MySQL Kubernetes service name (we will set it up later). The ddl-auto property set to update will automatically export the schema DDL to the database when the SessionFactory is created (to put it simply, it will create the database schema based on our Spring Boot entities).

3.2. Create Model

As the next step, let’s add two classes to our project. User class, which will be the user entity we will persist in our MySQL database:

@Entity
class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long = 0,

    @Column
    val name: String
)

As you can see, it is a simple class containing two fields- autogenerated id and a name.

Nextly, let’s add UserRequest, which will be bound to the bodies of the web requests later:

class UserRequest(
    val name: String
)

3.4. Create Repository

After that, we can create the UserRepository interface, which will be used to perform the operations on the database:

interface UserRepository : CrudRepository<User, Long>

3.4. Create Service

As the next step, let’s implement the UserService class as follows:

@Service
class UserService(
    private val userRepository: UserRepository
) {

    fun saveUser(request: UserRequest): User =
        userRepository.save(
            User(
                name = request.name
            )
        )

    fun findAllUsers(): MutableIterable<User> =
        userRepository.findAll()
}

These two functions will be responsible for creating and getting the list of all users.

3.5. Implement Controllers

Finally, we can set up our controllers responsible for handling the incoming requests. Let’s start by adding the ApiStatusController:

@RestController
@RequestMapping("/api/status")
class ApiStatusController {
    @GetMapping
    fun getStatus(): ResponseEntity = ResponseEntity.ok("OK")
}

The GET /api/status endpoint will be used later to configure our K8s Pod’s readiness and liveness probes.

As the last step, let’s create the UserController class:

@RestController
@RequestMapping("/api/user")
class UserController(
    private val userService: UserService
) {
    @PostMapping
    fun createUser(@RequestBody request: UserRequest): ResponseEntity {
        val user = userService.saveUser(request)

        return ResponseEntity.ok(user)
    }

    @GetMapping
    fun findAllUsers(): ResponseEntity<MutableIterable> {
        val users = userService.findAllUsers()

        return ResponseEntity.ok(users)
    }
}

These two functions will be responsible for responding to the POST and GET requests to the /api/user endpoint. As you can see, we are using here the UserRequest class, we’ve implemented earlier as the createUser method parameter.

3.6. Build the JAR

Let’s build the jar with the Gradle wrapper:

./gradlew clean build -x test

Please notice “-x test” flag here. Testing goes beyond the main topic of this tutorial, so I’ve decided to go with this workaround for now.

3.7. Create Dockerfile

Nextly, let’s create the Dockerfile:

FROM openjdk
WORKDIR /work
COPY ./build/libs/k8s-mysql-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/work/app.jar"]

4. Deploy to Kubernetes

In this part, we are going to prepare the Kubernetes environment for our Spring Boot application. To keep this article as pragmatic as possible, we won’t spend too much time on the details here. If you would like to get a better understanding of these topics, please, let me know about it and I will prepare more materials about it.

4.1. Create Secrets

As the first step, let’s create the secrets.yaml file and put the configuration for our secrets here:

apiVersion: v1
kind: Secret
data:
  root-password: <BASE64-ENCODED-PASSWORD>
  database-name: <BASE64-ENCODED-DB-NAME>
  user-username: <BASE64-ENCODED-DB-USERNAME>
  user-password: <BASE64-ENCODED-DB-PASSWORD>
metadata:
  name: mysql-secret

In simple words, secrets let us store and manage sensitive information in a secure manner.
Please also notice that all of the values should be encoded as base 64. You can do that for instance on this website.

4.2. Create Persistent Volume

As the next step, let’s create the deployment-mysql.yaml file:

apiVersion: v1
kind: PersistentVolume
metadata:
  name: mysql-pv
  labels:
    type: local
spec:
  storageClassName: standard
  capacity:
    storage: 250Mi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/mnt/data"
  persistentVolumeReclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: mysql-pvc
  labels:
    app: spring-boot-mysql
spec:
  storageClassName: standard
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 250Mi

A PersistentVolume (PV) is a piece of the storage in the cluster, while the PersistentVolumeClaim (PVC) is the request for storage.

4.3. Create MySQL Deployment

Deployments allow us to provide updates for our Pods and ReplicaSets in a declarative manner. To configure the MySQL database let’s add the following config to our deployment-mysql.yaml file:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mysql-deployment
  labels:
    app: spring-boot-mysql
spec:
  selector:
    matchLabels:
      app: spring-boot-mysql
      tier: mysql
  strategy:
    type: Recreate
  template:
    metadata:
      labels:
        app: spring-boot-mysql
        tier: mysql
    spec:
      containers:
        - image: mysql:5.7
          name: mysql
          env:
            - name: MYSQL_ROOT_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: root-password
            - name: MYSQL_DATABASE
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: database-name
            - name: MYSQL_USER
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: user-username
            - name: MYSQL_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: user-password
          ports:
            - containerPort: 3306
              name: mysql
          volumeMounts:
            - name: mysql-persistent-storage
              mountPath: /var/lib/mysql
      volumes:
        - name: mysql-persistent-storage
          persistentVolumeClaim:
            claimName: mysql-pvc

As you can see above, we are passing our 4 secrets to the MySQL container as the environment variables- these values will be used for our connection.

4.4. Create MySQL Service

Finally, we need to expose the database for our Spring Boot application. Let’s do that by creating the service-mysql.yaml file and putting the following confirugation:

apiVersion: v1
kind: Service
metadata:
  name: mysql-svc
  labels:
    app: spring-boot-mysql
spec:
  ports:
    - port: 3306
  selector:
    app: spring-boot-mysql
    tier: mysql
  clusterIP: None

Please notice, that the name property of the metadata has to match the value from the application.yaml file of the Spring Boot project.

4.5. Create Spring Boot Deployment

After the MySQL part is correctly set up, we can configure the resources for our application. Let’s start by creating the deployment-spring.yaml file:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: spring-boot-deployment
  labels:
    app: spring-boot-mysql
spec:
  replicas: 2
  selector:
    matchLabels:
      app: spring-boot-mysql
  template:
    metadata:
      labels:
        app: spring-boot-mysql
    spec:
      containers:
        - image: spring-boot:0.0.1
          name: spring-boot-container
          imagePullPolicy: Never 
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              port: 8080
              path: /api/status
            initialDelaySeconds: 10
            failureThreshold: 5
          livenessProbe:
            httpGet:
              port: 8080
              path: /api/status
            initialDelaySeconds: 10
            failureThreshold: 5
          env:
            - name: DB_NAME
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: database-name
            - name: DB_USERNAME
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: user-username
            - name: DB_PASSWORD
              valueFrom:
                secretKeyRef:
                  name: mysql-secret
                  key: user-password

Please notice one important thing here- in the next steps, I will show you how to use the local Spring Boot Docker image of our application. That’s why we’ve set the imagePullPolicy property to Never.

4.6. Create Spring Boot Service

The last thing, we need to configure for our backend is a Service. Let’s create then the service-spring.yaml file:

apiVersion: v1
kind: Service
metadata:
  name: spring-boot-svc
spec:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: TCP
      name: http
  selector:
    app: spring-boot-mysql
  type: LoadBalancer

4.7. Apply and Verify Resources

Finally, we can deploy our configurations to the Kubernetes cluster. As the first step, let’s start a local Kubernetes cluster with minikube:

minikube start --vm-driver=virtualbox

As I’ve mentioned in step 4.5, in this tutorial, we will use the local docker image without pushing to any remote image repository.

To do that, let’s reuse the Docker daemon from Minikube:

eval $(minikube docker-env)

Please keep in mind, that this is a Unix command. If you are using Windows OS, please check out this StackOverflow topic.

Nextly, let’s build our docker image:

docker build -t spring-boot:0.0.1 .

Please remember, that the tag has to match the value from our Spring Boot deployment.

As the last step, let’s apply our configurations:

kubectl apply -f k8s/secrets.yaml
kubectl apply -f k8s/deployment-mysql.yaml 
kubectl apply -f k8s/service-mysql.yaml
kubectl apply -f k8s/deployment-spring.yaml
kubectl apply -f k8s/service-spring.yaml

To verify if everything is ready, we can use the following commands:

kubectl get svc
kubectl get deployments
kubectl get pods
kubectl get secrets

5. Verify the Application

To verify, if the application is working correctly, we will have to get the IP address of the running cluster and the port of the Spring Boot Service.

We can obtain the IP address in two ways: either by minikube or using kubectl command:

minikube ip
//result
192.168.39.87


kubectl cluster-info
//result
Kubernetes master is running at https://192.168.39.87:8443

To get the port of the application, let’s invoke the following command:

kubectl get svc spring-boot-svc 

//Example response
NAME              TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
spring-boot-svc   LoadBalancer   10.107.222.166        8080:30264/TCP   33s

As you can see above, the desired port of the application will be 30264. Please keep in mind, that this value will be different each time we will create the Service.

Finally, let’s create the user using the curl command:

curl -X POST -H "Content-Type: application/json" \
  --data '{"name":"Piotr"}' \
  192.168.39.87:30264/api/user

//response
{"id":1,"name":"Piotr"}

As the last step, let’s get the list of all users:

curl 192.168.39.87:30264/api/user

//Response:
[{"id":1,"name":"Piotr"}]

6. Summary

And that would be all for this article. In this step by step guide, we’ve learned how to deploy the Spring Boot and MySQL application on Kubernetes.

I am really aware that this topic is quite complex and requires an understanding of several concepts. The main goal for this tutorial was to walk you through the whole process without diving too much into the details. If you find anything in this article confusing or have any additional questions/suggestions, I will be more than happy if you would let me know about it (you can do that by our fan page, group, or a contact form).

To see the whole project, please visit this GitHub repository.

Share this:

Related content

Newsletter
Image presents 3 ebooks with Java, Spring and Kotlin interview questions.

Never miss any important updates from the Kotlin world and get 3 ebooks!

You may opt out any time. Terms of Use and Privacy Policy