Containerizing Maven/Gradle based Multi Module Spring Boot Microservices using Docker & Kubernetes
Last modified: 28 Dec, 2020Introduction
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 C
are independent services running separately and responding with a greeting message when accessed.Service A
consumesService B
andService 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-a
Multi 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:bootBuildImage
Working 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 : 8080
Set 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.jar
Use 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/greeting
Below 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-module
Start 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-a
Deploying 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 down
Deploying 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/proxy
To 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: LoadBalancer
As 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 --watch
Service 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 k8s
Conclusion
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…