JVM Advent

The JVM Programming Advent Calendar

Microservices Architecture with Java and Docker

Developing and running a Microservices Architecture seems to be a must have, now days. Everybody talking about micro services, how to migrate to micro services or what are the best practice to run them.

Listening at those who are not using micro services, it seems that it’s a weird world, where everything works magically with no effort for developers or DevOps. Well, everybody knows that DevOps are wizards, and that explains everything, isn’t it?

While there is no magic about micro services to let you think that reality is not what it seems, I can tell you that Wizards exist! I’ll get back to this topic later in this series.

If you know very well Java, and you’re reading this article while waiting for your partner to finish his/her shopping, you might skip next section (Java) to go straight to Microservices, but if you’re waiting for her to finish to try that cool dress for Christmas, man you have all the time to read every section and get pizza delivered 😉

Java

When in 1991 James Gosling started designing Java it was for interactive televisions, but turned to be too advanced for that time. Twenty-six years later, Java is almost everywhere and only relatively recently it found its way to small devices.

Some might say that it’s still far from reaching its five primary goals [1], but three months ago was released version 9 and it’s still one of the most popular programming languages in use, particularly for client-server web applications, with reported over 9 million developers.

The community behind Java largely contributed to the success of Java, providing libraries and feedback, and that helped making Java appropriate to develop microservices.

Microservices architecture

When we’re talking of Microservices, we’re actually referring to the Microservices Architecture. Even if there is no standard/formal definition of Microservices, there are certain common characteristics that allow this type of architecture to automated deployment and decentralizing control and data via a multitude of applications that communicate through a well-defined protocol.

To understand Microservices it’s useful to compare that to Monoliths. Enterprise Application are often built according to the MVC pattern, thus the User Interface (UI) accesses data via a server-side application.

Monoliths

Strictly speaking, a Monolith is not bad in itself; I know example of successful Monolithic applications, as well as awful implementation where it’s literally everything in the same application (forget about MVC and Separation of Concern). However a Monolith application requires more resources in terms of hardware and human work, and an increasing number of people is getting frustrated by Monolithic application. Change cycles requires the entire monolith to be rebuilt, retested and redeployed, and the upgrading an entire server farm easily takes months (and increased frustration of every part involved) and scaling requires scaling of the entire application.

A Microservices architecture focuses on developing and deploying small modular services, where each service deals with a specific task in a very efficient way. Change cycles can be very short and you can easily complete many per day. When it gets to scaling, you just scale those parts that require more resources.

There is no standard protocol nor a mandatory programming language required to build Microservices, but many implementations are based on HTTP/REST and using JSON to exchange data. Personally I prefer to support both JSON and XML, because that gives me a lot of flexibility,  and covers a wider range of scenarios. And if you can get both for free, the only reason not to do so is if the specification says so. We’re going to produce results in both formats.

Having a Microservices architecture you can deploy multiple instances of your service on the same host (that can be either physical or a virtual machine). Starting a new service usually requires few seconds, so whenever you need more from that service (whatever means more for that functionality) you just start a new service or multiple instances of that service, and et voilat, you’re done. One issue that you could raise, is that running multiple instances of the same application on the same host, all of them will be running in the same JVM heap, thus there will be no isolation. And you’re absolutely right. In a minute we’ll see why this is not an issue for us.

Before that I want to add another important point about Microservices. Building Microservices we try to reduce their size to the bare minimum. I’ve seen Microservices bigger than 3GB which doesn’t fit my definition of micro. In one of those cases I managed to reduce to about half of the original size.

Docker

Docker is a software technology providing an abstraction layer on top of the Operating System using native resources, and do achieve, users that use Docker container images (usually referred as just images) and containers. The following is the official description of images and containers from Docker:  “A container image is a lightweight, stand-alone, executable package of a piece of software that includes everything needed to run it: code, runtime, system tools, system libraries, settings.

If you ever built a new physical or virtual machine, you know that the first thing to do is to install the Operating System and to do some configuration, as required. Well, the image is nothing else than those instructions in a text file named Dockerfile.

Hello, World with Docker

Let’s see how looks like a classic example.

HelloWorld.java

package com.daftano.examples.commons;

public class helloworld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");
    }
}

Given the following Dockerfile and running the command

docker build helloworld .

Dockerfile

FROM daftano:java

LABEL maintainer-twitter="@daftano"

ADD build/helloworld-1.0.0.jar /opt/helloworld.jar

ENTRYPOINT java -jar /opt/helloworld.jar

Docker will build a new image named helloworld.

To run our brand new image we’ll just need to run this command:

docker run helloworld

And on the console you’ll see

Hello, World!

Yes, this is indeed a quite complicate way to run a jar file, but has still some advantages. You can share this image with anybody: they do not need to have java on their system and you’ll be sure that it will always run the same, regardless of the environment, and it will be isolated from its surroundings.

Let’s analyze the Dockerfile in details:

  • A valid Dockerfile must start with a FROM instruction that sets the Base Image for subsequent instructions. The image can be any valid image.
  • The LABEL instruction adds metadata to an image and the key maintainer is a special one that sets the author of that image.
  • The ADD instruction copies the jar that we built, into the built image.
  • ENTRYPOINT configures the command that should be executed when the container is run.

There are 18 possible instructions currently available at https://docs.docker.com/engine/reference/builder, and there is another one commonly used that I didn’t use in this example, on purpose. Many implementations are based on HTTP/REST and the service needs to listen at one or more ports and to informs Docker on what ports the container should listen at runtime we’ll use the instruction EXPOSE.

