In a previous article, I talked about an environment, I prepared on my Windows laptop, with a guest Operating System, Docker and Minikube available within an Oracle VirtualBox appliance.
[https://technology.amis.nl/2019/02/12/rapidly-spinning-up-a-vm-with-ubuntu-docker-and-minikube-using-the-vm-drivernone-option-on-my-windows-laptop-using-vagrant-and-oracle-virtualbox/]
In another article I created two versions of a RESTful Web Service Spring Boot application, being a book service. With this service you can add, update, delete and retrieve books from a catalog.
The application uses an H2 in-memory database but is also prepared for using an external MySQL database. For demo purposes I created a 1.0 and 2.0 version of the application.
The latter has some additional fields representing a book.
[https://technology.amis.nl/2019/02/26/building-a-restful-web-service-with-spring-boot-using-an-h2-in-memory-database-and-also-an-external-mysql-database/]
In this article I will describe how these applications will be used in Minikube, together with an external “Dockerized” MySQL database.
Minikube
Minikube is a tool that makes it easy to run Kubernetes locally. Minikube runs a single-node Kubernetes cluster for users looking to try out Kubernetes or develop with it day-to-day.
[https://kubernetes.io/docs/setup/minikube/]
As described in my previous article, I created a subdirectory named env on my Windows laptop. In this directory I created an applications subdirectory.
[https://technology.amis.nl/2019/02/26/building-a-restful-web-service-with-spring-boot-using-an-h2-in-memory-database-and-also-an-external-mysql-database/]
For this article, in the applications directory I created a subdirectory books_service_1.0 and a subdirectory books_service_2.0.
In the books_service_1.0 directory I created a subdirectory target and I placed the following file in there: books_service-1.0.0-SNAPSHOT.jar
In the books_service_2.0 directory I created a subdirectory target and I placed the following file in there: books_service-2.0.0-SNAPSHOT.jar
You can read about how these jar files were created in my previous article.
[https://technology.amis.nl/2019/02/26/building-a-restful-web-service-with-spring-boot-using-an-h2-in-memory-database-and-also-an-external-mysql-database/]
Now that we have the jar files, let’s build the Docker Images for the applications.
I am going to create a development (DEV) and a testing (TST) environment.
In the DEV environment the applications (version 1.0 and 2.0) will be using an H2 in-memory database.
In the TST environment the application (version 1.0) will be using an external MySQL database.
I the table below you can see the overview of the “application landscape” I want to create:
| Environment | Database | Booksservice version |
| DEV | H2 in-memory | 1.0 |
| 2.0 | ||
| TST | MySQL | 1.0 |
Because on my Windows laptop, Minikube runs within an Oracle VirtualBox appliance, I will be using a Linux Command Prompt via ssh.
In the books_service_1.0 directory, I created a Dockerfile with the following content:
# Start with a base image containing Java runtime
FROM openjdk:8-jdk-alpine
# Add Maintainer Info
LABEL maintainer="amis"
# Application runs on port 8080
EXPOSE 8080
# The application's jar file
ARG JAR_FILE=target/books_service-1.0.0-SNAPSHOT.jar
# Add the application's jar to the container
ADD ${JAR_FILE} books_service.jar
# Run the jar file
ENTRYPOINT ["java","-jar","/books_service.jar"]
In the books_service_2.0, I created a Dockerfile with the following content:
# Start with a base image containing Java runtime
FROM openjdk:8-jdk-alpine
# Add Maintainer Info
LABEL maintainer="amis"
# Application runs on port 8080
EXPOSE 8080
# The application's jar file
ARG JAR_FILE=target/books_service-2.0.0-SNAPSHOT.jar
# Add the application's jar to the container
ADD ${JAR_FILE} books_service.jar
# Run the jar file
ENTRYPOINT ["java","-jar","/books_service.jar"]
For my own convenience I created a shell script with several commands. I used this script for setting up an external MySQL database.
In the scripts directory I therefor created file applications.sh with the following content:
#!/bin/bash echo "**** Begin building Docker images" cd /vagrant cd applications cd books_service_1.0 docker build -t booksservice:v1.0 . cd .. cd books_service_2.0 docker build -t booksservice:v2.0 . echo "**** End building Docker images" echo "**** Begin listing Docker images" docker image ls echo "**** End listing Docker images" echo "**** Begin creating Minikube resources" cd /vagrant cd yaml kubectl create -f namespace-development.yaml kubectl create -f namespace-testing.yaml kubectl create -f persistent-volume-mysql.yaml kubectl create -f persistent-volume-claim-mysql.yaml kubectl create -f deployment-mysql.yaml kubectl create -f service-mysql.yaml echo "**** End creating Minikube resources"
Interact with the Cluster via kubectl
I went to the env directory and opened a Windows Command Prompt (cmd) to access linux (within the VirtualBox Appliance) via ssh: vagrant ssh
Linux Command Prompt: cd /vagrant
Linux Command Prompt: cd scripts
Linux Command Prompt: ./applications.sh
Although a shell script is used, I will describe each command and its output separately.
Docker build
With the following command, a Docker Image for application books_service_1.0 is build: docker build -t booksservice:v1.0 .
With the following command, a Docker Image for application books_service_2.0 is build: docker build -t booksservice:v2.0 .
These commands returned the following output:
**** Begin building Docker images
Sending build context to Docker daemon 37.84MB
Step 1/6 : FROM openjdk:8-jdk-alpine
8-jdk-alpine: Pulling from library/openjdk
6c40cc604d8e: Pull complete
e78b80385239: Pull complete
47317d99e629: Pull complete
Digest: sha256:893271fc760d6adabd79b6b8c20e24bdba47f7b253ab63b13836ac3a2a16ff70
Status: Downloaded newer image for openjdk:8-jdk-alpine
---> 792ff45a2a17
Step 2/6 : LABEL maintainer="amis"
---> Running in af59bb458dc8
Removing intermediate container af59bb458dc8
---> 9a5551c8c55a
Step 3/6 : EXPOSE 8080
---> Running in b2db926a00b3
Removing intermediate container b2db926a00b3
---> 9e21698eab45
Step 4/6 : ARG JAR_FILE=target/books_service-1.0.0-SNAPSHOT.jar
---> Running in 8622706e774a
Removing intermediate container 8622706e774a
---> ed8ef8c169b0
Step 5/6 : ADD ${JAR_FILE} books_service.jar
---> 51c5cffec129
Step 6/6 : ENTRYPOINT ["java","-jar","/books_service.jar"]
---> Running in 895f7003d342
Removing intermediate container 895f7003d342
---> 9b384295f40c
Successfully built 9b384295f40c
Successfully tagged booksservice:v1.0
Sending build context to Docker daemon 37.84MB
Step 1/6 : FROM openjdk:8-jdk-alpine
---> 792ff45a2a17
Step 2/6 : LABEL maintainer="amis"
---> Using cache
---> 9a5551c8c55a
Step 3/6 : EXPOSE 8080
---> Using cache
---> 9e21698eab45
Step 4/6 : ARG JAR_FILE=target/books_service-2.0.0-SNAPSHOT.jar
---> Running in 92e4e6577bea
Removing intermediate container 92e4e6577bea
---> 95b391fd15a6
Step 5/6 : ADD ${JAR_FILE} books_service.jar
---> e54353066caf
Step 6/6 : ENTRYPOINT ["java","-jar","/books_service.jar"]
---> Running in ba0f84be6a63
Removing intermediate container ba0f84be6a63
---> 62f8e35b747d
Successfully built 62f8e35b747d
Successfully tagged booksservice:v2.0
**** End building Docker images
Docker image ls
With the following command, a list of all the docker images on your system is returned: docker image ls
This command returned the following output:
**** Begin listing Docker images REPOSITORY TAG IMAGE ID CREATED SIZE booksservice v2.0 62f8e35b747d Less than a second ago 143MB booksservice v1.0 9b384295f40c 1 second ago 143MB openjdk 8-jdk-alpine 792ff45a2a17 3 weeks ago 105MB k8s.gcr.io/kubernetes-dashboard-amd64 v1.10.1 f9aed6605b81 2 months ago 122MB k8s.gcr.io/kube-proxy v1.12.4 6d393e89739f 2 months ago 96.5MB k8s.gcr.io/kube-apiserver v1.12.4 c04b373449d3 2 months ago 194MB k8s.gcr.io/kube-controller-manager v1.12.4 51b2a8e5ff78 2 months ago 164MB k8s.gcr.io/kube-scheduler v1.12.4 c1b5e63c0b56 2 months ago 58.4MB k8s.gcr.io/etcd 3.2.24 3cab8e1b9802 5 months ago 220MB k8s.gcr.io/coredns 1.2.2 367cdc8433a4 6 months ago 39.2MB k8s.gcr.io/kube-addon-manager v8.6 9c16409588eb 12 months ago 78.4MB k8s.gcr.io/pause 3.1 da86e6ba6ca1 14 months ago 742kB gcr.io/k8s-minikube/storage-provisioner v1.8.1 4689081edb10 15 months ago 80.8MB **** End listing Docker images
Use kubectl to create a container, based on a Docker Image
Instead of using docker run, we are now going to use kubectl.
You can create and manage a resource by using the Kubernetes command line interface, kubectl.
Kubectl uses the Kubernetes API to interact with the cluster.
For creating a resource with kubectl , a file can be used. This file is called a manifest.
Kubernetes manifests can be defined in json or yaml. The file extension .yaml, .yml, and .json can be used.
[https://kubernetes.io/docs/reference/kubectl/cheatsheet/]
I used yaml files.
For setting up a yaml file for a certain resource I kindly refer you to the Kubernetes documentation.
[https://kubernetes.io/docs/concepts/overview/working-with-objects/kubernetes-objects/]
In the env directory I created a subdirectory yaml.
Navigate to the yaml directory.
Linux Command Prompt: cd yaml
As a naming convention for the yaml files, I used: <resource kind>-<specific name>.yaml
Namespaces
Within Minikube, I created resources with a specific namespace. So first I had to create these namespaces.
I added to the yaml directory a file namespace-development.yaml with the following contents:
apiVersion: v1 kind: Namespace metadata: name: "nl-amis-development" labels: name: "nl-amis-development"
I added to the yaml directory a file namespace-testing.yaml with the following contents:
apiVersion: v1 kind: Namespace metadata: name: "nl-amis-testing" labels: name: "nl-amis-testing"
With the following commands, the namespaces are created:
kubectl create -f namespace-development.yaml namespace/nl-amis-development created
kubectl create -f namespace-testing.yaml namespace/nl-amis-testing created
You can list the current namespaces in a cluster using:
kubectl get namespaces
This command returned the following output:
NAME STATUS AGE default Active 7m1s kube-public Active 6m56s kube-system Active 7m1s nl-amis-development Active 2m49s nl-amis-testing Active 2m49s
Or via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=default
Navigate to Cluster | Namespaces:
Remark:
In this article some resources that I created have the Namespace: nl-amis-development
In a yaml file this can be done via:
namespace: nl-amis-development
Setting up the external “Dockerized” MySQL database
So now let’s focus on setting up an external MySQL database, running in a separate Docker container.
I used the following yaml files to set everything up:
- persistent-volume-mysql.yaml
- persistent-volume-claim-mysql.yaml
- deployment-mysql.yaml
- service-mysql.yaml
PersistentVolume (MySQL)
I added to the yaml directory a file persistent-volume-mysql.yaml with the following contents:
kind: PersistentVolume apiVersion: v1 metadata: name: mysql-persistent-volume namespace: nl-amis-testing labels: type: local spec: storageClassName: manual capacity: storage: 1Gi accessModes: - ReadWriteOnce hostPath: path: "/mnt/data"
Create the PersistentVolume:
kubectl create -f persistent-volume-mysql.yaml persistentvolume/mysql-persistent-volume created
You can list the current persistentvolumes in a cluster using:
kubectl get persistentvolumes
This command returned the following output:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS REASON AGE mysql-persistent-volume 1Gi RWO Retain Bound nl-amis-testing/mysql-pv-claim manual 3m51s
Via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=nl-amis-testing
Navigate to Cluster | Persistent Volumes:
I f you click on mysql-persistent-volume the following details are shown:
PersistentVolumeClaim (MySQL)
Add to the yaml directory a file persistent-volume-claim-mysql.yaml with the following contents:
apiVersion: v1 kind: PersistentVolumeClaim metadata: name: mysql-pv-claim namespace: nl-amis-testing spec: storageClassName: manual accessModes: - ReadWriteOnce resources: requests: storage: 1Gi
Create the PersistentVolumeClaim:
kubectl create -f persistent-volume-claim-mysql.yaml persistentvolumeclaim/mysql-pv-claim created
You can list the current persistentvolumeclaims in a cluster using:
kubectl get persistentvolumeclaim --all-namespaces
This command returned the following output:
NAMESPACE NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE nl-amis-testing mysql-pv-claim Bound mysql-persistent-volume 1Gi RWO manual 13m
Via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=nl-amis-testing
Navigate to Config and Storage | Persistent Volume Claims:
If you click on mysql-pv-claim the following details are shown:
Deployments (MySQL)
I added to the yaml directory a file deployment-mysql.yaml with the following contents:
apiVersion: apps/v1 kind: Deployment metadata: name: mysql namespace: nl-amis-testing labels: app: mysql version: "1.0" environment: testing spec: replicas: 1 selector: matchLabels: app: mysql version: "1.0" environment: testing strategy: type: Recreate template: metadata: labels: app: mysql version: "1.0" environment: testing spec: containers: - image: mysql:5.6 name: mysql env: # Use secret in real usage - name: MYSQL_ROOT_PASSWORD value: password ports: - containerPort: 3306 name: mysql volumeMounts: - name: mysql-persistent-storage mountPath: /var/lib/mysql volumes: - name: mysql-persistent-storage persistentVolumeClaim: claimName: mysql-pv-claim
Remark:
This deployment uses the mysql:5.6 image.
The replicas are set to 1, so the ReplicaSet ensures that 1 pod replica is running at any given time.
The ReplicaSet manages all the pods with labels that match the selector. In my case these labels are:
| Label key | Label value |
| app | mysql |
| version | 1.0 |
| environment | testing |
Create the deployment:
kubectl create -f deployment-mysql.yaml deployment.apps/mysql created
You can list the deployments in a cluster using:
kubectl get deployments --all-namespaces
With the following output:
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE kube-system coredns 2 2 2 2 27m kube-system kubernetes-dashboard 1 1 1 1 26m nl-amis-testing mysql 1 1 1 1 23m
Via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=nl-amis-testing
Navigate to Workloads | Deployments:
The deployment I created contains a Replica Set:
Replica Sets (MySQL)
The Replica Set contains one Pod:
Pods (MySQL)
The Pod includes 1 container (based on image mysql:5.6) and has IP address 172.17.0.5:
You can list the pods in a cluster using:
kubectl get pods --all-namespaces
With the following output:
NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-576cbf47c7-8v48m 1/1 Running 0 31m kube-system coredns-576cbf47c7-n4b9r 1/1 Running 0 31m kube-system etcd-minikube 1/1 Running 0 30m kube-system kube-addon-manager-minikube 1/1 Running 0 30m kube-system kube-apiserver-minikube 1/1 Running 0 30m kube-system kube-controller-manager-minikube 1/1 Running 0 30m kube-system kube-proxy-clpgn 1/1 Running 0 31m kube-system kube-scheduler-minikube 1/1 Running 0 30m kube-system kubernetes-dashboard-5bff5f8fb8-z2cfc 1/1 Running 0 31m kube-system storage-provisioner 1/1 Running 0 31m nl-amis-testing mysql-64846c7974-cnmch 1/1 Running 0 27m
Services (MySQL)
I added to the yaml directory a file service-mysql.yaml with the following contents:
kind: Service apiVersion: v1 metadata: name: mysql-service namespace: nl-amis-testing labels: app: mysql version: "1.0" environment: testing spec: selector: app: mysql version: "1.0" environment: testing ports: - port: 3306 selector: app: mysql clusterIP: None
Create the service:
kubectl create -f service-mysql.yaml service/mysql-service created
You can list the services in a cluster using:
kubectl get services --all-namespaces
With the following output:
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 34m kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 34m kube-system kubernetes-dashboard ClusterIP 10.97.35.192 <none> 80/TCP 34m nl-amis-testing mysql-service ClusterIP None <none> 3306/TCP 30m
Via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=nl-amis-testing
Navigate to Discovery and Load Balancing| Services:
Click on the service.
Via the Details you can see the following:
- The Cluster IP address of this service is: None
- The Internal endpoints of this service is:
- mysql-service.nl-amis-testing:3306 TCP
- The Endpoint of the Pod/Container is:
- 172.17.0.5:3306 TCP
Mysql-client
In my previous article, in the application-testing.properties you can see that a MySQL database named test is used.
[https://technology.amis.nl/2019/02/26/building-a-restful-web-service-with-spring-boot-using-an-h2-in-memory-database-and-also-an-external-mysql-database/]
So, I first created that database. For this I used a mysql-client.
kubectl --namespace=nl-amis-testing run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql-service.nl-amis-testing -ppassword If you don't see a command prompt, try pressing enter.
I had to click on the Enter button.
mysql> show databases;
With the following output:
mysql> create database test;
Query OK, 1 row affected (0.00 sec)
mysql> show databases;
With the following output:
mysql> exit
Bye pod "mysql-client" deleted
Deployments (booksservice)
So now that the MySQL database is set up and the test database is created, let’s focus on creating deployments for both versions of the booksservice (RESTful Web Service Spring Boot application).
I wanted to create the following deployments:
| Environment | Port | Booksservice version |
| DEV | 9090 | 1.0 |
| 2.0 | ||
| TST | 9091 | 1.0 |
In my previous article I talked about a RESTful Web Service Spring Boot application, being able to use different profiles.
[https://technology.amis.nl/2019/02/26/building-a-restful-web-service-with-spring-boot-using-an-h2-in-memory-database-and-also-an-external-mysql-database/]
For the booksservice (RESTful Web Service Spring Boot application), I created a development profile and a testing profile. In the development profile I use an H2 in-memory database, while in the testing profile I use an external MySQL database.
I added to the yaml directory a file deployment-booksservice-dev-v1.0.yaml with the following contents:
apiVersion: apps/v1 kind: Deployment metadata: name: booksservice-v1.0 namespace: nl-amis-development labels: app: booksservice version: "1.0" environment: development spec: replicas: 2 selector: matchLabels: app: booksservice version: "1.0" environment: development template: metadata: labels: app: booksservice version: "1.0" environment: development spec: containers: - name: booksservice-v1-0-container image: booksservice:v1.0 env: - name: spring.profiles.active value: "development" ports: - containerPort: 9090
Remark:
This deployment uses the booksservice (RESTful Web Service Spring Boot application) with H2 as an embedded in-memory database.
The replicas are set to 2, so the ReplicaSet ensures that 2 pod replicas are running at any given time.
The ReplicaSet manages all the pods with labels that match the selector. In my case these labels are:
| Label key | Label value |
| app | booksservice |
| version | 1.0 |
| environment | development |
Via the Environment variables, the active Spring profile has to be set up.
With regard to the metadata name (metadata.name), the following convention must be used:
a DNS-1123 subdomain must consist of lower case alphanumeric characters, ‘-‘ or ‘.’, and must start and end with an alphanumeric character (e.g. ‘example.com’, regex used for validation is ‘[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*’)
With regard to the metadata name (spec.template.spec.containers[0].name), the following convention must be used:
a DNS-1123 label must consist of lower case alphanumeric characters or ‘-‘, and must start and end with an alphanumeric character (e.g. ‘my-name’, or ‘123-abc’, regex used for validation is ‘[a-z0-9]([-a-z0-9]*[a-z0-9])?’)
Create the deployment:
kubectl create -f deployment-booksservice-dev-v1.0.yaml deployment.apps/booksservice-v1.0 created
I added to the yaml directory a file deployment-booksservice-dev-v2.0.yaml with the following contents:
apiVersion: apps/v1 kind: Deployment metadata: name: booksservice-v2.0 namespace: nl-amis-development labels: app: booksservice version: "2.0" environment: development spec: replicas: 2 selector: matchLabels: app: booksservice version: "2.0" environment: development template: metadata: labels: app: booksservice version: "2.0" environment: development spec: containers: - name: booksservice-v2-0-container image: booksservice:v2.0 env: - name: spring.profiles.active value: "development" ports: - containerPort: 9090
Remark:
This deployment uses the booksservice (RESTful Web Service Spring Boot application) with H2 as an embedded in-memory database.
The replicas are set to 2, so the ReplicaSet ensures that 2 pod replicas are running at any given time.
The ReplicaSet manages all the pods with labels that match the selector. In my case these labels are:
| Label key | Label value |
| app | booksservice |
| version | 2.0 |
| environment | development |
Via the Environment variables, the active Spring profile has to be set up.
Create the deployment:
kubectl create -f deployment-booksservice-dev-v2.0.yaml deployment.apps/booksservice-v2.0 created
I added to the yaml directory a file deployment-booksservice-tst-v1.0.yaml with the following contents:
apiVersion: apps/v1 kind: Deployment metadata: name: booksservice-v1.0 namespace: nl-amis-testing labels: app: booksservice version: "1.0" environment: testing spec: replicas: 2 selector: matchLabels: app: booksservice version: "1.0" environment: testing template: metadata: labels: app: booksservice version: "1.0" environment: testing spec: containers: - name: booksservice-v1-0-container image: booksservice:v1.0 env: - name: spring.profiles.active value: "testing" - name: spring.datasource.url value: "jdbc:mysql://mysql-service.nl-amis-testing/test?allowPublicKeyRetrieval=true&useSSL=false" ports: - containerPort: 9091
Remark:
This deployment uses the booksservice (RESTful Web Service Spring Boot application) with H2 as an embedded in-memory database.
The replicas are set to 2, so the ReplicaSet ensures that 2 pod replicas are running at any given time.
The ReplicaSet manages all the pods with labels that match the selector. In my case these labels are:
| Label key | Label value |
| app | booksservice |
| version | 1.0 |
| environment | testing |
Via the Environment variables, the active Spring profile has to be set up. Also the correct Spring datasource url has to be set up. See the internal endpoint of the mysql-service resource, I created earlier.
Create the deployment:
kubectl create -f deployment-booksservice-tst-v1.0.yaml deployment.apps/booksservice-v1.0 created
You can list the deployments in a cluster using:
kubectl get deployments --all-namespaces
With the following output:
NAMESPACE NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE kube-system coredns 2 2 2 2 38m kube-system kubernetes-dashboard 1 1 1 1 38m nl-amis-development booksservice-v1.0 2 2 2 2 36s nl-amis-development booksservice-v2.0 2 2 2 2 23s nl-amis-testing booksservice-v1.0 2 2 2 2 10s nl-amis-testing mysql 1 1 1 1 34m
Or via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=default
Navigate to Workloads | Deployments:
Remember, in Minikube the Namespace that is used is: default
So, we first have to switch to the development namespace (via Namespace | nl-amis-development) and then navigate to Workloads | Deployments:
Next, we switch to the testing namespace (via Namespace | nl-amis-testing) and then navigate to Workloads | Deployments:
Each deployment I have created contains a Replica Set. Let’s for example focus on the testing namespace. Click on booksservice-v1.0.
The booksservice-v1.0 deployment we have created contains a Replica Set:
Replica Sets (booksservice)
The Replica Set contains two Pods:
Pods (booksservice)
The first Pod includes 1 container (based on image booksservice:v1.0) and has IP address 172.17.0.10:
The second Pod includes 1 container (based on image booksservice:v1.0) and has IP address 172.17.0.11:
You can list the pods in a cluster using:
kubectl get pods --all-namespaces
With the following output:
NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-576cbf47c7-8v48m 1/1 Running 0 54m kube-system coredns-576cbf47c7-n4b9r 1/1 Running 0 54m kube-system etcd-minikube 1/1 Running 0 53m kube-system kube-addon-manager-minikube 1/1 Running 0 53m kube-system kube-apiserver-minikube 1/1 Running 0 53m kube-system kube-controller-manager-minikube 1/1 Running 0 53m kube-system kube-proxy-clpgn 1/1 Running 0 54m kube-system kube-scheduler-minikube 1/1 Running 0 53m kube-system kubernetes-dashboard-5bff5f8fb8-z2cfc 1/1 Running 0 54m kube-system storage-provisioner 1/1 Running 0 54m nl-amis-development booksservice-v1.0-68785bc6ff-k9cg6 1/1 Running 0 16m nl-amis-development booksservice-v1.0-68785bc6ff-q2fkj 1/1 Running 0 16m nl-amis-development booksservice-v2.0-869c5bb47d-6shjh 1/1 Running 0 16m nl-amis-development booksservice-v2.0-869c5bb47d-dqftq 1/1 Running 0 16m nl-amis-testing booksservice-v1.0-5bcd5fddbd-fxzxs 1/1 Running 0 16m nl-amis-testing booksservice-v1.0-5bcd5fddbd-pb64h 1/1 Running 0 16m nl-amis-testing mysql-64846c7974-cnmch 1/1 Running 0 50m
You can list the pods in a certain namespace (of a cluster) using:
kubectl --namespace=nl-amis-testing get pods
With the following output:
NAME READY STATUS RESTARTS AGE booksservice-v1.0-5bcd5fddbd-fxzxs 1/1 Running 0 16m booksservice-v1.0-5bcd5fddbd-pb64h 1/1 Running 0 16m mysql-64846c7974-cnmch 1/1 Running 0 50m
Services (booksservice)
I added to the yaml directory a file service-booksservice-dev-v1.0.yaml with the following contents:
kind: Service apiVersion: v1 metadata: name: booksservice-v1-0-service namespace: nl-amis-development labels: app: booksservice version: "1.0" environment: development spec: selector: app: booksservice version: "1.0" environment: development type: NodePort ports: - protocol: TCP nodePort: 30010 port: 9190 targetPort: 9090
Remark:
With regard to metadata name (metadata.name) the following convention must be used:
a DNS-1035 label must consist of lower case alphanumeric characters or ‘-‘, start with an alphabetic character, and end with an alphanumeric character (e.g. ‘my-name’, or ‘abc-123’, regex used for validation is ‘[a-z]([-a-z0-9]*[a-z0-9])?’)
Create the service:
kubectl create -f service-booksservice-dev-v1.0.yaml service/booksservice-v1-0-service created
I added to the yaml directory a file service-booksservice-dev-v2.0.yaml with the following contents:
kind: Service apiVersion: v1 metadata: name: booksservice-v2-0-service namespace: nl-amis-development labels: app: booksservice version: "2.0" environment: development spec: selector: app: booksservice version: "2.0" environment: development type: NodePort ports: - protocol: TCP nodePort: 30020 port: 9190 targetPort: 9090
Create the service:
kubectl create -f service-booksservice-dev-v2.0.yaml service/booksservice-v2-0-service created
I added to the yaml directory a file service-booksservice-tst-v1.0.yaml with the following contents:
kind: Service apiVersion: v1 metadata: name: booksservice-v1-0-service namespace: nl-amis-testing labels: app: booksservice version: "1.0" environment: testing spec: selector: app: booksservice version: "1.0" environment: testing type: NodePort ports: - protocol: TCP nodePort: 30110 port: 9191 targetPort: 9091
Create the service:
kubectl create -f service-booksservice-tst-v1.0.yaml service/booksservice-v1-0-service created
You can list the services in a cluster using:
kubectl get services --all-namespaces
With the following output:
NAMESPACE NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE default kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 56m kube-system kube-dns ClusterIP 10.96.0.10 <none> 53/UDP,53/TCP 56m kube-system kubernetes-dashboard ClusterIP 10.97.35.192 <none> 80/TCP 55m nl-amis-development booksservice-v1-0-service NodePort 10.96.159.109 <none> 9190:30010/TCP 37s nl-amis-development booksservice-v2-0-service NodePort 10.110.146.146 <none> 9190:30020/TCP 26s nl-amis-testing booksservice-v1-0-service NodePort 10.99.232.66 <none> 9191:30110/TCP 13s nl-amis-testing mysql-service ClusterIP None <none> 3306/TCP 52m
You can list the services in a certain namespace (of a cluster) using:
kubectl --namespace=nl-amis-testing get services
With the following output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE booksservice-v1-0-service NodePort 10.99.232.66 <none> 9191:30110/TCP 31s mysql-service ClusterIP None <none> 3306/TCP 52m
Or via the Kubernetes Web UI (Dashboard):
http://127.0.0.1:8001/api/v1/namespaces/kube-system/services/http:kubernetes-dashboard:/proxy/#!/node?namespace=default
Again, we first have to switch to the development namespace (via Namespace | nl-amis-development) and then navigate to Discovery and Load Balancing | Services:
Next, we switch to the testing namespace (via Namespace | nl-amis-testing) and then Navigate to Discovery and Load Balancing | Services:
Each service I have created contains details. Let’s for example focus on the testing namespace. Click on the booksservice-v1-0-service service.
Via the Details you can see the following:
- The Cluster IP address of this service is: 10.99.232.66
- The Internal endpoints of this service are:
- booksservice-v1-0-service.nl-amis-testing:9191 TCP
- booksservice-v1-0-service.nl-amis-testing:30110 TCP
- The Endpoints of the Pods/Containers are:
- 172.17.0.10:9091 TCP
- 172.17.0.11:9091 TCP
Pods
The first Pod includes 1 container (based on image booksservice:v1.0) and has IP address 172.17.0.10:
Here you can also see the Environment variables, this Pod/Container uses:
spring.profiles.active: tetsting spring.datasource.url: jdbc:mysql://mysql-service.nl-amis-testing/test?allowPublicKeyRetrieval=true&useSSL=falsea
The second Pod includes 1 container (based on image booksservice:v1.0) and has IP address 172.17.0.11:
Here you can also see the Environment variables, this Pod/Container uses:
spring.profiles.active: tetsting spring.datasource.url: jdbc:mysql://mysql-service.nl-amis-testing/test?allowPublicKeyRetrieval=true&useSSL=falsea
Next we take a look at the Logs of the Pods:
In these logs, you can see the Environment variables that were used, when creating this Pod/Container.
Calling the booksservice application
Alright, so now the application should be running two Docker container. Let’s test how the application responds by firing some requests.
First, I added some books with POST-requests to the booksservice application:
curl --header "Content-Type: application/json" --request POST --data '{"id": 1, "title": "The Threat: How the FBI Protects America in the Age of Terror and Trump", "author": "Andrew G. McCabe", "type": "Hardcover", "price": 17.99, "numOfPages": 288, "language": "English", "isbn13": "978-1250207579"}' http://10.99.232.66:9191/books
curl --header "Content-Type: application/json" --request POST --data '{"id": 2, "title": "Becoming", "publishDate": "2018-11-13", "author": "Michelle Obama", "type": "Hardcover", "price": 17.88, "numOfPages": 448, "publisher": "Crown Publishing Group; First Edition edition", "language": "English", "isbn13": "978-1524763138"}' http://10.99.232.66:9191/books
As you can see, I used the Cluster IP address of the service.
Next, we should be able to see our updated list at the /books path with a GET request. First I tried the service IP address:
curl http://10.99.232.66:9191/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
I also tried, the first Pod IP address:
curl http://172.17.0.10:9091/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
And then I tried, the second Pod IP address:
curl http://172.17.0.11:9091/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
Remember, this deployment uses the booksservice (RESTful Web Service Spring Boot Application) with an external MySQL database, running in a separate Docker container. So, the first and second Pod both use the same external MySQL database. Therefor it doesn’t matter to which Pod the service sent the POST-requests. Each of the above GET-requests responds with the same answer (all 2 books).
As a final step I checked the contents of the book table. Again I used a mysql-client for this.
kubectl --namespace=nl-amis-testing run -it --rm --image=mysql:5.6 --restart=Never mysql-client -- mysql -h mysql-service.nl-amis-testing -ppassword If you don't see a command prompt, try pressing enter.
I had to click on the Enter button.
mysql> use test; Reading table information for completion of table and column names You can turn off this feature to get a quicker startup with -A Database changed
mysql> select * from book;
With the following output:
Again, both books that I added, were returned.
mysql> exit Bye pod "mysql-client" deleted
ReplicaSet
A ReplicaSet’s purpose is to maintain a stable set of replica Pods running at any given time. As such, it is often used to guarantee the availability of a specified number of identical Pods.
A ReplicaSet is defined with fields, including a selector that specifies how to identify Pods it can acquire, a number of replicas indicating how many Pods it should be maintaining, and a pod template specifying the data of new Pods it should create to meet the number of replicas criteria. A ReplicaSet then fulfills its purpose by creating and deleting Pods as needed to reach the desired number. When a ReplicaSet needs to create new Pods, it uses its Pod template.
[https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/]
Remember, I added to the yaml directory a file deployment-booksservice-tst-v1.0.yaml. Below you can see the fields, mentioned above:
apiVersion: apps/v1 kind: Deployment metadata: name: booksservice-v1.0 namespace: nl-amis-testing labels: app: booksservice version: "1.0" environment: testing spec: replicas: 2 selector: matchLabels: app: booksservice version: "1.0" environment: testing template: metadata: labels: app: booksservice version: "1.0" environment: testing spec: containers: - name: booksservice-v1-0-container image: booksservice:v1.0 env: - name: spring.profiles.active value: "testing" - name: spring.datasource.url value: "jdbc:mysql://mysql-service.nl-amis-testing/test?allowPublicKeyRetrieval=true&useSSL=false" ports: - containerPort: 9091
Remark:
This deployment uses the booksservice (RESTful Web Service Spring Boot application) with H2 as an embedded in-memory database.
The replicas are set to 2, so the ReplicaSet ensures that 2 pod replicas are running at any given time.
The ReplicaSet manages all the pods with labels that match the selector. In my case these labels are:
| Label key | Label value |
| app | booksservice |
| version | 1.0 |
| environment | testing |
As a test case I then deleted a pod, in order to check what happens next.
I deleted the first Pod (with name booksservice-v1.0-5bcd5fddbd-fxzxs) which includes 1 container (based on image booksservice:v1.0) and has IP address 172.17.0.10:
Below, you can see that a new Pod is being created (with Status = Waiting: ContainerCreating):
After some time, the new Pod (with name booksservice-v1.0-5bcd5fddbd-mp95s) is running and the total pods is 2 again.
Click on the service.
So again, let’s test how the application responds by firing some requests.
We should be able to see our list of books, at the /books path with a GET request. First I tried the service IP address:
curl http://10.99.232.66:9191/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
I also tried, the old Pod IP address:
curl http://172.17.0.10:9091/books curl: (7) Failed to connect to 172.17.0.10 port 9091: No route to host
As expected, an error is shown, because this Pod no longer exists.
And then I tried, the second Pod IP address:
curl http://172.17.0.11:9091/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
And then I tried, the new Pod IP address:
curl http://172.17.0.12:9091/books
[{"id":"1","title":"The Threat: How the FBI Protects America in the Age of Terror and Trump","author":"Andrew G. McCabe","type":"Hardcover","price":17.99,"numOfPages":288,"language":"English","isbn13":"978-1250207579"},{"id":"2","title":"Becoming","author":"Michelle Obama","type":"Hardcover","price":17.88,"numOfPages":448,"language":"English","isbn13":"978-1524763138"}]
So, the ReplicaSet did fulfill its purpose by creating a new Pod. The service, via the selector (and using label matching) can identify the new Pod.
With this final check I conclude this article.

Thanks for this great tutorial…. I am wondering if you left out a step where you need to push your booksservice Docker images to a registry (e.g. docker hub) in order for the deployment yaml files to work. Thanks!
Hello Gerardo,
I didn’t push the booksservice Docker images to a registry because that is not necessary in my case.
In my article I described a step in which I used a Docker file for books_service_1.0, for example. In the file applications.sh there is a part that builds the image (docker build -t booksservice:v1.0) based on the Docker file (in the books_Service_1.0 directory).
In the file deployment-booksservice-dev-v1.0.yaml this image (image: booksservice:v1.0) is used for creating the booksservice-v1-0-container when the deployment is done (with the command: kubectl create -f deployment-booksservice-dev-v1.0.yaml).