Week 8 Microservices Tutorial

In this week’s lab, we will be learning about using Docker containers to develop microservices

Prerequisites

  1. Docker - for packaging microservices
  2. Maven mvn build system - an alternative to the gradle build system for java

Before you begin, Docker needs to be installed. For installation instructions, refer to the official Docker documentation You will build and run the microservices in Docker containers.

Introduction

This tutorial was inspired by Open Liberty’s guide

From development to production, and across your DevOps environments, you can deploy your microservices in a lightweight and portable manner by using containers. You can run a container from a container image. Each container image is a package of what you need to run your microservice or application, from the code to its dependencies and configuration. If you’re new to the development of applications in containers, you might want to start with the Using Docker containers to develop microservices guide before you work through this guide.

You’ll learn how to build container images and run containers using Docker for your microservices. You’ll learn about the Open Liberty container images and how to use them for your containerized applications. You’ll construct Dockerfile files, create Docker images by using the docker build command, and run the image as Docker containers by using docker run command.

The two microservices that you’ll be working with are called system and inventory. The system microservice returns the JVM system properties of the running container. The inventory microservice adds the properties from the system microservice to the inventory. This guide demonstrates how both microservices can run and communicate with each other in different Docker containers.

Getting started

Setting up the repository#

Clone the git repository for the example project


git clone https://gitlab.cecs.anu.edu.au/comp2120/2023/guide-containerize
cd guide-containerize

The start directory contains the starting project that you will build upon. The finish directory contains the finished project that you will build.

Before you begin, make sure you have all the necessary prerequisites.

Packaging your microservices#

To begin, run the following command to navigate to the start directory:

cd start

You can find the starting Java project in the start directory. This project is a multi-module Maven project that is made up of the system and inventory microservices. Each microservice is located in its own corresponding directory, system and inventory.

To try out the microservices by using Maven, run the following Maven goal to build the system microservice and run it inside Open Liberty:

mvn -pl system liberty:run

Open another command-line session and run the following Maven goal to build the inventory microservice and run it inside Open Liberty:

mvn -pl inventory liberty:run

After you see the following message in both command-line sessions, both of your services are ready:

To access the inventory service, which displays the current contents of the inventory, see http://localhost:9081/inventory/systems

To access the system service, which shows the system properties of the running JVM, see http://localhost:9081/system/properties

You can add the system properties of your localhost to the inventory service at http://localhost:9081/inventory/systems/localhost.

After you are finished checking out the microservices, stop the Liberty instances by pressing CTRL+C in the command-line sessions where you ran the system and inventory services. Alternatively, you can run the liberty:stop goal in another command-line session:

mvn -pl system liberty:stop
mvn -pl inventory liberty:stop

To package your microservices, run the Maven package goal to build the application .war files from the start directory so that the .war files are in the system/target and inventory/target directories.

mvn package

To learn more about RESTful web services and how to build them, see Creating a RESTful web service for details about how to build the system service. The inventory service is built in a similar way.

Building your Docker images#

A Docker image is a binary file. It is made up of multiple layers and is used to run code in a Docker container. Images are built from instructions in Dockerfiles to create a containerized version of the application.

A Dockerfile is a collection of instructions for building a Docker image that can then be run as a container. As each instruction is run in a Dockerfile, a new Docker layer is created. These layers, which are known as intermediate images, are created when a change is made to your Docker image.

Every Dockerfile begins with a parent or base image over which various commands are run. For example, you can start your image from scratch and run commands that download and install a Java runtime, or you can start from an image that already contains a Java installation.

Learn more about Docker on the official Docker page.

Creating your Dockerfiles#

You will be creating two Docker images to run the inventory service and system service. The first step is to create Dockerfiles for both services.

In this guide, you’re using an official image from the IBM Container Registry (ICR), icr.io/appcafe/open-liberty:full-java11-openj9-ubi, as your parent image. This image is tagged with the word full, meaning it includes all Liberty features. full images are NOT recommended for development only because they significantly expand the image size with features that are not required by the application.

