Spring Boot Microservices — Containerization
By: Lal Verma
Tags: Virtualization, Software Engineering, Docker, Spring Boot, Containers

Spring Boot Microservices — Containerization

In this article, we will understand the advantages of containerizing Spring Boot Microservices. We will also do sample implementation, based on Docker and Spring Boot. This is the 7th part of our series — Spring Boot Microservices — Learning through examples

Photo by Sebastian Herrmann on Unsplash

A container is a standard unit of software that packages up code and all its dependencies so the application runs quickly and reliably from one computing environment to another. A Docker container image is a lightweight, standalone, executable package of software that includes everything needed to run an application: code, runtime, system tools, system libraries and settings.

This is very similar to the package we build for Spring Boot Microservices. Our Spring Boot applications are packaged as Jar files which assume that the runtime environment has Java installed. Containers are packaged in such a way that they can run directly at operating system level. This helps to isolate the bundled softwares from its environment and ensure that they work uniformly. We will understand this topic in more detail with the help of following sections —

The background — This part will discuss the evolution of deployment patterns and technologies, resulting in the concept of Containers. I have already covered similar content in the article on Kubernetes. If you have already gone through this, you can skip this part.

How Containers Work? — This part will discuss Containers in more detail including its core components and functions.

Sample Implementation — We will do sample implementation based on Docker along with our Spring Boot Microservices. We will create and run containers for our already built services.

The background

Age of Physical Machines

In the early era, applications were deployed on the physical machines. Deploying one application per machine was very costly in terms of resource utilization. On the other hand if we try to optimize this, by deploying multiple applications on the same machine, it was not possible to define the resource boundaries. Failure in one application could have propagated to others systems.

To understand this, lets say our physical machine is hosting two applications — E-commerce Application and the Inventory Management System and if the latter is eating up all the memory, most often our e-commerce system will stop working as well. Virtualization brought the much needed reform!

Age of Virtual Machines

The technology allowed us to run multiple Virtual Machines (VM) on the single physical machine with the clear separation of resources, platform, libraries and security.

This facilitated better utilization of resources. Enterprises invested in the shared pool of Virtual Machines, which provided more flexibility and scalability to the software systems.

If the holidays season require our e-commerce system to work with two machines, we can get the additional machine(virtual) much faster. When the holiday season is over, I can return back the additional Virtual machine, releasing the resources back to the shared pool.

The technology worked great, but soon the enterprises realized this is relatively heavier. It required the whole operating system to be installed, on each of the virtual machines. Additionally, though the procurement time was greatly reduced, but the pace did lag behind in the age of Agile Development and Microservices.

Agile Development & Microservices

At the time when we were moving from physical to virtual machines, the software development was going through the transformation as well. The development model changed with the introduction of Agile methodology. We started updating and deploying the applications much faster.

It became difficult for the monoliths to survive the Agile world. The fast pace development set the foundation for Microservices. Our e-commerce system was no more a monolith now. It got divided into multiple microservices — Product Catalog, Inventory, Shopping Cart, Order Management and many others. With the Microservices, the traditional deployment model did not work, and it posed new challenges.

The number of microservices, each with a different set of technologies and dependencies, needed a different environment. The earlier adopted options included “one service one host” or “one service one vm”. Again, this was a costly option, and on the other hand deploying “multiple services per host or vm” created similar challenges as in the case of applications.

Microservices were broken down in smaller services so that they can evolve independently. Each of service needs were unique, in terms of technologies and resources, and it needed its own isolated space. Containers provided the lightweight virtualization.

How Containers Work?

Lightweight Containers

Containerization provided the means to partition the underline system (physical or virtual) into the virtual containers. They are lightweight compared to the virtual machines because they don’t need the extra load of a hypervisor, but run directly within the host machine’s kernel.

Containers share the operating system but they can have their own filesystem, memory and CPU. This facilitate flexible and faster deployments, which in turn helps in achieving continuous development and deployment practices.

Containers solved the fundamental problem of resource sharing and isolation with the much efficient mechanism.

Docker

There are many container technologies available in the market but Docker is a clear winner. We will be using this for our sample implementation. Before jumping to the exercise, lets get familiar with the core components of this technology.

Containers are realized with the help of images which are nothing but the build and run instructions for a software application. We need to bundle all the dependencies as part of the container image. In case of Spring Boot Microservices this will typically contain the Java environment and the executable Jar. You will soon see the running example as part of the Sample Implementation.

courtesy: docker.com

Docker CLI (Command Line Interface) provide the options to create and run the container images. But the real work is done by Docker Engine which keeps running as a long-running daemon process. Docker provide the APIs which the programs can use to talk and instruct the Docker daemon.

Images typically reside in a central repository called Docker Registry. Any user or group interested in running the container image can pull it from here.

