This is a small tutorial of microservice containerization. For demonstrative purposes, the applications used are based on two different technologies: microservice A running on C#/.NET-Core-Framework and microservice B running on Java with Spring-Boot.
The microservice-based system created in this tutorial has a simple demonstrative functionality: It fetches current exchange rate from the server of the European Central Bank every 10 seconds. The application can be of course extended to any purpose.
In the following steps we first create Docker image of each application and then use docker-compose up
to create, boot and run all containers in the virtual network hosted by the message-broker.
For this tutorial you will need a few things:
This demo solution consists of a message-broker and two microservices, one of which connects to the server of the European Central Bank:
To show the interaction between the components, the microservice A continously pushes new requests in the message queue managed by the message-broker. The microservice A is a simple C#/.NET application. In this scenario the microservice A behaves solely as a service consumer.
To get this app running in Visual Studio, make sure that:
- Microsoft.NETCore 3.1 is set the framework;
- Package Newtonsoft.Json,
- Package System.Text.Encodings .Web and
- Package RabbitMQ.Client (6.0.0) are included.
When running the app in the Visual Studio's IDE the execution stops at the attempt to push the first request into the broker's message queue. The reason is simple - the broker is not running yet (which we will fix below). The console should show the following:
(A.0) Demo app initiated
(A.1) FX request USD->EUR prepared, starting loop
(A.2) Initiating request #1
(A.3) Async request task started
(A.4) Request stringified
If you look at the code you notice that this application is not set to connect over the local host, but the message-broker (more details in the sections below).
var factory = new ConnectionFactory(){ HostName = "message-broker" };
The messages are passed by the message-broker in different queues. In this case we need only one queue connecting the microservice A with the microservice B. These messages function as remote procedure calls (RPC). Both services subscribe therefore to the same queue:
private const string QUEUE_NAME = "rpc_queue";
The the remote procedure calls are sent in non-blocking asynchronous messages.
RpcResponse rpcResponse = Task.Run(async () => await GetRpcResult(request)).Result;
A Docker image is a read-only template for a specific application which can be used to create and run an instance in a container.
The elaboration on how to create a Docker image from a .NET-Core application can be found in the Microsoft documentation, but is also summarized below.
Before creating a Docker image the .NET-Core-application must be
- built and
- published.
To build your application you can simply use Visual Studio's IDE (e.g. "Build Solution" in the solution explorer's context menu). This creates .dll-files in the bin-folder.
To publish the application you can run the dotnet publish command in command prompt from the DemoApp-directory:
cd microservice_A_(C#.NET)\DemoApp
dotnet publish -c Release
This creates the directory DemoApp\bin\Release\netcoreapp3.1\publish. These dll-files are needed to create the docker-image. For this tutorial a simple Dockerfile with the following contents can be used:
FROM mcr.microsoft.com/dotnet/core/aspnet:3.1
COPY DemoApp/bin/Release/netcoreapp3.1/publish/ App/
WORKDIR /App
ENTRYPOINT ["dotnet", "DemoApp.dll"]
Note: This file is already prepared for you in the directory microservice_A_(C#.NET).
To create a new docker image, run the following command from the directory microservice_A_(C#.NET)/DemoApp where also the Dockerfile is placed.
docker build -t microservice_a-image -f Dockerfile .
Note: You may need to start your Command Prompt with elevated rights ("run as administrator") in order to execute this command.
If you now display all images with the command below, you should see a new repository called "microservice_a-image".
docker images
The image showing on your list should look something like this:
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
---|---|---|---|---|
microservice_a-image | latest | c9b3efde6514f | 3 hours ago | 209MB |
This image will be used to create a container running the app later.
After the microservice A pushes its request to the message-brokers's queue, this message is forwarded to the microservice B. This microservice then fetches the current exchange rates from the European Central Bank's server and sends the requested exchange rate back to the message-broker. The message-broker then forwards the response from the microservice B to the microservice A.
The microservice B is hence a service consumer in the relation to the European Central Bank's server and a service provider in the relation to the microservice A.
The microservice B is a maven-project. All dependacies are predefined in the pom.xml-file. All you need to do is to import as maven in your IntelliJ.
Note that IntelliJ already comes equiped with functionalities to handle maven-projects. However if you want to call maven commands from the command prompt see maven installation guide.
If you run the microservice B in the IntelliJ's IDE the console should give you the output below. The execution stops at the attempt to connect to the message-broker. The reason is simple - the message-broker is not running yet.
(B.0) Demo app initiated
(B.1) Opening connection to the message broker
In the Java application files you can again see that the microservice B - just as the microservice A - is set to connect the message-broker's host and subscribes to the same message queue:
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("message-broker");
private static final String RPC_QUEUE_NAME = "rpc_queue";
The two microservices would not be able to communicate with each other otherwise.
Before creating a Docker image, the Java-application must be
- built and
- packaged
Note: There are also other alternatives than JAR-packaging that are not addressed in this tutorial.
The packaging method is already defined in the pom.xml-file as JAR. To build and package the microservice B you can simply run the following command in the IntelliJ's terminal or in the command prompt.
mvn clean package
This command creates the file microservice_B-0.0.1-SNAPSHOT.jar in the directory microservice_B_(Java_wSpringBoot)\target.
You can either move the JAR-file to the directory microservice_B_(Java_wSpringBoot) where also the Dockerfile is saved and rename it as referenced in the prepared Dockerfile or change the cofiguration as it fits you.
Note: The JAR-file is has already been created for you in the project files.
The Dockerfile can look as follows (included in the project files). Note that in order to run a Java-based application, the image needs to have a JRE layer for which the openJDK can be used. This application runs on Java 8.
FROM adoptopenjdk/openjdk8:ubi
RUN mkdir /opt/app
COPY microservice_b.jar /opt/app
CMD ["java", "-jar", "/opt/app/microservice_b.jar"]
Once you have your JAR-file and the correct references in your Dockerfile (already included in the project files), you can create the Docker image with the following command:
docker build -t microservice_b-image -f Dockerfile .
If you now display all images with the command below, you should see a new repository called "microservice_b-image".
docker images
The image showing on your list should look something like this:
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
---|---|---|---|---|
mmicroservice_b-image | latest | c9b3efde6524f | 2 hours ago | 466MB |
The message-broker is a central piece of the puzzle as it manages the communication between the microservices. In this tutorial the request-reply pattern was used for queuing. For detail see RabbitMQ RPC tutorial:
The RabittMQ server is all you need for this tutorial.
After the installation you should be able to find the application RabbitMQ Service - start. You can start it, but as there is no communitation to manage for the message-broker at the time, there will not be much to see. Also note that by default RabbitMQ service starts in the background with every system boot, so by principle you do not need to start it manually.
The image is created automatically for you during the installation. After the installation, you can display all images with the command below. You should see new repositories called "rabbitmq".
docker images
REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
---|---|---|---|---|
rabbitmq | management | c9b3efde6524f | 4 hours ago | 181MB |
rabbitmq | latest | c7a4gfde6524f | 4 hours ago | 151MB |
Now that everything is prepared it is time to put all the pieces together. Docker enables the containerization and booting of all microservices in one go. This can be easily done using the configuration for each service in a single docker-compose.yml-file. This file is already prepared for you in the project files:
version: "3"
services:
message-broker:
image: rabbitmq:management
ports:
- "5672:5672"
- "15672:15672"
command: rabbitmq-server
expose:
- 5672
- 15672
healthcheck:
test: [ "CMD", "nc", "-z", "localhost", "5672" ]
interval: 5s
timeout: 15s
retries: 1
microservice_b:
image: microservice_b-image
restart: on-failure
depends_on:
- message-broker
microservice_a:
image: microservice_a-image
restart: on-failure
depends_on:
- message-broker
All three services can be now containerized and booted with a single command:
docker-compose up
This starts the booting sequence:
The execution steps are logged in the console as follows:
microservice_b_1 | (B.0) Demo app initiated
microservice_b_1 | (B.1) Opening connection
message-broker_1 | ...accepting AMQP connection...
message-broker_1 | ...user authenticated...
microservice_b_1 | (B.2) Connection successful.
microservice_b_1 | (B.3) Awaiting RPC requests
microservice_a_1 | (A.2) Initiating request #1
microservice_a_1 | (A.3) Async request task started
microservice_a_1 | (A.4) Request stringified
message-broker_1 | ...accepting AMQP connection...
message-broker_1 | ...user authenticated...
microservice_a_1 | (A.5) RpcClient created
microservice_b_1 | (B.4) Request received: USD->EUR
microservice_b_1 | (B.5) Fetching FX rate from ECB
microservice_b_1 | (B.6) Sending response to A
microservice_a_1 | (A.6) Response received
message-broker_1 | ...closing AMQP connection...
microservice_a_1 | (A.7) Response: USD->EUR:0.8615
This project is released under the MIT license.