SpringBoot + Kubernetes StatefulSets: Perfect Combo for Reliable Banking Apps! + Concepts of configMap & Secrets

Ensuring stability, data persistence, and seamless scaling in critical applications.

SpringBoot + Kubernetes StatefulSets: Perfect Combo for Reliable Banking Apps! + Concepts of configMap & Secrets

We have configured a deployment for the Banking App with 3 Pods, ensuring data safety by associating the data with a PersistentVolumeClaim (PVC). The PVC is connected to a PersistentVolume (PV), which is then linked to the host to provide the necessary storage.

When a user signs up for our Banking App and clicks register, if the Pod crashes at that moment, the data might not be saved. However, thanks to the PersistentVolume (PV), we can recover the data. The challenge arises because Pods have random names and may contain login data for that specific time, making it difficult to identify the correct Pod since it is unnamed. This issue is known as State. In these situations, even if you attempt to save the data, it cannot manage the user's login due to the Pods having random, unnamed identifiers. Therefore, when developing any database-related project (MySQL, PostgreSQL, MongoDB), using a deployment is not recommended, as it can lead to these problems.

To address this, you should use a StatefulSets. It functions similarly to a deployment but creates named pods, ensuring better management of stateful applications.

In a StatefulSet, pods are created with specific names such as mysql-0, mysql-1, and mysql-2. Each pod maintains its own state, so if mysql-0 is deleted, it will be recreated with the same name, preserving its state. In contrast, a deployment would create pods with random names, which do not maintain state or sequence. This is the key difference between a deployment and a StatefulSet, as StatefulSets ensure better management of stateful applications by maintaining both state and order.

To prevent data loss, it is essential to create pods in a specific sequence, which can only be achieved using StatefulSets.

Steps to Implement StatefulSets and Secrets in Kubernetes:

  1. Create an EC2 instance on AWS or any cloud provider. Log in with SSH and then proceed with the next steps.

  2. If you've been following my Docker blogs, you might have noticed that we use passwords in the docker-compose.yml file. In Kubernetes, to address this security concern, we use Secrets. [Docker Compose blog link]

  3. Create secrets.yml:

kind: Secrets
apiVersion: v1
metadata:
  name: mysql-secret
  namespace: bankapp-namespace
type: Opaque   #Secret type for storing sensitive key-value data like passwords, encoded in base64.
spec:
  data:
    MYSQL_ROOT_PASSWORD: VGVzdEAxMjMK  # Base64 for "Test@123"
    SPRING_DATASOURCE_PASSWORD: VGVzdEAxMjM # Base64 for "Test@123"
  • To encode your password value, you can convert them into base64 format. Here's how you can do it:

  • But it can also decode easily right, so generally we don’t commit our secret files. But instead of keeping plain password at least encode it. And this encoding Kubernetes secrets internally decode and provide to container.

  • In Kubernetes, Opaque is the default Secret type used to store arbitrary key-value pairs of sensitive data, such as passwords or tokens, in base64-encoded form for secure handling and usage.

  1. Now, we will create configMap.yml. This file will include variables that contain data not requiring secure storage. It is used to store our regular variables.
apiVersion: v1
kind: ConfigMap
metadata:
  name: bankapp-config
  namespace: bankapp-namespace
data:
  MYSQL_DATABASE: BankDB
  SPRING_DATASOURCE_USERNAME: root

How do these secrets and configurations reach StatefulSets in a pod? This is achieved using environment variables, as demonstrated in the statefulSets.yml file below.

  1. To apply the changes to the secrets, follow these steps:
kubectl apply -f secrets.yml

#Check if it created ot not
kubectl get secrets -n bankapp-namespace

It displays two values in the Data section.

Now, we will use the describe command to view more details:

You will notice that it does not display the actual value but instead shows 8 bytes of data, as it is a secret.

  1. To apply the changes to the configMap, follow these steps:
kubectl apply -f configMap.yml

#Check if it created ot not
kubectl get configmaps -n bankapp-namespace

It displays a single value in the Data section.

Now, we will use the describe command to view more details:

You will notice that it display the actual value.

  1. Create mysqlStatefulSet.yml:
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: mysql
  namespace: bankapp-namespace
  labels:
    app: mysql
spec:
  serviceName: mysql-headless
  replicas: 1
  selector:
    matchLabels:
      app: mysql
  template:
    metadata:
      labels:
        app: mysql
    spec:
      containers:
      - name: mysql
        image: mysql:latest
        ports:
        - containerPort: 3306
        env:
        - name: MYSQL_ROOT_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: MYSQL_ROOT_PASSWORD
        - name: MYSQL_DATABASE
          valueFrom:
            configMapKeyRef:
              name: bankapp-config
              key: MYSQL_DATABASE
        volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql
        livenessProbe:
          exec:
            command:
            - mysqladmin
            - ping
            - -h
            - localhost
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          exec:
            command:
            - mysqladmin
            - ping
            - -h
            - localhost
          initialDelaySeconds: 10
          periodSeconds: 5
  volumeClaimTemplates:
  - metadata:
      name: mysql-data
      labels:
        app: mysql
    spec:
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 5Gi

To determine if our containers are ready, we use probes.

Probes are requests sent to a container to check if it is functioning properly.

  • What are livenessProbe and readinessProbe?

    • livenessProbe: This is a request that checks whether your health checks are passing and if your pod is active and healthy.

    • readinessProbe: This is a request that checks if your pod is ready to handle traffic or not.

  • What is VolumeMount?

