Kubernetes Deployment: Springboot application with MySQL

Kubernetes is an open source container orchestration tool design by the Google. Kubernetes is managing the cluster of containerised applications. This blog provides an overview that how to deploy a micro service that interact with the database on kubernetes deployment.

NOTE: Here we are not going into details of Kubernetes (possibly will cover in some other blog in future), we are focusing on the deployment.

Approach

Following diagram is depicting the deployment design.

Order

We can refer the following flow for the order application.

Application

Order Service application is a Springboot application, which act as a micro service. This application have the Rest Controller, Repository and Business Layer service etc.

API

OrderApi Controller is exposing the Rest API. These API are used to add, update, get and delete the orders.

package com.itlogiclab.order.api;

import java.util.List;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.itlogiclab.order.modal.OrderModal;
import com.itlogiclab.order.service.OrderService;

@RestController
@RequestMapping("/order/api/")
public class OrderApi {

	private static Logger logger = LogManager.getLogger(OrderApi.class);

	@Autowired
	private OrderService orderService;

	@PutMapping(value="/add", produces=MediaType.APPLICATION_JSON_VALUE, consumes=MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<OrderModal> add(@RequestBody OrderModal modal){
		logger.error("Creating new Order: ");
		try {
			OrderModal persisted = orderService.add(modal);
			logger.error("Order created successfully with id: "+persisted.getOrderId());
			return ResponseEntity.ok(persisted);	
		} catch (RuntimeException e) {
			logger.error("Error occured while creating new order: "+e.getMessage());
			return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
		}
	}

	@PostMapping(value="/update", produces=MediaType.APPLICATION_JSON_VALUE, consumes=MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<OrderModal> updateOrder(@RequestBody OrderModal order){
		logger.error("Update Order : "+order.getOrderId());
		try {
			OrderModal persisted = orderService.update(order);
			return ResponseEntity.ok(persisted);	
		} catch (RuntimeException e) {
			logger.error("Error occured while update the order: order id {} : ", order.getOrderId() +e.getMessage());
			return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
		}
	}

	@GetMapping(value="/get", produces=MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<List<OrderModal>> get(){
		logger.error("Fetching all orders: ");
		try {
			List<OrderModal> persisted =   orderService.get();
			return ResponseEntity.ok(persisted);	
		} catch (RuntimeException e) {
			logger.error("Error occured while fetching all orders from DB: "+e.getMessage());
			return ResponseEntity.status(HttpStatus.FORBIDDEN).build();

		}
	}

	@GetMapping(value="/get/{id}", produces=MediaType.APPLICATION_JSON_VALUE)
	public ResponseEntity<OrderModal> getOrder(@PathVariable Long id){
		logger.error("Fetching order by id: "+id);
		try {
			OrderModal persisted =   orderService.get(id);
			return ResponseEntity.ok(persisted);	
		} catch (RuntimeException e) {
			logger.error("Error occured while fetching order with id {} from DB: ",id +e.getMessage());
			return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
		}
	}
	
	@DeleteMapping(value = "delete/{id}")
	public ResponseEntity<String> delete(@PathVariable Long id){
		orderService.delete(id);
		return ResponseEntity.status(200).body("Order with id "+id+": Delete");
	}
	
	@DeleteMapping(value = "delete")
	public ResponseEntity<String> delete(){
		orderService.delete();
		return ResponseEntity.status(200).body("All Orders are deleted");
	}
}

Repository

Order Repository is defined as mention below. This is a simple CrudRepository which manage the OrderEntity.

package com.itlogiclab.order.repos;

import org.springframework.data.repository.CrudRepository;

import org.springframework.stereotype.Repository;

import com.itlogiclab.order.entity.OrderEntity;


@Repository
public interface OrderRepos extends CrudRepository<OrderEntity, Long>{

	
}
Database Script

Database is to be created with the following script.

DROP DATABASE IF EXISTS orderdb;
CREATE DATABASE orderdb;
USE orderdb;
#ALTER TABLE orders DROP FOREIGN KEY order_fk;
DROP TABLE IF EXISTS address;
DROP TABLE IF EXISTS hibernate_sequence;
DROP TABLE IF EXISTS orders;
CREATE TABLE address (id bigint NOT NULL, city varchar(255), country varchar(255), line1 varchar(255), line2 varchar(255), STATE varchar(255), PRIMARY KEY (id)) engine=InnoDB;
CREATE TABLE hibernate_sequence (next_val bigint) engine=InnoDB;
INSERT INTO hibernate_sequence VALUES ( 1 );
CREATE TABLE orders (id bigint NOT NULL, order_date varchar(255), order_time varchar(255), status integer, delivery_address_id bigint, PRIMARY KEY (id)) engine=InnoDB;
ALTER TABLE orders ADD CONSTRAINT order_fk FOREIGN KEY (delivery_address_id) REFERENCES address (id);
Kubernetes Manifest :

We will be managing the manifest that is used to deploy the springboot application and mysql server. Lets discuss it one by one.

Springboot application

Application is required to define the Deployment and Node port service as part of the manifest.

Deployment:

This deployment is used to deploy the SpringBoot application. This definition of the deployment is as described.

apiVersion: apps/v1
kind: Deployment
metadata: 
    name: order
    labels:
        app: order-app-deployment
spec: 
    replicas: 1
    selector: 
        matchLabels:
            app: order-app-pod

    template: 
        metadata: 
            name: order-pod
            labels:  
                app: order-app-pod
        spec: 
             containers:
                  - name: order-pod
                    image: pandeych009/itlogiclab-restaurant-order@sha256:fa45876a5a84499f8e4ef23c540b2963cc3fd9017072b1938379fc795ef48080
                    imagePullPolicy: IfNotPresent
                    resources:   
                        limits: 
                            memory: "512Mi"
                            cpu: "0.6"                                 
                        requests:
                            memory: "256Mi"
                            cpu: "0.5"        
                    env:
                      - name: SPRING_DATASOURCE_URL
                        value: jdbc:mysql://mysql-order-service:3306/orderdb?useSSL=false&max_allowed_packet=15728640&allowPublicKeyRetrieval=true

                      - name: SPRING_DATASOURCE_USERNAME
                        value: cpandey

                      - name: SPRING_DATASOURCE_PASSWORD
                        value: chandan@1234

                      - name: SPRING_DATASOURCE_DRIVER_CLASS_NAME
                        value: com.mysql.cj.jdbc.Driver                                            
                                                                           
                    ports: 
                       - name: httpport
                         containerPort: 20001 
                         protocol:  TCP
---

    

This deployment define replicas (Number of pods) , selector (pods to target) and template (pod definition) under the spec section. It also include the environment variable that are used to connect to the mysql service.

Service:

Client will access the deployment using service. This service will be of type NodePort, which expose the port of the node to access the underlying application. Here targetPort defined the port of the pod. port define the port of the service and containerPort is the port of the Node, where application is accessible.

apiVersion: v1
kind: Service
metadata: 
    name: order-service
spec: 
    selector: 
        app: order-app-pod
    type: NodePort                      
    ports: 
        - targetPort: 20001
          port: 8080
          nodePort: 30001
Database Manifest:

To deploy the database service we need to have thr following manifest.

StatefulSet:

This is the deployment for the MySQL database. The definition of the StatesfulSet is as defined mention below.


apiVersion: apps/v1
kind: StatefulSet
metadata: 
    name: mysql-order-server
    labels:   
        app: mysql-order-server-app
spec: 
    selector: 
        matchLabels:
            app: mysql-order-app-pod

    serviceName: mysql-h  #Headless Service Name #Master DNS Become: mysql-0.mysql-h.default.svc.cluster.local, mysql-1.mysql-h.default.svccluster.lcoal
    replicas: 1
    template: 
        metadata: 
            name: mysql-order-pod
            labels: 
                app: mysql-order-app-pod
        spec: 
            containers: 
                - name: mysql-container
                  image:  mysql/mysql-server:8.0.23
                  imagePullPolicy: IfNotPresent
                  
                  ports:
                    - containerPort: 3306
                  env:
                    - name: MYSQL_ROOT_PASSWORD
                      value: root

                    - name: MYSQL_USER
                      value: cpandey

                    - name: MYSQL_PASSWORD
                      value: chandan@1234

                    - name: MYSQL_DATABASE
                      value: orderdb

                  volumeMounts: 
                    - name: mysql-initdb ##MOUNT PATH FOR INITIAL SCRIPT TO RUN ON DB
                      mountPath: /docker-entrypoint-initdb.d

                    - name: mysql-server-storage
                      mountPath: /var/lib/mysql

            volumes:
                - name: mysql-initdb
                  configMap: 
                    name: mysql-order-init-script
    
                - name: mysql-server-storage
                  persistentVolumeClaim: 
                      claimName:  mysql-order-pvc

ConfigMap: Initially when the database pods are up, the DB is blank. So there should be a database present before the application starts. We have an initial script which create the database when the StatefulSet is up. The definition of configMap is as mention.

apiVersion: v1
kind: ConfigMap
metadata: 
    name: mysql-order-init-script
data:
    init-script.sql:  |
        CREATE DATABASE IF NOT EXISTS orderdb;
        USE orderdb;
        CREATE TABLE address (id BIGINT NOT NULL, city VARCHAR(255), country VARCHAR(255), line1 VARCHAR(255), line2 VARCHAR(255), STATE VARCHAR(255), PRIMARY KEY (id)) engine=InnoDB;
        CREATE TABLE hibernate_sequence (next_val BIGINT) engine=InnoDB;
        INSERT INTO hibernate_sequence VALUES ( 1 );
        CREATE TABLE orders (id BIGINT NOT NULL, order_date VARCHAR(255), order_time VARCHAR(255), status INTEGER, delivery_address_id BIGINT, PRIMARY KEY (id)) engine=InnoDB;
        ALTER TABLE orders ADD CONSTRAINT order_fk FOREIGN KEY (delivery_address_id) REFERENCES address (id);

Nothing fancy, just a configMap and the script is defined with the name init-script.sql. This script will copied to /docker-entrypoint-initdb.d inside the container and you are set.

PersistentVolume: Since database pods are crashes and spawn again, so the data inside the mysql pods will get destroyed as long as pods terminated, so we need to define the external storage where the transactional data can be stored. PersistentVolume is an answer for that and definition will look like.

apiVersion: v1
kind: PersistentVolume
metadata: 
    name: mysql-order-pv
    labels: 
       app: mysql-order-pv-app 
spec: 
    storageClassName: standard
    capacity: 
        storage: 256Mi
    accessModes: 
        - ReadWriteOnce
    hostPath: 
        path: "/Users/cpandey/dev/docker-data/restaurant/k8/mysql-order" #LOCAL PATH TO THE SYSTEM 
    persistentVolumeReclaimPolicy: Retain

Again the orthodox PersistentVolume which pointing to the path at the local Node. There would be a drawback when we use the hostPath that we cannot scale the StatefulSet with multiple replicas.

PersistentVolumeClaim: PersistentVolumeClaim is consuming the storage from the PersistentVolume and request a specific size and accessModes. Claim in this example is defined as.

apiVersion: v1
kind: PersistentVolumeClaim
metadata: 
    name: mysql-order-pvc
    labels: 
       app: mysql-order-pvc-app
    
spec: 
    storageClassName: standard
    accessModes:
        - ReadWriteOnce
    resources: 
        requests:
           storage: 250Mi

Service: This is required when the front end will be accessing the DB pods. This service type is cluster and defined as.

apiVersion: v1
kind: Service
metadata: 
    name: mysql-order-service
    labels: ##Labels that will be applied to the resource
        app: mysql-order-service-app
spec: 
    ports: 
      - port: 3306
        targetPort: 3306
    clusterIP: None  #This is also termed as Headless service, where ClusterId is None...
    selector:  ##SELECT ANY POD WITH LABEL mysql-app AND OPTIONAL TIER
        app: mysql-order-app-pod

This service will be used to access the Mysql and this service will be used while defining the env property SPRING_DATASOURCE_URL in the backend pods.

jdbc:mysql://mysql-order-service:3306/orderdb

Thats it. Configurations are done and now get ready to run these configurations.

Deployment

We will be deploying the application in the minikube cluster.

Start the minikube cluster and run the kubernetes commands. Minikube is a single node cluster used for testing the configuration.

Run the following kubernetes configuration that we defined above.

kubectl create -f <STATEFUL-SET-FILE-NAME> 
kubectl create -f <ORDER-DEPLOYMENT-FILE-NAME>

The above commands will result in the following output.

Image

Kubernetes Objects are created above. Following points should be noted.

  • POD mysql-order-server-0 is running on port 3306. This service is accessible through mysql-order-service which is exposed on port 3306.
  • The Data files of database is present at the hostpath defined in persistent volume.
  • POD order- is running on port 20001 inside container. This pod is accessible through the service mysql-order-service exposed on NodePort 30001
  • POD order- is accessing mysql-order-service for database connectivity.

Validations

All the objects are created, and we are ready to validate the deployment.

Add Operation:

lets use the curl command to check the add functionality.

#! /bin/bash -e
port=$1

curl -X PUT "http://localhost:$port/order/api/add" \
-H "Content-Type: application/json" \
-d @./order-deployment/test/order_add.json

Curl command will look like the above snippet. The data to push to the container is look like below.

  {
    "orderDate": "20230213",
    "orderTime": "0835",
    "deliveryAddress": {
      "line1": "2 Gatehall Dr",
      "line2": "Third Floor",
      "city": "Parsippany",
      "state": "NJ",
      "pinCode":"07054",
      "country": "United States"
    },
    "status": "INQUEUE"
  }

The above curl command will result as

Screenshot 2023 02 13 At 10.01.32 PM 1024x74
Record Added

Update Operations

Use the curl command to update the currently created record

#! /bin/bash -e
port=$1

curl -X PUT "http://localhost:$port/order/api/add" \
-H "Content-Type: application/json" \
-d @./order-deployment/test/order_update.json

The record will be used to update is as below (as it is created in add operation)

{"orderId":13,"orderDate":"13022023","orderTime":"163101","deliveryAddress":{"line1":"2 Gatehall Dr","line2":"Third Floor","city":"Parsippany","state":"NJ","country":"United States"},"status":"COMPLETED"}

Here we will be updating the status from INQUEUE to COMPLETED.

Screenshot 2023 02 13 At 10.29.16 PM 1024x63
Update the record with id 13

Get Operation

lets get the record with order id 13

port=$1

echo "GET FIRST THE MESSAGE with URL http://localhost:$port/order/api/get/13"
curl "http://localhost:$port/order/api/get/13" \
-H "Accept: application/json"
Screenshot 2023 02 13 At 10.35.24 PM 1024x84
Fetch the record with id 13

Get All Operation:

Lets get all the records from database.

#! /bin/bash -e
port=$1
echo "GET ALL THE MESSAGE with URL http://localhost:$port/order/api/get"
curl "http://localhost:$port/order/api/get" \
-H "Accept: application/json"
Screenshot 2023 02 13 At 10.37.27 PM 1024x157
Fetch all the records

Important Note

Reason to add port additionally in the script.

Since I am using Mac with M1 chip, and because of this NodePort service is not accessible. To access the NodePort service following commands are to be executed.

minikube service order-service --url
This command will start the tunnel for the order-service. We get the following output. 
Because you are using a Docker driver on darwin, the terminal needs to be open to run it.
Then we need to find the port on which this tunnel is open. 
ps -ef | grep docker@127.0.0.1
This command produce the below output. TUNNEL_PORT will be used to access this service from localhost. 
ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -N docker@127.0.0.1 -p 50604 -i /Users/cpandey/.minikube/machines/minikube/id_rsa -L TUNNEL_PORT:CLUSTER_IP:TARGET_PORT
For other system, the service is available on port 30001. Please follow the steps. 
Run minikube tunnel
check the minikube ip, with the command 
minikube ip
This result the ip like 192.168.49.2
then access the service http://192.168.49.2:30001/<URI> 

Codebase

codebase for this article can be found at the github.

https://github.com/pandeych009/itlogiclab-microservices