Microservice

Containerizing Maven/Gradle based Multi Module Spring Boot Microservices using Docker & Kubernetes

Clone the source code of the article from mavengradle-based-multi-module-spring-boot-microservices

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.

Service A
Service B
Service C

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

"Project directory structure"
Project directory structure

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 consumes Service B and Service C. Greeting messages responded by the downstream APIs is concatenated to the greeting message from Service 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>.

Root Project - 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 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.

Service A - 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.

Service B - 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-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.

List all modules under maven project
$ mvn help:evaluate -Dexpression=project.modules
Build and create docker images for all modules from parent
$ mvn clean package spring-boot:build-image
Execute maven goal on specific module from parent
$ 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.

setting.gradle
rootProject.name = 'spring-multi-module-service'

include 'service-a'include 'service-b'include 'service-c'
Root Project - build.gradle
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.

Service A - 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.

Service B - build.gradle
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.

List all projects under gradle project
$ gradle -q projects
Build and create docker images for all project from parent
$ gradle clean build bootBuildImage
Execute gradle task on specific project from parent
$ 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.

Service A application.yaml
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.

Start services using Java by running fat jar
$ 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.

Access RESTful endpoint using cURL
$ 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

"Response output from cURL call"
Response output from cURL call

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.

"Docker Images created by Spring Boot DevTools"
Docker Images created by Spring Boot DevTools

Execute the below commands to list the docker images, start the containers and test the services.

List Docker images
$ 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.

Run docker images
$ 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
List containers currently running
$ docker ps
Stop running container
$ docker stop <CONTAINER_ID>
List all containers that are running or stopped
$ docker ps -a
Start a container which is not running
$ docker start <CONTAINER_ID>
Delete docker container
$ docker rm <CONTAINER_ID>
Delete docker image
$ docker rmi <IMAGE_ID>
One liner to stop all running containers
$ docker stop $(docker ps -a -q)
One liner to remove all stopped containers
$ 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.

Ensure to login to Docker
$ docker login
Tag docker image before pushing to docker hub if image name is not tagged with docker hub username
$ docker tab <IMAGE_ID> <Docker_Username>/<IMAGE_NAME>

$  docker tag ffc5ec760103 $DOCKER_USER_NAME$/springboot-servicea
Push to docker hub
$ 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.

docker-compose.yaml
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.

Validate docker-compose file and configuration
$ docker-compose config
Builds, (re)creates, starts, and attaches to containers for a service.
$ docker-compose up -d
Lists containers
$ docker-compose ps
Stops running containers without removing them
$ docker-compose stop
Starts existing containers for a service
$ docker-compose start
Stops containers and removes containers, networks, volumes, and images created by - up
$ 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.

"Enable Kubernetes from Docker Desktop settings tab"
Enable Kubernetes from Docker Desktop settings tab

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.

Verify if kubernetes is running
$  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.

"K8s configuration files"
K8s configuration files

Below is the sample deployment & service configuration for Service A to deploy to kubernetes.

service-a.yaml
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.

Apply deployments and services
$ kubectl apply -f k8s
List deployments, services and pods after applying the change
$ kubectl get all
Watch pods by getting all pods or for a specific app
$ 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.

Scale deployment up/down by setting replicas
$ kubectl scale --replicas=3 deployment/springboot-service-a
Get Environment Variables set to the pod
$ kubectl exec <POD_NAME> -- printenv | grep SERVICE
Dump pod logs
$ kubectl logs <POD_NAME>

$ kubectl logs -f <POD_NAME>
Delete pods and services
$ 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…
Clone the source code of the article from mavengradle-based-multi-module-spring-boot-microservices

Madan Narra18 Posts

Software developer, Consultant & Architect

Madan is a software developer, writer, and ex-failed-startup co-founder. He has over 10+ years of experience building scalable and distributed systems using Java, JavaScript, Node.js. He writes about software design and architecture best practices with Java and is especially passionate about Microservices, API Development, Distributed Applications and Frontend Technologies.

  • Github
  • Linkedin
  • Facebook
  • Twitter
  • Instagram

Contents

Get The Best Of All Hands Delivered To Your Inbox

Subscribe to our newsletter and stay updated.