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.
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.
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
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.
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"
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"
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 [email protected]
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 [email protected] -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