In the previous post When Java Meets Docker Part I, we covered the basics of Cloud-Native, Docker, and how to deploy a simple Java application. In this post, we'll take it a step further. First, we'll implement a more sophisticated application using the Spring Boot framework. Then, we'll deploy this application in a more efficient and practical way using Docker Compose. Finally, we'll go even further by deploying it in Kubernetes on our local environment using Minikube. Let's start !
The architecture of our solution remains the same: a Java application that connects to a database. However, this time, we're going to have a Spring Boot application that exposes REST APIs. We'll have two API endpoints: POST http://myserver:8080/api/player/v1/add to create a new player and save it into our database, and GET http://myserver:8080/api/player/v1/list to retrieve the list of players we have in our database. Additionally, we'll have an API for testing purposes.
As the objective of this post is not Java development itself, we won't go into too much detail about how to develop an application in Spring Boot.
Players app v2
For our updated "Players" application, we will be able to add and view the list of players through REST APIs. To achieve this, we'll have two API endpoints. The first will create a new player and save it into our database: POST http://myserver:8080/api/player/v1/add. The second will retrieve the list of players we have in our database: GET http://myserver:8080/api/player/v1/list.
In summary, our application should now look like this:
In this solution we will not need to create a table in the database, springboot will take care of it. To achieve this we have put the property spring.jpa.hibernate.ddl-auto=create-drop in our properties file, which will indicate to our application to drop and create the table based on the class (entity) Player.java at every start up.
The code source of the application can be found in this GitHub repo : https://github.com/MalikooR/players_spring_boot/tree/main/players
Push docker image to docker hub
Before we start using Docker Compose, let's containerize our Spring Boot application and push it to Docker Hub. This will allow us to use the container at different stages of this post. Creating the container is a simple process and follows the same steps we covered in the previous post.
Once we've executed the Maven package goal on our application, a .jar file will be created in the target folder. We'll then copy this jar file to a new folder and create a Dockerfile with the following content:
FROM adoptopenjdk:11-jre-hotspot
ARG JAR_FILE=players.jar
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java","-jar","/app.jar"]
And run this command :
docker build -t malikoo/players:1.0 .
Where malikoo is the dockerhub user, players is the repository name and the number after the semicolon is the image tag.
Now we can push our image into docker hub simply by executing the command below :
docker push malikoo/players:1.0
Before running the docker push command, you need to run docker login and use your docker hub credentials to connect to the registry.
Docker compose
In this step we are going to deploy our application using docker compose. Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration.
In our case our we will have two services, our java application running on playersapp container exposing a service on the port 8080 which will be mapped to the port 8080 of the host machine and the Postgres db playersdb that will expose a service on a port 5432 mapped to the port 5555 of the host machine.
create a yaml file and past the following :
services:
web:
container_name: playersapp
depends_on:
- post
image: malikoo/players:1.0
ports:
- "8080:8080"
environment:
POSTGRES_URL: playersdb:5432/testdb
post:
image: postgres
container_name: playersdb
ports:
- "5555:5432"
environment:
POSTGRES_PASSWORD: postgres
POSTGRES_DB: testdb
Note that the default compose file name is docker-compose.yaml, with this filename you won't need to specify the file to docker compose as long as it's in the current path. Otherwise, you should use -f option to specify your custom file name.
Now that our docker compose file is ready, all we need is to run this command :
docker compose up
We can test that the process went well by checking this address:
$ curl --location --request GET 'localhost:8080/api/player/v1/test'
Output: This is a test
If you can see the test message, it means that now our Spring-boot application is running and listening to the port 8080, now let's try to make some API call and confirm that our application is talking properly to our database.
let's run those two API calls to add two players :
$ curl -i --location --request POST 'localhost:8080/api/player/v1/add' \
--header 'Content-Type: application/json' \
--data-raw '{
"firstName": "Roberto",
"lastName": "Boutabout",
"team":"Real Madrid",
"position":"CF"
}'
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 09 Feb 2022 19:18:58 GMT
{"id":1,"firstName":"Roberto","lastname":"Boutabout","team":"Real Madrid","position":"CF"}
$ curl -i --location --request POST 'localhost:8080/api/player/v1/add' \
--header 'Content-Type: application/json' \
--data-raw '{
"firstName": "Mahmoud",
"lastName": "Donadoni",
"team":"FC Barcelona",
"position":"LW"
}'
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 09 Feb 2022 19:19:48 GMT
{"id":2,"firstName":"Mahmoud","lastname":"Donadoni","team":"FC Barcelona","position":"LW"}
We can see above that the two players have been added with a unique Id.
Now, let's try to get the list of our players :
$ curl -i --location --request GET 'localhost:8080/api/player/v1/list'
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Date: Wed, 09 Feb 2022 19:21:45 GMT
[{"id":1,"firstName":"Roberto","lastname":"Boutabout","team":"Real Madrid","position":"CF"},{"id":2,"firstName":"Mahmoud","lastname":"Donadoni","team":"FC Barcelona","position":"LW"},]
Fine, at this stage we already see that our deployment is more elegant and simpler, but what if our application have 20, 30 or maybe 100 containers, here an ORCHESTRATOR comes into the picture, and this is exactly what Kubernetes do.
Kuberenetes
Kubernetes, also known as k8s, is an open-source container orchestration platform used for deploying, scaling, and managing containerized applications. It provides a way to automate the deployment, scaling, and management of containerized applications, allowing developers to focus on writing code without worrying about the underlying infrastructure. Kubernetes provides a platform for running and managing containers, with features like load balancing, automatic scaling, self-healing, and rolling updates, making it easier for developers to deploy and manage their applications. It is commonly used in cloud-native environments, allowing applications to be deployed and managed in a consistent way across multiple cloud providers and on-premises environments.
Environment
For this tutorial we will use minikube. Minikube is an open-source tool that enables us to set up a single-node Kubernetes cluster on our local machine.
The installation is pretty simple, all you have to do is to follow the steps from the official doc in this link : https://minikube.sigs.k8s.io/docs/start/
Once the installation is done, run minikube start
and here you go, now you have a local Kubernetes cluster on your machine.
Workload
As defined in the official Kubernetes documentation (https://kubernetes.io/docs/home/), a workload is an application running on Kubernetes. Whether your workload is a single component or several components that work together, in Kubernetes, you run it inside a set of pods. In Kubernetes, a Pod represents a set of running containers on your cluster.
In our case, we will create manifests for the following Kubernetes objects: two Deployments and two Services. The Deployments will roll out ReplicaSets, which in turn will create Pods containing the Application and Database containers. One Service will expose the database port, allowing the application to communicate with it. The second Service will expose the application endpoints we will call.
Manifests
One of the core principles of Kubernetes is the concept of desired state, where you define the state you want your application to be in, and Kubernetes takes care of the rest.
In Kubernetes, the desired state is defined using a declarative approach, where you specify the desired configuration of your application in a YAML or JSON file called a manifest. This manifest includes information about the application's containers, networking, volumes, and other resources required to run the application.
When you deploys the manifest to Kubernetes, Kubernetes compares the desired state to the actual state of the cluster and takes the necessary steps to reconcile any differences. This process is known as the reconciliation loop and is the key to Kubernetes' ability to maintain the desired state of an application.
The reconciliation loop consists of four steps. The first step is to observe the current state of the cluster and compare it to the desired state specified in the manifest. In the second step, Kubernetes determines what actions need to be taken to bring the actual state in line with the desired state. These actions may include creating new resources, updating existing resources, or deleting resources that are no longer needed.
Once Kubernetes has determined the necessary actions, it takes the third step of executing those actions to make the required changes to the cluster. Finally, Kubernetes waits for the changes to take effect and repeats the process to ensure that the actual state of the cluster matches the desired state.
In summary, Kubernetes takes the desired state defined in a manifest file and uses a reconciliation loop to compare it to the actual state of the cluster. It then takes the necessary steps to bring the actual state in line with the desired state and repeats the process to ensure that the application remains in the desired state.
Four our solution and for a sake of simplicity we are going to create two manifest files, application.yaml for the application's Deployment and Service objects. And database.yaml for the Database's Deployment and Service objects.
Application.yaml manifest
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app: players
name: players
spec:
replicas: 1
selector:
matchLabels:
app: players
strategy: {}
template:
metadata:
labels:
app: players
spec:
containers:
- image: malikoo/players:2.0
name: players
resources: {}
env:
- name: POSTGRES_URL
value: playersdb:5432/testdb
initContainers:
- name: init-postgres
image: postgres:9.6.5
command: ['sh', '-c', 'until pg_isready -h playersdb -p 5432; do echo waiting for database; sleep 2; done;']
apiVersion: v1
kind: Service
metadata:
labels:
app: players
name: players
spec:
ports:
- name: 8080-8080
port: 8080
protocol: TCP
targetPort: 8080
selector:
app: players
type: ClusterIP
status:
loadBalancer: {}
This manifest defines a Deployment in Kubernetes that creates a single replica of a container based on the Docker image malikoo/players:2.0. The container is named "players".
this manifest creates also a Service that exposes the players pods on port 8080 within the cluster.
Note that we have an init container; this section is used to specify any containers that should run before the main container starts. Our goal here is to wait until the database is up and running before running our Spring Boot application.
Database.yaml manifest
apiVersion: apps/v1
kind: Deployment
metadata:
creationTimestamp: null
labels:
app: playersdb
name: playersdb
spec:
replicas: 1
selector:
matchLabels:
app: playersdb
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
app: playersdb
spec:
containers:
- image: postgres
name: postgres
resources: {}
env:
- name: POSTGRES_PASSWORD
value: postgres
- name: POSTGRES_DB
value: testdb
status: {}
---
apiVersion: v1
kind: Service
metadata:
labels:
app: playersdb
name: playersdb
spec:
ports:
- name: 5432-5432
port: 5432
protocol: TCP
targetPort: 5432
selector:
app: playersdb
type: ClusterIP
status:
loadBalancer: {}
this manifest creates a Postgres database deployment and a Service that exposes the database within the Kubernetes cluster on the port 5432.
Kubectl Apply
Minikube comes with a built-in kubectl installation. Kubectl is a command-line interface (CLI) tool used to interact with Kubernetes clusters. It allows us to deploy, inspect, and manage applications running on Kubernetes, as well as access and manage cluster resources such as nodes, pods, services, and configurations.
To deploy our solution all we have to do is run those two commands :
kubectl apply -f application.yaml
kubectl apply -f database.yaml
Now we can use kubectl to see the newly created k8s objects :
>kubectl get deployment
NAME READY UP-TO-DATE AVAILABLE AGE
players 1/1 1 1 11d
playersdb 1/1 1 1 11d
>Kubectl get pods
NAME READY STATUS RESTARTS AGE
players-69cc7f875d-4rnph 1/1 Running 0 12m
playersdb-668967cfb5-859hn 1/1 Running 2 11d
>kubectl get services
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP
players ClusterIP 10.99.208.51 <none> 8080/TCP
playersdb ClusterIP 10.100.152.81 <none> 5432/TCP
Great, now our application is up and running, we can test it to confirm that it's working properly.
Before to do test we need to make a port forwarding to be able to access the application, to do this run this command :
>kubectl port-forward service/players 8080:8080
Forwarding from 0.0.0.0:8080 -> 8080
Let's do our test :
>curl -i --request GET 'localhost:8080/api/player/v1/test'
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 14
Date: Sun, 09 Apr 2023 11:51:42 GMT
This is a test
Congratulations! You have successfully deployed your application on Kubernetes. You can now access your application using the service endpoint the same way we did previously in this tutorial after using docker compose.
So, to wrap up, we can summarize the different steps to go from an application to Kubernetes as follows :
Thank you for following this tutorial, and I hope it was helpful. If you have any questions or feedback, please don't hesitate to reach out.
Top comments (0)