DEV Community

Malek Ramdani
Malek Ramdani

Posted on • Updated on

When Java meet Docker... Part II kubernetes

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:

Image description

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"]
Enter fullscreen mode Exit fullscreen mode

And run this command :

docker build -t malikoo/players:1.0 .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"}
Enter fullscreen mode Exit fullscreen mode

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"},]

Enter fullscreen mode Exit fullscreen mode

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: {}
Enter fullscreen mode Exit fullscreen mode

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: {}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
kubectl apply -f database.yaml
Enter fullscreen mode Exit fullscreen mode

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 

Enter fullscreen mode Exit fullscreen mode

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 players-69cc7f875d-4rnph 8080:8080
 Forwarding from 0.0.0.0:8080 -> 8080
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 :

Image description

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)