volumeMounts:
        - name: mysql-data
          mountPath: /var/lib/mysql

Volume Mounts: Within the pod, MySQL is running, and we are attaching it to a PersistentVolume.

Inside the pod, MySQL is running, and its data is stored in /var/lib/mysql.

On the worker node, there is a path /tmp/bankapp-data. If you want to store the container's data on the worker node, so that the data persists even if the pod is deleted, you need to store it on the node.

To achieve this, we will attach a PersistentVolume (PV) to the path /tmp/bankapp-data and allocate 5Gi. To connect these paths, we need a PersistentVolumeClaim (PVC) to claim 5Gi.

We will instruct the pod to use the PV named mysql-data, which will bind both paths together.

  1. Now we will EXPOSE the MySQL service using the following method:
  • Since you are creating this service internally and do not want to expose it publicly, set clusterIP to none.
apiVersion: v1
kind: Service
metadata:
  name: mysql-headless
  namespace: bankapp-namespace
  labels:
    app: mysql
spec:
  clusterIP: None
  selector:
    app: mysql
  ports:
  - protocol: TCP
    port: 3306
    targetPort: 3306
  1. Now we will apply all these changes:
kubectl apply -f mysqlStatefulSet.yml

kubectl apply -f mysqlService.yml
  1. Now list down for all the namespaces which we have created.
kubectl get all -n bankapp-namespace

Here, you can observe that the pod name is not random; it follows a proper naming convention.

To create a StatefulSet, it is important to understand three key concepts: PersistentVolume (PV), PersistentVolumeClaim (PVC), and secrets.

Creating BankApp:

Now, we will create the BankApp application and store the data to verify if it is being persisted correctly.

On the worker node, we create a StatefulSet for MySQL, and attach PersistentVolumes (PV), PersistentVolumeClaims (PVC), secrets, configMaps, and services to it. Similarly, we will create the BankApp, which is a frontend application that connects to the MySQL database. For this, we will create a deployment with an attached service.

  1. Now we will create deployment.yml for Bankapp
apiVersion: apps/v1
kind: Deployment
metadata:
  name: bankapp-deployment
  namespace: bankapp-namespace
  labels:
    app: bankapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: bankapp
  template:
    metadata:
      labels:
        app: bankapp
    spec:
      initContainers:
      - name: wait-for-mysql
        image: busybox:1.28
        command: ['sh', '-c', 'until nc -z mysql-0.mysql-headless 3306; do echo waiting for mysql; sleep 5; done;']
      containers:
      - name: bankapp
        image: chetan0103/springboot-bankapp:latest
        ports:
        - containerPort: 8080
        env:
        - name: SPRING_DATASOURCE_URL
          valueFrom:
            configMapKeyRef:
              name: bankapp-config
              key: SPRING_DATASOURCE_URL
        - name: SPRING_DATASOURCE_USERNAME
          valueFrom:
            configMapKeyRef:
              name: bankapp-config
              key: SPRING_DATASOURCE_USERNAME
        - name: SPRING_DATASOURCE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mysql-secret
              key: SPRING_DATASOURCE_PASSWORD
        livenessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /actuator/health
            port: 8080
          initialDelaySeconds: 10
          periodSeconds: 5
  1. Create service.yml
apiVersion: v1
kind: Service
metadata:
  name: bankapp-service
  namespace: bankapp-namespace
  labels:
    app: bankapp
spec:
  type: NodePort
  selector:
  ports:
    - port: 8080
      targetPort: 8080
      protocol: tcp
      nodePort: 30080
  1. Apply the changes:
kubectl apply -f deployment.yml

kubectl apply -f service.yml

kubectl get all -n bankapp-namespace

  1. You can confidently state that your application is deployed on Kubernetes.
kubectl port-forward service/bankapp-service -n bankapp-namespace 8080:8080 --address=0.0.0.0
  1. Now, navigate to AWS and access your EC2 instance. Proceed to the security group settings and add an entry for port 8080, allowing access from Anywhere IPv4. Next, go to the instance details and copy the public IP address. Paste this IP address into your browser using the format: http://public_ip:8080. You should then be able to view the results as shown below.

Validations:

  1. Now, we will register a new user and then log in with it. This step is important because we want to verify whether our data persists or disappears when we delete our StatefulSets and deployment.

  1. Add some transactions:

  1. Now, proceed to delete and then reapply the StatefulSets and Deployment.
kubectl delete -f mysqlStatefulSet.yml

kubectl delete -f deployment.yml

kubectl apply -f mysqlStatefulSet.yml

kubectl apply -f deployment.yml
  1. Now, let's proceed to run this application:
kubectl port-forward service/bankapp-service -n bankapp-namespace 8080:8080 --address=0.0.0.0

The transactions remain intact even after deleting the StatefulSets and deployment, indicating that our Persistent Volume is functioning correctly.

This is the process for creating a multi-tier application in Kubernetes.

Note: It is not required to memorize the syntax or code; Kubernetes.io is a valuable resource you can use. Even during the CKA exam, you are permitted to refer to it.

Interview Question:

  • What are initContainers?

—> An initContainer is an initializer container for a pod. Before the main/app container starts, the initContainer ensures that its tasks are completed. It functions similarly to a "dependsOn" mechanism.

Happy Learning :)

Chetan Mohod ✨

For more DevOps updates, you can follow me on LinkedIn.