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’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:
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.
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.
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 -