Benefits

Containerization provides many benefits in the development of software systems —

Isolation and security allow us to run multiple containers at the same time on a given host. For instance, we can deploy one or more instances of Product Catalog Service and Inventory Service on the same machine.

The container becomes the unit for distributing the application. This means, we can create container image of Product Catalog Service and give it to other teams for further validation and deployment steps.

Container images are standardized and portable. We can use the same image of Product Catalog Service to deploy in the production environment. Be it local data center or the remote cloud, it works consistently.

Now that we understand container and its benefits, lets dive into the practical world of it.

Sample Implementation

With the sample implementation, we will try to understand how the container technology works with our Spring Boot based Microservices. I have segregated the exercise in easy to follow steps —

Basic Setup

In the sample implementation we will use Docker, which is the most popular container technology. You can visit https://www.docker.com/get-started to get the appropriate docker setup on your machine. If you are on linux you just need a binary, but for Mac or Windows, you need to install Docker Desktop.

We also need Java, Git, Maven as we will be containerizing our Spring Boot Microservices. We already created multiple services during our previous exercises in this series — Learning through Examples. Lets get the microservices code from Github, by running the following command —

This will get the code for all the services. We will be creating the container image for our product catalog service. For this, lets browse to the directory spring-boot/product-catalog . All the commands in subsequent sections have to be executed in the context of this directory.

Creating Container Image

Docker can build images automatically by reading the instructions from a Dockerfile . A Dockerfile is a text document that contains all the commands a user could call on the command line to assemble an image. Using docker build users can create an automated build that executes several command-line instructions in succession. Here is the sample Dockerfile —

FROM adoptopenjdk:11-jre-hotspot as builder

ARG JAR_FILE=target/product_catalog-0.0.1-SNAPSHOT.jar

ADD ${JAR_FILE} app.jar

ENTRYPOINT ["java","-jar","/app.jar"]

In this file, we are giving following instructions

FROM command instructs to use base image as the one available for adoptopenjdk:11-jre-hotspot .

command instructs to use base image as the one available for . ARG command instructs to use JAR_FILE as a variable. Default value specified is target/product_catalog-0.0.1-SNAPSHOT.jar . This refers to the executable jar which will be generated once we run mvn install command

command instructs to use JAR_FILE as a variable. Default value specified is . This refers to the executable jar which will be generated once we run command ADD command instructs to copy the jar file to the image filesystem with the new name as app.jar

command instructs to copy the jar file to the image filesystem with the new name as ENTRYPOINT command allow you to configure a container that will run as an executable. In our case, we are instructing to execute the jar file with the help of java command.

With all the build instructions written in Dockerfile , we will create the container image with the help of docker build command. This will generate the container image and tag it with the name spring-boot-examples/product-catalog

docker build -t spring-boot-examples/product-catalog .

You can check the available image with the docker command docker images . This will list down all the docker images registered locally. One of them should be with the name above. Now that you have the container image ready for Product Catalog Service, you can start the service with docker run command

docker run -p 8080:8080 spring-boot-examples/product-catalog

The service will work the same way as it does through mvn spring-boot:run . We can access the create/update/delete and get apis. Play around with the service to ensure its apis are working correctly. As a sanity check, I can create a test-product-123 with the following post request.

{

"title":"test-product-123",

"desc":"test product 123",

"imagePath":"http://test-image-path",

"unitPrice":10.00

}

and use the get api to view the product details by accessing http://localhost:8080/product/test-product-123 . If both the api work, it means you have successfully created the container image of our Spring Boot Microservice.

We can also create the container image with the help of Spring Boot build plugin, Spotify Maven plugin, Palantir Gradle plugin, and Jib from Google. You can check out the spring guide on this here. The guide also provides suggestions to optimize the image through multi-stage builds.

We touched upon some basic commands in dockerfile to create the image. To make the images more robust and flexible, you should check the full list of commands available here.

Running the Container

We already did a sample run of our container image in the previous section. ENTRYPOINT command helps in defining the executable instructions for the image. In this section, we will focus on running the container with some additional options.

Lets say we want to set the initial memory size for the Product Catalog Service. We can update the dockerfile by adding this option with the ENTRYPOINT command.

FROM adoptopenjdk:11-jre-hotspot as builder

ARG JAR_FILE=target/product_catalog-0.0.1-SNAPSHOT.jar

ADD ${JAR_FILE} app.jar

ENTRYPOINT ["java","-Xmx500m","-jar","/app.jar"]

This will start the service and ensure that the maximum size of the memory allocation for JVM is not exceeded beyond 500 MB. If we want to keep this value flexible, we can refer an environment variable JAVA_OPTS with the sh command as below.

FROM adoptopenjdk:11-jre-hotspot as builder

