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.