To minimize your image footprint in production, you can use one of the kernel-slim images, such as icr.io/appcafe/open-liberty:kernel-slim-java11-openj9-ubi. This image installs the basic Liberty runtime. You can then add all the necessary features for your application with the usage pattern that is detailed in the Open Liberty https://openliberty.io/docs/latest/container-images.html#build[container image documentation]. To use the default image that comes with the Open Liberty runtime, define the FROM instruction as FROM icr.io/appcafe/open-liberty. You can find all official images on the Open Liberty container image repository.

Create the Dockerfile for the inventory service.

The FROM instruction initializes a new build stage, which indicates the parent image of the built image. If you don’t need a parent image, then you can use FROM scratch, which makes your image a base image.

It is also recommended to label your Docker images with the LABEL command, as the label information can help you manage your images. For more information, see Best practices for writing Dockerfiles.

The COPY instructions are structured as COPY [--chown=<user>:<group>] <source> <destination>. They copy local files into the specified destination within your Docker image. In this case, the inventory Liberty configuration files that are located at src/main/liberty/config are copied to the /config/ destination directory. The inventory application WAR file inventory.war, which was created from running mvn package, is copied to the /config/apps destination directory.

The COPY instructions use the 1001 user ID and 0 group because the icr.io/appcafe/open-liberty:full-java11-openj9-ubi image runs by default with the USER 1001 (non-root) user for security purposes. Otherwise, the files and directories that are copied over are owned by the root user.

Place the RUN configure.sh command at the end to get a pre-warmed Docker image. It improves the startup time of running your Docker container.

The Dockerfile for the system service follows the same instructions as the inventory service, except that some labels are updated, and the system.war archive is copied into /config/apps.

Create the Dockerfile for the system service. system/Dockerfile

Building your Docker image#

Now that your microservices are packaged and you have written your Dockerfiles, you will build your Docker images by using the docker build command.

Run the following commands to build container images for your application:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.

The -t flag in the docker build command allows the Docker image to be labeled (tagged) in the name[:tag] format. The tag for an image describes the specific image version. If the optional [:tag] tag is not specified, the latest tag is created by default.

To verify that the images are built, run the docker images command to list all local Docker images:

docker images

Or, run the docker images command with --filter option to list your images:

docker images -f "label=org.opencontainers.image.authors=Your Name"

Your inventory and system images appear in the list of all Docker images:

REPOSITORY    TAG             IMAGE ID        CREATED          SIZE
inventory     1.0-SNAPSHOT    08fef024e986    4 minutes ago    1GB
system        1.0-SNAPSHOT    1dff6d0b4f31    5 minutes ago    977MB

Running your microservices in Docker containers#

Now that your two images are built, you will run your microservices in Docker containers:

docker run -d --name system -p 9080:9080 system:1.0-SNAPSHOT
docker run -d --name inventory -p 9081:9081 inventory:1.0-SNAPSHOT

The following table describes the flags in these commands:

Flag Description
-d Runs the container in the background.
–name Specifies a name for the container.
-p Maps the host ports to the container ports. For example: -p <HOST_PORT>:<CONTAINER_PORT>

Next, run the docker ps command to verify that your containers are started:

docker ps

Make sure that your containers are running and show Up as their status:

CONTAINER ID    IMAGE                   COMMAND                  CREATED          STATUS          PORTS                                        NAMES
2b584282e0f5    inventory:1.0-SNAPSHOT  "/opt/ol/helpers/run…"   2 seconds ago    Up 1 second     9080/tcp, 9443/tcp, 0.0.0.0:9081->9081/tcp   inventory
99a98313705f    system:1.0-SNAPSHOT     "/opt/ol/helpers/run…"   3 seconds ago    Up 2 seconds    0.0.0.0:9080->9080/tcp, 9443/tcp             system

