Containerizing Maven/Gradle based Multi Module Spring Boot Microservices using Docker & Kubernetes
Introduction
This article is about bootstrapping Maven / Gradle multi-module Spring Boot Microservices and have them deployed using Docker Compose & Kubernetes.
Most of the articles around the web try to define a single Spring Boot application and provide steps on dockerizing and have them deployed to Docker Compose or Kubernetes. This article focuses on filling the gaps for implementing and deploying multi-module interdependent microservices, which could be base for any similar usecase.
Usecase
We shall look into implementing a simple usecase which can help us understand the core concepts around configuring multi-module project with Maven & Gradle and the commands to build and create docker images. This is further extended with steps on having these microservices deployed with Docker Compose & Kubernetes.
Below is the use case we shall go through in this article. This includes three Spring Boot application exposing RESTFul endpoint and one of them being the root application and the other two being downstream applications.
Rather than providing detailed explanation on the underlying concepts around Spring Boot (YAML configuration, Spring Profiles, Docker, Kubernetes), We shall only focus on configurations and commands to execute to have the three applications built and deployed successfully.
Technology stack for implementing the Restful APIs...
Bootstrapping Project with Spring Initializr
Spring Initializr generates spring boot project with just what we need to start implementing Restful services quickly. Initialize the project with appropriate details and the below dependencies.
- Spring Web
- Spring Boot DevTools
Click Here to download maven / gradle project with the above dependencies.
This step is to bootstrap the project with necessary folder structure and dependencies. We shall use this as a base for defining the multi-module application with the root project and its child modules.
Project directory Structure
Below is the directory structure for the root project and the individual child modules for each of the spring boot application