In the examples of this series I used a relaxed way to build images, to keep them cleaner. But in production and when you want to share your images you’ll have to adopt a more strict approach. I’m not digging  into all the details, but the following is what you want to take home.

When we build an image, Docker generates a unique ID and we usually tag that image with a meaningful name (named repository name) such as daftano/java. What follows the colon (“: “) is the version number (in our case 1.8 – if not specified, the default version is latest). The repository name must be at least one lowercase optionally separated by periods, dashes or underscores; it must in fact match this regular expression: [a-z0-9]+(?:[._-][a-z0-9]+)*

Java Image

There is a large choice of Docker images for Java and you might be asking why I decided to build my own. The primary reason for that is that none of them fitted my needs. I found very easy to build a new custom image that I forgot the number of the images that I built, but if there is something already available that I could reuse, why don’t? You know, there are many benefits in reusing, one of them is that if there is an error is likely to be discovered (and maybe fixed) before it could impact me, or I might be able to fix it and share back to the author(s)/community.

Unfortunately the Technical Leader of one of the application I was migrating to Microservices believes that he needs not to write any documentation or specification as reading the source code is all that you need. That lead their requirements to provide an image strictly based on Ubuntu 12.04 with tons of packages installed (including all for Xorg). I also had to use the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files, which are not available with the default JDK/JRE binaries and pose some restrictions to the users. There are many things that you should check if you want to distribute an image based on Oracle Java to stay complaint to the terms of the agreement subscribed with Oracle when downloading the JDK/JRE. And we have the option to use the OpenJDK which is released under the GNU General Public License, version 2, with the Classpath Exception.

Another reason why you might want to use OpenJDK is that the image based on Alpine is 101 MB, while including the Oracle JDK adds 399 MB to your image. It was funny to me to discover that my Java image is 101 MB lighter than the current openjdk:latest. I could make my mage lighter removing some files that I don’t need, but doing so I would breach the terms of the agreement with Oracle if I would share that image with anybody else. This is not the place to talk about that in deep but if there is enough interest in the subject, I might write an article (let me know by either commenting on this article or via twitter).

For the purpose of this series I’m using the Java base image available on my GitHub account at https://github.com/daftano/docker-images/blob/java/Dockerfile but you do not need that image to run the examples, as long as your Java image contains Java 8 or 9. If you decide to build my image, please note that you need to either download the JCE Policy available at http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html and copy the jar files in the directory jce-policy/8.

Java Dockerfile

1.  FROM daftano/ubuntu:16.04
2.  LABEL maintainer="Davide Fiorentino lo Regio"
3.  LABEL maintainer-twitter"@daftano"
4.
5.  ARG JAVA_VERSION=8
6.  ENV JAVA_HOME=/usr/lib/jvm/java-${JAVA_VERSION}-oracle
7.
8.  ENTRYPOINT ["/usr/bin/java"]
9.  CMD ["-version"]
10.
11. RUN \
12.   echo oracle-java${JAVA_VERSION}-installer shared/accepted-oracle-license-v1-1 select true | debconf-set-selections \
13.   && add-apt-repository -y ppa:webupd8team/java \
14.   && apt-get update \
15.   && apt-get install -qqy --no-install-recommends oracle-java$ {JAVA_VERSION}-installer oracle-java${JAVA_VERSION}-set-default \
16.   && rm -rf /var/lib/apt/lists/* \
17.   && rm -rf /var/cache/oracle-jdk${JAVA_VERSION}-installer
18.
19. ADD ["./jce-policy/${JAVA_VERSION}/*.jar", "/usr/lib/jvm/java-${JAVA_VERSION}-oracle/jre/lib/security/"]

I wanted a flexible image for major Java versions without having to maintain multiple Dockerfile almost identical with the only exception for the Java version. And on line 5 I used a build argument with the default to version 8. That means that if you run the usual build command, Docker will build the image using Java 8. But if you want to build it for Java 7, you’ll have to provide the argument to the build command as follow: –build-arg JAVA_VERSION=7.

This image is not meant to be run alone; but as a base layer for another image, I decided to output the version of java would you run it on its own (lines 8 and 9).

When you install Java on Ubuntu, the installer will ask you to confirm your acceptance of their Licence Terms, and won’t ask you again for the same version, but when you’re building an image you cannot interact with the installers and your build will fail. As you’re already using Java and accepted those terms, lines 12 mark your acceptance and lines 13 to 15 install Java for you and set the default JVM.

To keep the size of my image the smallest possible, lines 16 and 17 remove cache information and the Java installer.

Lines 19 adds the aforementioned JCE Policy files. If you don’t need them, you could safely remove that line.

The build should not take too long to complete, and with that you’ll have a working Java image that you can use for your projects as well as for next articles of this series that will be published tomorrow.

 


Notes

[1] Java primary goals:

  • It must be “simple, object-oriented, and familiar”
  • It must be “robust and secure”
  • It must be “architecture-neutral and portable”
  • It must execute with “high performance”
  • It must be “interpreted, threaded, and dynamic”

Author: Davide Fiorentino lo Regio

Known to investigate, prototype and deliver new and innovative solutions previously considered impossible, his life philosophy is “Make yourself better to make the best of everything and achieve the impossible.”
Engaged in multiple projects for large organizations and consultanting for the United Nations, he won the “Premio Colosseo” in Rome, Italy

Next Post

Previous Post

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

© 2024 JVM Advent | Powered by steinhauer.software Logosteinhauer.software

Theme by Anders Norén