If a problem occurs and your containers exit prematurely, the containers don’t appear in the container list that the docker ps command displays. Instead, your containers appear with an Exited status when they run the docker ps -a command. Run the docker logs system and docker logs inventory commands to view the container logs for any potential problems. Run the docker stats system and docker stats inventory commands to display a live stream of usage statistics for your containers. You can also double-check that your Dockerfiles are correct. When you find the cause of the issues, remove the faulty containers with the docker rm system and docker rm inventory commands. Rebuild your images, and start the containers again.

To access the application, go to the http://localhost:9081/inventory/systems URL. An empty list is expected because no system properties are stored in the inventory yet.

Next, retrieve the system container’s IP address by running the following:


docker inspect -f "{{.NetworkSettings.IPAddress }}" system


The command returns the system container IP address:

172.17.0.2

In this case, the IP address for the system service is 172.17.0.2. Take note of this IP address to construct the URL to view the system properties.

Go to the \http://localhost:9081/inventory/systems/pass:c[[system-ip-address]] URL by replacing [system-ip-address] with the IP address that you obtained earlier. You see a result in JSON format with the system properties of your local JVM. When you go to this URL, these system properties are automatically stored in the inventory. Go back to the http://localhost:9081/inventory/systems URL and you see a new entry for [system-ip-address].

Externalizing Liberty’s configuration#

As mentioned at the beginning of this guide, one of the advantages of using containers is that they are portable and can be moved and deployed efficiently across all of your DevOps environments. Configuration often changes across different environments, and by externalizing your Liberty’s configuration, you can simplify the development process.

Imagine a scenario where you are developing an Open Liberty application on port 9081 but to deploy it to production, it must be available on port 9091. To manage this scenario, you can keep two different versions of the server.xml file; one for production and one for development. However, trying to maintain two different versions of a file might lead to mistakes. A better solution would be to externalize the configuration of the port number and use the value of an environment variable that is stored in each environment.

In this example, you will use an environment variable to externally configure the HTTP port number of the inventory service.

In the inventory/server.xml file, the default.http.port variable is declared and is used in the httpEndpoint element to define the service endpoint. The default value of the default.http.port variable is 9081. However, this value is only used if no other value is specified. You can replace this value in the container by using the -e flag for the podman run command.

Run the following commands to stop and remove the inventory container and rerun it with the default.http.port environment variable set:

docker stop inventory
docker rm inventory
docker run -d --name inventory -e default.http.port=9091 -p 9091:9091 inventory:1.0-SNAPSHOT

The -e flag can be used to create and set the values of environment variables in a Docker container. In this case, you are setting the default.http.port environment variable to 9091 for the inventory container.

Now, when the service is starting up, Open Liberty finds the default.http.port environment variable and uses it to set the value of the default.http.port variable to be used in the HTTP endpoint.

The inventory service is now available on the new port number that you specified. You can see the contents of the inventory at the http://localhost:9091/inventory/systems URL. You can add your local system properties at \http://localhost:9091/inventory/systems/pass:c[[system-ip-address]] by replacing [system-ip-address] with the IP address that you obtained in the previous section. The system service remains unchanged and is available at the http://localhost:9080/system/properties URL.

You can externalize the configuration of more than just the port numbers. To learn more about Open Liberty configuration, check out the Server Configuration Overview docs.

Optimizing the image size#

As mentioned previously, the parent image that is used in each Dockerfile contains the full tag, which includes all of the Liberty features. This parent image with the full tag is recommended for development, but while deploying to production it is recommended to use a parent image with the kernel-slim tag. The kernel-slim tag provides a bare minimum Liberty runtime with the ability to add the features required by the application.

Replace the Dockerfile for the inventory service. inventory/Dockerfile

Replace the parent image with icr.io/appcafe/open-liberty:kernel-slim-java11-openj9-ubi at the top of your Dockerfile. This image contains the kernel-slim tag that is recommended when deploying to production.

