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-

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 -

Kubernetes Persistent Volume Secrets Full Stack deployment example

References