Below are the three Spring Boot RESTful applications exposing greeting endpoint.
Service B&Service Care independent services running separately and responding with a greeting message when accessed.Service AconsumesService BandService C. Greeting messages responded by the downstream APIs is concatenated to the greeting message fromService A.
Multi Module Spring Boot applications with Maven
For Maven, we need to configure multi module projects with <modules> in Root Projects pom.xml. Modules rely upon dependencies and plugins that are configured within the root project. So its ideal to configure all dependencies and plugins within root pom.xml such that module’s pom.xml will be minimal with only specific dependencies they rely upon.
Below is the pom.xml for Root Project with all three modules registered under <modules>.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.toomuch2learn.microservices</groupId>
<artifactId>spring-multi-module-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-multi-module-service</name>
<description>Spring multi module service</description>
<packaging>pom</packaging>
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.4.1</version> <relativePath/> <!-- lookup parent from repository --> </parent>
<modules> <module>service-a</module> <module>service-b</module> <module>service-c</module> </modules>
<properties>
<java.version>11</java.version>
<spring-cloud.version>2020.0.0</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
</project>As per the usecase, Service A consumes Service B and Service C using Feign Http Client and thus this dependency is included in Service A pom.xml rather than in Root pom.xml.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.toomuch2learn.microservices</groupId>
<artifactId>spring-multi-module-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service-a</artifactId> <name>service-a</name> <description>Service A</description>
<properties>
<java.version>11</java.version>
<spring-boot.build-image.imageName>$DOCKER_USER_NAME$/${parent.artifactId}-${project.artifactId}</spring-boot.build-image.imageName>
</properties>
<dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> </dependencies>
</project>Apart from project specific meta data, pom.xml remains the same for both Service B & Service C.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.toomuch2learn.microservices</groupId>
<artifactId>spring-multi-module-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service-b</artifactId> <name>service-b</name> <description>Service B</description>
<properties>
<java.version>11</java.version>
<spring-boot.build-image.imageName>$DOCKER_USER_NAME$/${parent.artifactId}-${project.artifactId}</spring-boot.build-image.imageName>
</properties>
<dependencies>
</dependencies>
</project>Maven commands to execute to build, package and create docker images for the Spring Boot modules.
$ mvn help:evaluate -Dexpression=project.modules$ mvn clean package spring-boot:build-image$ mvn clean spring-boot:run -pl service-a
$ mvn clean package spring-boot:build-image -pl service-aMulti Module Spring Boot applications with Gradle
For Gradle, we need to include multi module projects in settings.gradle in Root Projects. Modules rely upon dependencies and plugins that are configured within the root project. So its ideal to configure all dependencies and plugins within root build.gradle such that module’s build.gradle will be minimal with only specific dependencies they rely upon.
Below is setting.gradle for Root Project with all three modules registered with include.
rootProject.name = 'spring-multi-module-service'
include 'service-a'include 'service-b'include 'service-c'plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
}
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
ext {
set('springCloudVersion', "2020.0.0")
}
allprojects {
group = 'com.toomuch2learn.microservices'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
}
subprojects {
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
apply plugin: 'java'
repositories {
mavenCentral()
}
test {
useJUnitPlatform()
}
dependencies {
implementation 'org.yaml:snakeyaml:1.27'
implementation 'org.springframework.boot:spring-boot-starter-web'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}
}
bootJar {
enabled = false
}
bootBuildImage{
enabled = false
}As per the usecase, Service A consumes Service B and Service C using Feign Http Client and thus this dependency is included in Service A build.gradle rather than in Root build.gradle.
dependencies { implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'}
bootJar {
enabled = true
}
bootBuildImage{
imageName='$DOCKER_USER_NAME$/'+rootProject.name+'-'+project.name
}Apart from project specific meta data, build.gradle remains the same for both Service B & Service C.
dependencies {}
bootJar {
enabled = true
}
bootBuildImage{
imageName='$DOCKER_USER_NAME$/'+rootProject.name+'-'+project.name
}Gradle commands to execute to build, package and create docker images for the Spring Boot modules.
$ gradle -q projects$ gradle clean build bootBuildImage$ gradle clean :service-a:bootRun
$ gradle clean :service-a:build :service-a:bootBuildImageWorking with Spring Profiles
One of the core features with Spring Framework is the support for Profiles. It allows us to configure environment specific properties and choose the appropriate profile based on the environment the service is deployed to.
Below is application.yaml for Service A which shows profiles dev and prod configured with dev being the default. Each profile is separated with --- in yaml file.
spring:
application:
name: service-a
profiles: active: "dev"
---
spring:
profiles: devserver:
port : 8081
serviceb:
url: http://localhost:8082
servicec:
url: http://localhost:8083
---
spring:
profiles: prodserver:
port : 8080Set spring.profiles.active to choose the appropriate profile to be used when starting the service.
Testing APIs via cURL
Start services using java and test services using curl command.
$ java -jar service-a\target\service-a-0.0.1-SNAPSHOT.jar
$ java -jar service-b\target\service-b-0.0.1-SNAPSHOT.jar
$ java -jar service-b\target\service-c-0.0.1-SNAPSHOT.jarUse the below cURLs to invoke the services. Observe the response from Service A which includes the concatenated message from Service B and Service C.
$ curl http://localhost:8081/greeting
$ curl http://localhost:8082/greeting
$ curl http://localhost:8083/greetingBelow is the output that should display upon accessing Service A, Service B & Service C using cURL. Observe the response from Service A with concatenated message from Service B and Service C

Working with Docker Images
Either with Maven or Gradle, Docker Images created by Spring Boot DevTools plugin remain’s the same.
Below are the images that are created.

Execute the below commands to list the docker images, start the containers and test the services.
$ docker images | grep spring-multi-moduleStart the containers individually. Observe passing spring.profiles.active environment variable to set prod profile during startup.
Also, observe the docker run command for service-a. To access downstream APIs, serviceb.url and servicec.url is passed explicitly for Service A to call Service B & Service C.
$ docker run -d -p 8081:8080 -e spring.profiles.active=prod -e serviceb.url=http://<HOST_IP>:8082 -e servicec.url=http://<HOST_IP>:8083 $DOCKER_USER_NAME$/spring-multi-module-service-service-a
$ docker run -d -p 8082:8080 -e spring.profiles.active=prod $DOCKER_USER_NAME$/spring-multi-module-service-service-b
$ docker run -d -p 8083:8080 -e spring.profiles.active=prod $DOCKER_USER_NAME$/spring-multi-module-service-service-c$ docker ps$ docker stop <CONTAINER_ID>$ docker ps -a$ docker start <CONTAINER_ID>$ docker rm <CONTAINER_ID>$ docker rmi <IMAGE_ID>$ docker stop $(docker ps -a -q)$ docker rm $(docker ps -a -q)Pushing docker images to Docker Hub
Follow the below steps to tag the images and push them to Docker Hub.
$ docker login$ docker tab <IMAGE_ID> <Docker_Username>/<IMAGE_NAME>
$ docker tag ffc5ec760103 $DOCKER_USER_NAME$/springboot-servicea$ docker push <Docker_Username>/<IMAGE_NAME>
$ docker push $DOCKER_USER_NAME$/spring-multi-module-service-service-aDeploying with Docker Compose
Instead of running docker images individually, the whole stack can be brought up / down with docker-compose.
Below is docker-compose.yaml with all the services configured individually.
version: '3.9'
# Define services
services:
# Service A
service-a:
image: $DOCKER_USER_NAME$/spring-multi-module-service-service-a
ports:
- "8081:8080"
restart: always
links: - service-b - service-c environment:
- "spring.profiles.active=prod" - "serviceb.url=http://service-b:8082" - "servicec.url=http://service-c:8083" networks:
- backend
# Service B
service-b:
image: $DOCKER_USER_NAME$/spring-multi-module-service-service-b
ports:
- "8082:8080"
restart: always
environment:
- "spring.profiles.active=prod" networks:
- backend
# Service C
service-c:
image: $DOCKER_USER_NAME$/spring-multi-module-service-service-c
ports:
- "8083:8080"
restart: always
environment:
- "spring.profiles.active=prod" networks:
- backend
# Networks to be created to facilitate communication between containers
networks:
backend:As observed, For Service A to access downstream APIs Service B & Service C, links is configured for service-a and the name of the services service-b & service-c is configured for serviceb.url & servicec.url which will ensure HTTP call to the downstream API is successful.
Run the below docker-compose commands to get the stack up, stop, start, down appropriately.
$ docker-compose config$ docker-compose up -d$ docker-compose ps$ docker-compose stop$ docker-compose start$ docker-compose downDeploying with Kubernetes
Docker Desktop is the easiest way to run Kubernetes on your windows machine. It gives ua a fully certified Kubernetes cluster and manages all the components for us.
Install Docker Desktop and ensure to enable kubernetes from settings window.

Execute $ kubectl cluster-info to verify if Kubernetes cluster is running successfully. If not, uninstall and install Docker Desktop and ensure you receive the below message as expected to proceed with kubernetes deployment.
$ kubectl cluster-info
Kubernetes master is running at https://kubernetes.docker.internal:6443
KubeDNS is running at https://kubernetes.docker.internal:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxyTo deploy docker images to kubernetes, we need to configure Deployment and Service objects and have them applied to orchestrate the deployment into kubernetes cluster.
Deployment configuration files are created for services in individual yaml file under k8s folder.

Below is the sample deployment & service configuration for Service A to deploy to kubernetes.
apiVersion: apps/v1
kind: Deployment
metadata:
name: springboot-service-a
spec:
replicas: 1
selector:
matchLabels:
app: springboot-service-a
template:
metadata:
labels:
app: springboot-service-a
spec:
containers:
- name: app
image: $DOCKER_USER_NAME$/spring-multi-module-service-service-a
ports:
- containerPort: 8080
imagePullPolicy: Always
env: - name: spring.profiles.active value: "prod" - name: serviceb.url value: "http://$(SPRINGBOOT_SERVICE_B_SVC_SERVICE_HOST):8080" - name: servicec.url value: "http://$(SPRINGBOOT_SERVICE_C_SVC_SERVICE_HOST):8080"---
apiVersion: v1
kind: Service
metadata:
name: springboot-service-a-svcspec:
selector:
app: springboot-service-a
ports:
- port: 8080
targetPort: 8080
type: LoadBalancerAs observed, Service A deployment is configured with environment variables to set spring.profiles.active to prod.
To communicate with downstream APIs, we need to pass serviceb.url & servicec.url with host & port details of Service B and Service C. For this to work, we need to configure the environment variable that are created when services are created. One of them is SPRINGBOOT_SERVICE_B_SVC_SERVICE_HOST.
Passing $(SPRINGBOOT_SERVICE_B_SVC_SERVICE_HOST) will expand the environment variable with host of Service B and set to serviceb.url.
Run the below kubectl commands to deploy or delete the stack.
$ kubectl apply -f k8s$ kubectl get all$ kubectl get pods --watch
$ kubectl get pods -l app=springboot-service-a --watchService A is configured with LoadBalancer service. This ensures that when Service A is accessed, the request will be routed to one of the provisioned pod. To test this, we can scale the services up by increasing the replica count. To bring down the services, we can decrease the replica count.
$ kubectl scale --replicas=3 deployment/springboot-service-a$ kubectl exec <POD_NAME> -- printenv | grep SERVICE$ kubectl logs <POD_NAME>
$ kubectl logs -f <POD_NAME>$ kubectl delete -f k8sConclusion
This article is useful for usecase which needs all the services to be bundled as modules under a root project. With the support of configuring multiple modules with Maven and Gradle, it is easy for one to build all the modules with single command and have the stack deployed seamlessly to docker-compose and kubernetes.
For better understanding on microservices, it is always ideal to have more than one Spring Boot service implemented.
This article shall be the base for trying out the below concepts with Spring Boot & Spring Cloud.
- Centralized Configuration
- Service Discovery
- Distributed Tracing
- Fault Tolerance
- API Gateway
- And many more…
No comments:
Post a Comment