Deploying a full-stack Spring boot, Mysql, and React app on Kubernetes with Persistent Volumes and Secrets
Introduction
In this article, you’ll learn how to deploy a Stateful app built with Spring Boot, Mysql, and React on Kubernetes. We’ll use a local minikube cluster to deploy the application. Please make sure that you have kubectl and minikube installed in your system.
If you want to Gain In-depth Knowledge on Full Stack Spring Boot, please go through this link Spring Boot Online Training
If you’re new to Kubernetes, I recommend reading the following hands-on guides before reading this one-
- Deploying a containerized Go app on Kubernetes
- Deploying a multi-container Go app with Redis on Kubernetes
The sample application that we’ll deploy on Kubernetes in this article can be downloaded from Github:
- Spring Boot, Mysql, React, Ant design Polling App
It is a full-stack Polling app where users can login, create a Poll, and vote for a Poll.
To deploy this application, we’ll use few additional concepts in Kubernetes called PersistentVolumes and Secrets. Let’s first get a basic understanding of these concepts before moving to the hands-on deployment guide. Let's See How the Kubernetes helps to Spring Boot Training
Kubernetes Persistent Volume
We’ll use Kubernetes Persistent Volumes to deploy Mysql. A PersistentVolume (PV
) is a piece of storage in the cluster. It is a resource in the cluster just like a node. The Persistent volume’s lifecycle is independent from Pod lifecycles. It preserves data through restarting, rescheduling, and even deleting Pods.
PersistentVolumes are consumed by something called a PersistentVolumeClaim (PVC
). A PVC is a request for storage by a user. It is similar to a Pod. Pods consume node resources and PVCs consume PV resources. Pods can request specific levels of resources (CPU and Memory). PVCs can request specific size and access modes (e.g. read-write or read-only).
Kubernetes Secrets
We’ll make use of Kubernetes secrets to store the Database credentials. A Secret is an object in Kubernetes that lets you store and manage sensitive information, such as passwords, tokens, ssh keys etc. The secrets are stored in Kubernetes backing store, etcd. You can enable encryption to store secrets in encrypted form in etcd.
Deploying Mysql on Kubernetes using PersistentVolume and Secrets
Following is the Kubernetes manifest for MySQL deployment. I’ve added comments alongside each configuration to make sure that its usage is clear to you.
apiVersion: v1 kind: PersistentVolume # Create a PersistentVolume metadata: name: mysql-pv labels: type: local spec: storageClassName: standard # Storage class. A PV Claim requesting the same storageClass can be bound to this volume. capacity: storage: 250Mi accessModes: - ReadWriteOnce hostPath: # hostPath PersistentVolume is used for development and testing. It uses a file/directory on the Node to emulate network-attached storage path: "/mnt/data" persistentVolumeReclaimPolicy: Retain # Retain the PersistentVolume even after PersistentVolumeClaim is deleted. The volume is considered “released”. But it is not yet available for another claim because the previous claimant’s data remains on the volume. --- apiVersion: v1 kind: PersistentVolumeClaim # Create a PersistentVolumeClaim to request a PersistentVolume storage metadata: # Claim name and labels name: mysql-pv-claim labels: app: polling-app spec: # Access mode and resource limits storageClassName: standard # Request a certain storage class accessModes: - ReadWriteOnce # ReadWriteOnce means the volume can be mounted as read-write by a single Node resources: requests: storage: 250Mi --- apiVersion: v1 # API version kind: Service # Type of kubernetes resource metadata: name: polling-app-mysql # Name of the resource labels: # Labels that will be applied to the resource app: polling-app spec: ports: - port: 3306 selector: # Selects any Pod with labels `app=polling-app,tier=mysql` app: polling-app tier: mysql clusterIP: None --- apiVersion: apps/v1 kind: Deployment # Type of the kubernetes resource metadata: name: polling-app-mysql # Name of the deployment labels: # Labels applied to this deployment app: polling-app spec: selector: matchLabels: # This deployment applies to the Pods matching the specified labels app: polling-app tier: mysql strategy: type: Recreate template: # Template for the Pods in this deployment metadata: labels: # Labels to be applied to the Pods in this deployment app: polling-app tier: mysql spec: # The spec for the containers that will be run inside the Pods in this deployment containers: - image: mysql:5.6 # The container image name: mysql env: # Environment variables passed to the container - name: MYSQL_ROOT_PASSWORD valueFrom: # Read environment variables from kubernetes secrets secretKeyRef: name: mysql-root-pass key: password - name: MYSQL_DATABASE valueFrom: secretKeyRef: name: mysql-db-url key: database - name: MYSQL_USER valueFrom: secretKeyRef: name: mysql-user-pass key: username - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: mysql-user-pass key: password ports: - containerPort: 3306 # The port that the container exposes name: mysql volumeMounts: - name: mysql-persistent-storage # This name should match the name specified in `volumes.name` mountPath: /var/lib/mysql volumes: # A PersistentVolume is mounted as a volume to the Pod - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pv-claim
We’re creating four resources in the above manifest file. A PersistentVolume, a PersistentVolumeClaim for requesting access to the PersistentVolume resource, a service for having a static endpoint for the MySQL database, and a deployment for running and managing the MySQL pod.
The MySQL container reads database credentials from environment variables. The environment variables access these credentials from Kubernetes secrets.
Let’s start a minikube cluster, create kubernetes secrets to store database credentials, and deploy the Mysql instance:
Starting a Minikube cluster
$ minikube start
Creating the secrets
You can create secrets manually from a literal or file using the kubectl create secret
command, or you can create them from a generator using Kustomize.
In this article, we’re gonna create the secrets manually:
$ kubectl create secret generic mysql-root-pass --from-literal=password=R00t secret/mysql-root-pass created $ kubectl create secret generic mysql-user-pass --from-literal=username=callicoder --from-literal=password=c@ll1c0d3r secret/mysql-user-pass created $ kubectl create secret generic mysql-db-url --from-literal=database=polls --from-literal=url='jdbc:mysql://polling-app-mysql:3306/polls?useSSL=false&serverTimezone=UTC&useLegacyDatetimeCode=false' secret/mysql-db-url created
You can get the secrets like this -
$ kubectl get secrets NAME TYPE DATA AGE default-token-tkrx5 kubernetes.io/service-account-token 3 3d23h mysql-db-url Opaque 2 2m32s mysql-root-pass Opaque 1 3m19s mysql-user-pass Opaque 2 3m6s
You can also find more details about a secret like so -
$ kubectl describe secrets mysql-user-pass Name: mysql-user-pass Namespace: default Labels: <none> Annotations: <none> Type: Opaque Data ==== username: 10 bytes password: 10 bytes
Deploying MySQL
Let’s now deploy MySQL by applying the yaml configuration -
$ kubectl apply -f deployments/mysql-deployment.yaml service/polling-app-mysql created persistentvolumeclaim/mysql-pv-claim created deployment.apps/polling-app-mysql created
That’s it! You can check all the resources created in the cluster using the following commands -
$ kubectl get persistentvolumes NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE mysql-pv 250Mi RWO Retain Bound default/mysql-pv-claim standard 30s
$ kubectl get persistentvolumeclaims NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE mysql-pv-claim Bound mysql-pv 250Mi RWO standard 50s
$ kubectl get services NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 5m36s polling-app-mysql ClusterIP None <none> 3306/TCP 2m57s
$ kubectl get deployments NAME READY UP-TO-DATE AVAILABLE AGE polling-app-mysql 1/1 1 1 3m14s
Logging into the MySQL pod
You can get the MySQL pod and use kubectl exec
command to login to the Pod.
$ kubectl get pods NAME READY STATUS RESTARTS AGE polling-app-mysql-6b94bc9d9f-td6l4 1/1 Running 0 4m23s $ kubectl exec -it polling-app-mysql-6b94bc9d9f-td6l4 -- /bin/bash root@polling-app-mysql-6b94bc9d9f-td6l4:/#
Deploying the Spring Boot app on Kubernetes
All right! Now that we have the MySQL instance deployed, Let’s proceed with the deployment of the Spring Boot app at Spring Boot Online Course
Following is the deployment manifest for the Spring Boot app -
--- apiVersion: apps/v1 # API version kind: Deployment # Type of kubernetes resource metadata: name: polling-app-server # Name of the kubernetes resource labels: # Labels that will be applied to this resource app: polling-app-server spec: replicas: 1 # No. of replicas/pods to run in this deployment selector: matchLabels: # The deployment applies to any pods mayching the specified labels app: polling-app-server template: # Template for creating the pods in this deployment metadata: labels: # Labels that will be applied to each Pod in this deployment app: polling-app-server spec: # Spec for the containers that will be run in the Pods containers: - name: polling-app-server image: callicoder/polling-app-server:1.0.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 # The port that the container exposes resources: limits: cpu: 0.2 memory: "200Mi" env: # Environment variables supplied to the Pod - name: SPRING_DATASOURCE_USERNAME # Name of the environment variable valueFrom: # Get the value of environment variable from kubernetes secrets secretKeyRef: name: mysql-user-pass key: username - name: SPRING_DATASOURCE_PASSWORD valueFrom: secretKeyRef: name: mysql-user-pass key: password - name: SPRING_DATASOURCE_URL valueFrom: secretKeyRef: name: mysql-db-url key: url --- apiVersion: v1 # API version kind: Service # Type of the kubernetes resource metadata: name: polling-app-server # Name of the kubernetes resource labels: # Labels that will be applied to this resource app: polling-app-server spec: type: NodePort # The service will be exposed by opening a Port on each node and proxying it. selector: app: polling-app-server # The service exposes Pods with label `app=polling-app-server` ports: # Forward incoming connections on port 8080 to the target port 8080 - name: http port: 8080 targetPort: 8080
The above deployment uses the Secrets stored in mysql-user-pass
and mysql-db-url
that we created in the previous section.
Let’s apply the manifest file to create the resources -
$ kubectl apply -f deployments/polling-app-server.yaml deployment.apps/polling-app-server created service/polling-app-server created
You can check the created Pods like this -
$ kubectl get pods NAME READY STATUS RESTARTS AGE polling-app-mysql-6b94bc9d9f-td6l4 1/1 Running 0 21m polling-app-server-744b47f866-s2bpf 1/1 Running 0 31s
Now, type the following command to get the polling-app-server service URL -
$ minikube service polling-app-server --url http://192.168.99.100:31550
You can now use the above endpoint to interact with the service -
$ curl http://192.168.99.100:31550 {"timestamp":"2019-07-30T17:55:11.366+0000","status":404,"error":"Not Found","message":"No message available","path":"/"}
Deploying the React app on Kubernetes
Finally, Let’s deploy the frontend app using Kubernetes. Here is the deployment manifest -
apiVersion: apps/v1 # API version kind: Deployment # Type of kubernetes resource metadata: name: polling-app-client # Name of the kubernetes resource spec: replicas: 1 # No of replicas/pods to run selector: matchLabels: # This deployment applies to Pods matching the specified labels app: polling-app-client template: # Template for creating the Pods in this deployment metadata: labels: # Labels that will be applied to all the Pods in this deployment app: polling-app-client spec: # Spec for the containers that will run inside the Pods containers: - name: polling-app-client image: callicoder/polling-app-client:1.0.0 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 80 # Should match the Port that the container listens on resources: limits: cpu: 0.2 memory: "10Mi" --- apiVersion: v1 # API version kind: Service # Type of kubernetes resource metadata: name: polling-app-client # Name of the kubernetes resource spec: type: NodePort # Exposes the service by opening a port on each node selector: app: polling-app-client # Any Pod matching the label `app=polling-app-client` will be picked up by this service ports: # Forward incoming connections on port 80 to the target port 80 in the Pod - name: http port: 80 targetPort: 80
Let’s apply the above manifest file to deploy the frontend app -
$ kubectl apply -f deployments/polling-app-client.yaml deployment.apps/polling-app-client created service/polling-app-client created
Let’s check all the Pods in the cluster -
$ kubectl get pods NAME READY STATUS RESTARTS AGE polling-app-client-6b6d979b-7pgxq 1/1 Running 0 26m polling-app-mysql-6b94bc9d9f-td6l4 1/1 Running 0 21m polling-app-server-744b47f866-s2bpf 1/1 Running 0 31s
Type the following command to open the frontend service in the default browser -
$ minikube service polling-app-client
You’ll notice that the backend api calls from the frontend app is failing because the frontend app tries to access the backend APIs at localhost:8080
. Ideally, in a real-world, you’ll have a public domain for your backend server. But since our entire setup is locally installed, we can use kubectl port-forward
command to map the localhost:8080
endpoint to the backend service -
$ kubectl port-forward service/polling-app-server 8080:8080
That’s it! Now, you’ll be able to use the frontend app. Here is how the app looks like -