ARG JAR_FILE=target/product_catalog-0.0.1-SNAPSHOT.jar

ADD ${JAR_FILE} app.jar

ENTRYPOINT ["sh","-c","java ${JAVA_OPTS} -jar /app.jar"]

Once we build the image, we can pass the value of this environment variable at the time of running the container.

docker run -p 8080:8080 -e "JAVA_OPTS=-Xmx500m" spring-boot-examples/product-catalog

You can define the environment variable in dockerfile with the help of ENV instruction, but it will restrict the value at build time only. With -e option, you can define the environment variable in docker run command. We used it to pass arguments to Java process. The approach is bit different if we want to pass the properties in spring context. To pass those values, we must provide placeholders ${0} and {@} as below in the dockerfile

ENTRYPOINT ["sh","-c","java ${JAVA_OPTS} -jar /app.jar ${0} ${@}"]

Once the dockerfile is updated, build the image again, and run the docker container. We can pass the value ${JAVA_OPTS} as part of the run command.

docker run -p 9000:9000 -e "JAVA_OPTS=-Xmx256m" spring-boot-examples/product-catalog --server.port=9000

Docker supports the command in two forms — exec and shell. We already used exec form in our example. Usage of shell form looks like this -

ENTRYPOINT command param1 param2

Docker suggests to use exec as the preferred form. This gives complete control on the process execution unlike the shell form. For instance we can stop the service by pressing CTRL-C in case of exec form which is not possible through shell form.

Publishing Container Image

Container images are like any other system file which can be exchanged across users and groups. We did learn to create the image in the previous section. To reuse the images, we need to publish them to a central repository. Once the image is published, it can be pulled by any authorized user.

Docker Hub is a hosted repository service provided by Docker for finding and sharing container images. Typically enterprises need private repository to push and pull the container images for their services and applications. There are many hosted providers providing this feature including Docker Hub. You can also create the repository in your local data center. Irrespective of where the repository is, lets see how we can publish the image so that its available to the other users and groups in your enterprise.

If we are publishing the image first time, we must save the image by passing its container id. This can be found with the help of docker ps command. Considering our Product Catalog Service , we can use commit command to achieve this.

$ docker commit c16378f943fe product-catalog

Now, push the image to the registry using the image ID. In this example the registry is on host named ecommerce-doc-registry and listening on port 5000 . To do this, tag the image with the host name or IP address, and the port of the registry

$ docker tag product-catalog ecommerce-doc-registry:5000/spring-boot-examples/product-catalog $ docker push ecommerce-doc-registry:5000/spring-boot-examples/product-catalog

Once you get the confirmation, other people or groups such as testing team can use pull command to get the image and use it for starting the service as discussed previously.

docker pull ecommerce-doc-registry:5000/spring-boot-examples/product-catalog

Running Multiple Instances

We successfully created and ran the container image of Product Catalog Service. Its time to see some real benefits of containers. In this section, we will try to run multiple instances of the service. Lets start the two instances of the service as below.

docker run --name instance-1 -p 8081:30001 spring-boot-examples/product-catalog docker run --name instance-1 -p 8082:30001 spring-boot-examples/product-catalog

The docker run command is bit different than we saw above. Here we are specifying names for each of the running containers — instance-1 & instance-2 . Also we are using different host ports for each of these containers. So the instance-1 is running at 8081 port on the machine whereas instance-2 is running at 8082. If you do docker ps , you will see the instances running as below.

You can access each of the instance separately. For instance, you can call http://localhost:8081/product/test-product-123 to get the product details for the product id — test-product-123 . Both the instances are completely isolated. They are using their own runtime environment. You can also check the resource consumption with the help of docker stats command.

Multiple instances of a service demands additional support like Service Discovery, Load Balancing and Container Orchestration. Docker Swarm and Kubernetes provide the required platform to cater these needs.

Similar to the multiple instances, we can also run multiple services on the same machine. It does not matter what the technology platform of other service is. For instance you might develop Inventory service based on Java 14 or you might completely change the technology stack to Node.js. Containers can provide the isolation to each of these services so that all of them can coexist without impacting each other.

Next Steps

Docker is a very wide topic and touches upon multiple aspects of application deployment. The objective of this article is to give you a high level view on containerization. This should be good enough from a developer perspective.If you are into software architecture, you must check out more specific docker topics to develop an holistic approach towards the technology.

You can check Docker Compose which is a tool for defining and running multi-container Docker applications. With Compose, using a single command, you can create and start set of services.

You can check Docker Swarm, which provides many features to build docker cluster and orchestration capabilities. You can also check Kubernetes which is one of the most popular container orchestration framework. I have already covered this topic in detail and can be checked out here — Spring Boot Microservices — Kubernetes-ization.