We covered the 'Multiple Services Instances' angle of Spring Boot and Docker in the Part 2 article of this series.
In this Part 3 article, we shall see how to manage dependencies between services, recovering from start failures, following a start up sequence through Docker Compose in an application that has multiple services. There are numerous examples of such an arrangement:
- Microservice that talks to a database (another service)
- Single Page application (SPA) that talks to microservice in background
- Necessary service bindings (e.g. distributed logging)
Pre-requisites:
To experience the journey as outlined in the article, it is recommended to have the pre-requisites in place before getting on with the technical steps.
- Reading: Basic awareness about Containerization & Docker
- Tool: Installed Docker Desktop – contains multiple Docker services
- Tool: Familiarity with Java (preferably 8 onwards), Spring Boot 3.4.x
- Tool: Installed JDK 17 & Maven (build tool)
- Hardware: Recommended to have at least 08 GB RAM (more the merrier)
- Reading: Understood the Part 1 & Part 2 articles (Basics & Docker Compose)
- Code: We shall use the 'Referral' Spring Boot application available at this Github Repo and Docker setup used in Part 1 & 2 article
Context:
We have a Spring Boot microservice called 'Referral'. This service helps persist & retrieve referral (any person) information in a back-end database - H2 (in memory) or external (PostgreSQL on-prem or container) and each of these 03 storage options can be enabled by choosing the active spring profile i.e. local (H2), dev (PostgreSQL on-prem) or dockerize (PostgreSQL on Docker container). Note that the default spring profile is local.
Let's look at the operations we can perform with the Referral application:
- GET /referrals/status to get the status of the service (alternative is Actuator but that is not enabled right now)
- GET /referrals to retrieve all the referrals
- POST /referrals to add a referral [JSON request like { "givenName": "Marty" }]
Objectives we want to accomplish:
- Deploy the Referral application to Docker (think service)
- Keep multiple instances of the API / service (think replicas)
- Have request distribution to the Referral application instances (think another service i.e NGINX)
- Referral application to connect with PostgreSQL running in container (think another service i.e. PostgreSQL)
When using 'dockerize' spring profile, the database connectivity information & schema setups are not provided in the application properties but are defined externally in the Docker Compose YML (we shall touch on this later in the article)
Steps:
In order to meet the objectives, let's move one step at a time as there is a lot to accomplish here.
Understand Services & inter-dependencies
The traffic to 'Referral' application instances will channeled though the web server NGINX (similar approach to Part 2 article). The 'Referral' application instances shall integrate with PostgreSQL running in another container. So, we see at least 03 entities with their inter-dependencies as below:
- NGINX (depends on Referral)
- Referral (depends on PostgreSQL)
- PostgreSQL (with necessary database schema)
What does 'Referral Controller' do?
Let's look at the below code:
package com.sb.mybatis.postgre.web;
import com.sb.mybatis.postgre.dto.ReferralDTO;
import com.sb.mybatis.postgre.service.ReferralService;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/referrals")
public class ReferralController {
@Autowired
private ReferralService referralService;
@GetMapping("/status")
public String getStatus() {
log.info("ReferralController.getStatus() is invoked");
return "Referral Service is Up";
}
@GetMapping
public List<ReferralDTO> getTransactions() throws Exception {
log.info("ReferralController.getTransactions() is invoked");
return referralService.findAllReferralRecords();
}
@PostMapping
public ReferralDTO createTransaction(@Valid @RequestBody ReferralDTO referralDTO) throws Exception {
log.info("ReferralController.createTransactions() is invoked");
if(referralService.insertReferral(referralDTO) == 1) {
//Success
return referralDTO;
}
else throw new Exception("Error in creating new Referral record");
}
}
The ReferralController allows us to create/retrieve Referral records, provide service status and at the same time also add a simple message in the application logs that can be verified (would be referred later in the article).
Create Referral Executable Jar
First and foremost, please clone the Referral spring boot application locally and run the command mvn clean install
to have the executable jar file ready (name like Referral-0.0.1-SNAPSHOT.jar).
Create Dockerfile
In order to create a Docker image for the Referral application, we first need to create a Dockerfile.
FROM openjdk:17
EXPOSE 8081
ARG JAR_FILE=Referral-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} Referral-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java","-Dspring.profiles.active=dockerize","-jar","/Referral-0.0.1-SNAPSHOT.jar"]
Provision of the spring profile 'dockerize' in the ENTRYPOINT ensures this profile is picked up in the eventual image creation
Create 'Referral' Docker image
Using the Docker image, let's execute the command docker build -t sb-referral-service:V1 .
to create the docker image for the Referral application.
A successful execution should look like below, indicating that the Docker image 'sb-referral-service:V1' is built.
Create NGINX.conf (web server configuration)
Similar to the Part 2 article, let's create a NGINX configuration file which should indicate that all requests to sb-referral-service should be channeled through the NGINX service.
user nginx;
events {
worker_connections 10;
}
http {
server {
listen 8000;
location / {
proxy_pass http://sb-referral-service:8081;
}
}
}
Create PostgreSQL DB Schema creation script
As mentioned previously in the Step 'Understand Services & inter-dependencies', we would like to ensure that the PostgreSQL DB in Docker should have the necessary database schema. If this is not the case, the Referral application will not work because Spring Boot is not instructed to initiate schema creation in 'dockerize' mode/profile.
Let's create an SQL script by the name 'create-db.sql' with content as below:
create table if not exists T_REFERRAL
(
id varchar(255) primary key,
name varchar(255) NOT NULL
);
'create-db.sql' needs to be part of the PostgreSQL configuration in Docker Compose YML file
Define Service Orchestration via Docker Compose
Once we have the Referral image, database schema creation SQL script and the NGINX configuration files in place, the next step is to define how the different services will be associated, sequenced (in start up) and recover (in case of any dependent service failure). Let's have a look at the Docker Compose YML below:
services:
nginx:
image: nginx:latest
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- sb-referral-service
ports:
- '8000:8000'
sb-referral-service:
image: 'sb-referral-service:V1'
restart: on-failure
expose:
- '8081'
build:
context: .
deploy:
replicas : 2
depends_on:
- db
environment:
- SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/test
- SPRING_DATASOURCE_USERNAME=test
- SPRING_DATASOURCE_PASSWORD=test
- SPRING_JPA_HIBERNATE_DDL_AUTO=none
db:
image: 'postgres:13.1-alpine'
container_name: db
environment:
- POSTGRES_USER=test
- POSTGRES_PASSWORD=test
volumes:
- ./db:/var/lib/postgresql/data
- ./create-db.sql:/docker-entrypoint-initdb.d/create_database.sql
Looking at the YML file closely, we find 03 services:
- NGINX: the web server, that will be setup by Docker by utilizing the NGINX.conf file and that has dependency on 'sb-referral-service'
- sb-referral-service: the Referral application, that will have 02 instances (replicas), will be restarted by Docker if it's previous attempt was unsuccessful (restart: on-failure) and is dependent on 'db'
- db: the PostgreSQL server, that will use the create-db.sql script and use the provided credentials for 'test' database
Notice the provision of the Spring Boot to database connectivity information in the 'environment' section under 'sb-referral-service' and the atypical spring datasource url as
jdbc:postgresql://db:5432/test
- that is because the database is running in a container
Starting the Services in Docker
To initiate creation of the services in Docker, we need to execute the command docker compose up
(please ensure create-db.sql and nginx.conf files are also present in the same folder) and we should see a message like below:
Once the services are started successfully, we should be able to see them in Docker Desktop like below:
Pre-Checks before consuming the services
Sometimes re-starts can be noticed in the upstream services (in this case, sb-referral-service) if the downstream services (in this case, db) are still getting setup while the upstream is trying to connect to the downstream. So, instead of rushing to consume the Referral application after seeing 05 containers visible in the Docker Desktop Dashboard, we can do some pre-checks also.
So, before we attempt to start using the API/end points of Referral application at the URL http://localhost:8000/referrals (note the port 8000 is of NGINX), we can additionally verify the following messages from the command prompt / log messages displayed by 'docker compose up' command:
Successful PostgreSQL setup:
Successful Referral application setup:
Because the 'depends on' (in Docker Compose YML) only waits for the other container to be up (and not for its contained application to be fully running), it is advisable to additionally verify the applications/services before consuming them.
Consuming the 'Referral' application
Now that the services are running in Docker, let's try consuming them, this time from Postman.
When we execute GET http://localhost:8000/referrals/status, we should get a successful message about service status, like below.
When we execute GET http://localhost:8000/referrals, we should receive zero Referral records, because none exist, like below.
When we execute POST http://localhost:8000/referrals, we should see a success message with the Referral information, like below.
Now, again when we execute GET http://localhost:8000/referrals to see if we see the newly created Referral in the fetch operation, we receive the Referral record(s), like below.
In addition to the above, let's also verify the Referral application logs. For the same, we can go to the individual sb-referral-service instances/containers and go to Logs option, we should see below (based on the above success).
Note that as per the above two logs, Referral services instances received different requests (from Postman), which also proves that NGINX is rotating the requests between the instances successfully
Destroy the containers / services
As our work finishes, we can finish the containers using the command docker compose down
and we should see a confirmation, like below.
Summary
In this article, we saw how Docker (and Docker Compose) allowed us to comfortably build a service orchestration that is typically only possible in a test environment - on our development machine and how we can use it - covering multiple services (including database), service to service communication, service start up sequencing and recovery.
Would appreciate any feedback for improvement on the approach and content.
References:
Top comments (0)