Place RUN features.sh command after the COPY command that copies the local /config/ directory into the Docker image. The features.sh script adds the Liberty features that your application is required to operate.

Ensure that you repeat these instructions for the system service.

Replace the Dockerfile for the system service.

system/Dockerfile

Continue by running the following commands to stop and remove your current Docker containers that are using the full parent image:

docker stop inventory system
docker rm inventory system

Next, build your new Docker images with the kernel-slim parent image:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.

Verify that the images have been built by executing the following command to list all the local Docker images:

docker images

Notice that the images for the inventory and system services now have a reduced image size.

REPOSITORY      TAG             IMAGE ID        CREATED         SIZE
inventory       1.0-SNAPSHOT	d5a3d1b2c20e    4 minutes ago	682MB
system          1.0-SNAPSHOT	6346cf87eae0	5 minutes ago	694MB

After confirming that the images have been built, run the following commands to start the Docker containers:

docker run -d --name system -p 9080:9080 system:1.0-SNAPSHOT
docker run -d --name inventory -p 9081:9081 inventory:1.0-SNAPSHOT

Once your Docker containers are running, run the following command to see the list of required features installed by features.sh:

docker exec -it inventory /opt/ol/wlp/bin/productInfo featureInfo

Your list of Liberty features should be similar to the following:

jndi-1.0
servlet-5.0
cdi-3.0
concurrent-2.0
jsonb-2.0
jsonp-2.0
mpConfig-3.0
restfulWS-3.0
restfulWSClient-3.0

To ensure that your containers are working properly, try accessing the system service to show the system properties of the running JVM. See http://localhost:9080/system/properties[http://localhost:9080/system/properties]

Next, replace [system-ip-address] with the IP address that you obtained earlier and add your localhost system properties to the inventory service by visiting: \http://localhost:9081/inventory/systems/pass:c[[system-ip-address]]

Then, verify the addition of your localhost system properties to your inventory service. See http://localhost:9081/inventory/systems

Testing the microservices#

You can test your microservices manually by hitting the endpoints or with automated tests that check your running Docker containers.


Create the SystemEndpointIT class. system/src/test/java/it/io/openliberty/guides/system/SystemEndpointIT.java

The testGetProperties() method checks for a 200 response code from the system service endpoint.

Create the InventoryEndpointIT class. inventory/src/test/java/it/io/openliberty/guides/inventory/InventoryEndpointIT.java

  • The testEmptyInventory() method checks that the inventory service has a total of 0 systems before anything is added to it.
  • The testHostRegistration() method checks that the system service was added to inventory properly.
  • The testSystemPropertiesMatch() checks that the system properties match what was added into the inventory service.
  • The testUnknownHost() method checks that an error is raised if an unknown host name is being added into the inventory service.
  • The systemServiceIp variable has the same value as the IP address that you retrieved in the previous section when you manually added the system service into the inventory service. This value of the IP address is passed in when you run the tests.

Activity 3: Running the tests#

Run the Maven package goal to compile the test classes. Run the Maven failsafe goal to test the services that are running in the Docker containers by replacing the [system-ip-address] with the IP address that you determined previously.

mvn package
mvn failsafe:integration-test -Dsystem.ip=[system-ip-address] -Dinventory.http.port=9081 -Dsystem.http.port=9080

If the tests pass, you see output similar to the following example:

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemEndpointIT
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.653 s - in it.io.openliberty.guides.system.SystemEndpointIT

Results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.inventory.InventoryEndpointIT
Tests run: 4, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.935 s - in it.io.openliberty.guides.inventory.InventoryEndpointIT

Results:

Tests run: 4, Failures: 0, Errors: 0, Skipped: 0

When you are finished with the services, run the following commands to stop and remove your containers:

docker stop inventory system
docker rm inventory system

Great work! You have just built Docker images and run two microservices on Open Liberty in containers.

bars search times