Deploy a Micronaut Microservices Application to a Local Kubernetes Cluster
This guide shows how to use the Graal Development Kit for Micronaut (GDK) to create an application consisting of three microservices, build containerized versions of the microservices, and deploy them with Kubernetes. Thanks to the Micronaut® Kubernetes module, you can connect the microservices using Kubernetes Service Discovery and Distributed Configuration.
Kubernetes is a portable, extensible, open source platform for managing containerized workloads and services, that facilitates both declarative configuration and automation. It has a large, rapidly growing ecosystem. Kubernetes services, support, and tools are widely available.
Prerequisites #
- JDK 17 or higher. See Setting up Your Desktop.
- A Docker-API compatible container runtime such as Rancher Desktop or Docker installed to run MySQL and to run tests using Testcontainers.
- A local Kubernetes cluster. This guide uses Minikube.
A note regarding your development environment
Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.
Note: If you use IntelliJ IDEA, enable annotation processing.
Follow the steps below to create the application from scratch. However, you can also download the completed example:
Note on the Sample Application #
The application consists of three microservices:
- users - contains customer data that can place orders on items, also a new customer can be created. It requires HTTP basic authentication to access it.
- orders - contains all orders that customers have created as well as available items that customers can order. This microservice also enables the creation of new orders. It requires HTTP basic authentication to access it.
- api - acts as a gateway to the orders and users services. It combines results from both services and checks data when a customer creates a new order.
Initially, the URLs of the orders and users services will be included in the code for the api microservice. Additionally, you will hard-code the credentials (username and password) into every microservice configuration that are required for HTTP basic authentication.
The second part of this guide describes how to use Kubernetes Discovery Service and Kubernetes Configuration Maps to dynamically resolve the URLs of the orders and users microservices and retrieve authentication credentials. The microservices call the Kubernetes API to register when they start up and then resolve placeholders inside the microservices’ configurations.
Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.
1. Create the Users Microservice #
Create the users microservice using the GDK Launcher.
-
Open the GDK Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Application (Default)
- Project Name: users
- Base Package: com.example (Default)
- Clouds: None
- Build Tool: Gradle (Groovy) or Maven
- Language: Java (Default)
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: Kubernetes
- Features: GraalVM Native Image, Kubernetes Service Discovery, Kubernetes Support, Micronaut Management, Micronaut Security, and Micronaut Serialization Jackson Core
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates a directory named users containing an application with a package named
com.example
. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.
Alternatively, use the GDK CLI as follows:
gdk create-app com.example.users \
--services=k8s \
--features=discovery-kubernetes,management,security,serialization-jackson,kubernetes,graalvm \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.users \
--services=k8s \
--features=discovery-kubernetes,management,security,serialization-jackson,kubernetes,graalvm \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
1.1. Controllers in the Users Microservice #
-
Create a
UsersController
class to handle incoming HTTP requests for the users microservice in a file named users/src/main/java/com/example/controllers/UsersController.java:package com.example.controllers; import com.example.models.User; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import io.micronaut.validation.Validated; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import java.util.ArrayList; import java.util.List; import java.util.Optional; @Controller("/users") // <1> @Secured(SecurityRule.IS_AUTHENTICATED) // <2> @Validated class UsersController { List<User> persons = new ArrayList<>(); @Post // <3> public User add(@Body @Valid User user) { Optional<User> existingUser = findByUsername(user.username()); if (existingUser.isPresent()) { throw new HttpStatusException(HttpStatus.CONFLICT, "User with provided username already exists"); } User newUser = new User(persons.size() + 1, user.firstName(), user.lastName(), user.username()); persons.add(newUser); return newUser; } @Get("/{id}") // <4> public User findById(int id) { return persons.stream() .filter(it -> it.id().equals(id)) .findFirst().orElse(null); } @Get // <5> public List<User> getUsers() { return persons; } Optional<User> findByUsername(@NotNull String username) { return persons.stream() .filter(it -> it.username().equals(username)) .findFirst(); } }
1 The class is defined as a controller with the
@Controller
annotation mapped to the path/users
.2 Annotate with
io.micronaut.security.Secured
to configure secured access. TheisAuthenticated()
expression will allow access only to authenticated users.3 The
@Post
annotation maps theadd
method to an HTTP POST request on/users
.4 The
@Get
annotation maps thefindById
method to an HTTP GET request on/users/{id}
.5 The
@Get
annotation maps thegetUsers
method to an HTTP GET request on/users
. -
Create a
User
record to represent the customer in a file named users/src/main/java/com/example/models/User.java, as follows:package com.example.models; import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @Serdeable // <1> public record User( @Nullable @Max(10000) Integer id, // <2> @NotBlank @JsonProperty("first_name") String firstName, @NotBlank @JsonProperty("last_name") String lastName, String username ) { }
1 Declare the
@Serdeable
annotation at the type level to enable the type to be serialized or deserialized.2 The ID will be generated by the application.
-
Create a
Credentials
class in a file named users/src/main/java/com/example/auth/Credentials.java, which will load and store credentials (username and password) from a configuration file:package com.example.auth; import io.micronaut.context.annotation.ConfigurationProperties; @ConfigurationProperties("authentication-credentials") // <1> public record Credentials (String username, String password) {}
1 The
@ConfigurationProperties
annotation takes the configuration prefix. -
Create a
CredentialsChecker
class in a file named users/src/main/java/com/example/auth/CredentialsChecker.java, which, as the name suggests, will check if the provided credentials inside the HTTP request’sAuthorization
header are the same as those that are stored inside theCredentials
class created above:package com.example.auth; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.AuthenticationProvider; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @Singleton // <1> class CredentialsChecker implements AuthenticationProvider<HttpRequest<?>> { private final Credentials credentials; CredentialsChecker(Credentials credentials) { this.credentials = credentials; } @Override public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) { return Mono.<AuthenticationResponse>create(emitter -> { if ( authenticationRequest.getIdentity().equals(credentials.username()) && authenticationRequest.getSecret().equals(credentials.password()) ) { emitter.success(AuthenticationResponse.success((String) authenticationRequest.getIdentity())); } else { emitter.error(AuthenticationResponse.exception()); } }); } }
1 Use
jakarta.inject.Singleton
to designate a class as a singleton.
1.2. Tests to Verify Application Logic #
-
Create a
UsersClient
Micronaut HTTP inline client for testing in a file named users/src/test/java/com/example/UsersClient.java, as follows:package com.example; import com.example.models.User; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import java.util.List; @Client("/") // <1> public interface UsersClient { @Get("/users/{id}") User getById(@Header String authorization, int id); @Post("/users") User createUser(@Header String authorization, @Body User user); @Get("/users") List<User> getUsers(@Header String authorization); }
1 Use
@Client
to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theid
member to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTest
test that checks there is/health
endpoint (required for service discovery) in a file named users/src/test/java/com/example/HealthTest.java with the following contents:package com.example; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import org.junit.jupiter.api.Test; import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; @MicronautTest // <1> class HealthTest { @Inject @Client("/") HttpClient client; // <2> @Test public void healthEndpointExposed() { HttpStatus status = client.toBlocking().retrieve(HttpRequest.GET("/health"), HttpStatus.class); assertEquals(HttpStatus.OK, status); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information , see Micronaut Test.)2 Inject the
HttpClient
bean and point it to the embedded server. -
Create a
UsersControllerTest
class to test endpoints inside theUserController
in a file named users/src/test/java/com/example/UsersControllerTest.java with the following contents:package com.example; import com.example.auth.Credentials; import com.example.models.User; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientException; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; import java.util.Base64; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @MicronautTest // <1> class UsersControllerTest { @Inject UsersClient usersClient; @Inject Credentials credentials; @Test void testUnauthorized() { HttpClientException exception = assertThrows(HttpClientException.class, () -> usersClient.getUsers("")); assertTrue(exception.getMessage().contains("Unauthorized")); } @Test void getUserThatDoesntExists() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); User retriedUser = usersClient.getById(authHeader, 100); assertNull(retriedUser); } @Test void multipleUserInteraction() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); String firstName = "firstName"; String lastName = "lastName"; String username = "username"; User user = new User(0 ,firstName, lastName, username); User createdUser = usersClient.createUser(authHeader, user); assertEquals(firstName, createdUser.firstName()); assertEquals(lastName, createdUser.lastName()); assertEquals(username, createdUser.username()); assertNotNull(createdUser.id()); User retriedUser = usersClient.getById(authHeader, createdUser.id()); assertEquals(firstName, retriedUser.firstName()); assertEquals(lastName, retriedUser.lastName()); assertEquals(username, retriedUser.username()); List<User> users = usersClient.getUsers(authHeader); assertNotNull(users); assertTrue(users.stream() .map(User::username) .anyMatch(name -> name.equals(username))); } @Test void createSameUserTwice() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); String firstName = "SameUserFirstName"; String lastName = "SameUserLastName"; String username = "SameUserUsername"; User user = new User(0 ,firstName, lastName, username); User createdUser = usersClient.createUser(authHeader, user); assertEquals(firstName, createdUser.firstName()); assertEquals(lastName, createdUser.lastName()); assertEquals(username, createdUser.username()); assertNotNull(createdUser.id()); assertNotEquals(createdUser.id(), 0); HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> usersClient.createUser(authHeader, user)); assertEquals(HttpStatus.CONFLICT, exception.getStatus()); assertTrue(exception.getResponse().getBody(String.class).orElse("").contains("User with provided username already exists")); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.) -
Edit the users/src/main/resources/application-k8s.properties file as follows:
# <1> authentication-credentials.username=${username} # <2> authentication-credentials.password=${password}
1 Placeholder for a username that will be populated by Kubernetes.
2 Placeholder for a password that will be populated by Kubernetes.
-
Edit the users/src/main/resources/bootstrap-k8s.properties file to enable distributed configuration. Modify the contents so that it matches the following:
micronaut.application.name=users # <1> micronaut.config-client.enabled=true # <2> kubernetes.client.secrets.enabled=true # <3> kubernetes.client.secrets.use-api=true
1 Set
micronaut.config-client.enabled: true
to read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: true
to enable Kubernetes secrets as distributed source.3 Set
kubernetes.client.secrets.use-api: true
to use the Kubernetes API to fetch the configuration. -
Create a file named users/src/main/resources/application-dev.properties. This configuration file is applied only for the
dev
environment.# <1> micronaut.server.port=8081 # <2> authentication-credentials.username=gdkdemo # <3> authentication-credentials.password=example-password
1 Configure the application to listen on port 8081.
2 Username for the development environment.
3 Password for the development environment.
-
Create a file named users/src/main/resources/bootstrap-dev.properties to disable distributed configuration in the
dev
environment:# <1> kubernetes.client.secrets.enabled=false
1 Disable the Kubernetes secrets client.
-
Create a file named users/src/test/resources/application-test.properties for use in the test environment:
# <1> authentication-credentials.username=gdkdemo # <2> authentication-credentials.password=example-password
1 Username for the test environment.
2 Password for the test environment.
-
Run the unit test, as follows:
1.3. Run the Users Microservice #
Run the users microservice as follows:
MICRONAUT_ENVIRONMENTS=dev ./gradlew run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && gradlew run"
MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && mvnw mn:run""
2. Create the Orders Microservice #
Create the orders microservice using the GDK Launcher.
-
Open the GDK Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Application (Default)
- Project Name: orders
- Base Package: com.example (Default)
- Clouds: None
- Build Tool: Gradle (Groovy) or Maven
- Language: Java (Default)
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: Kubernetes
- Features: GraalVM Native Image, Kubernetes Service Discovery, Kubernetes Support, Micronaut Management, Micronaut Security, and Micronaut Serialization Jackson Core
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates a directory named orders containing a Micronaut application with a package named
com.example
. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.
Alternatively, use the GDK CLI as follows:
gdk create-app com.example.orders \
--services=k8s \
--features=discovery-kubernetes,management,security,serialization-jackson,kubernetes,graalvm \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.orders \
--services=k8s \
--features=discovery-kubernetes,management,security,serialization-jackson,kubernetes,graalvm \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
2.1. Controllers in the Orders Microservice #
-
Create the
OrdersController
andItemsController
classes to handle incoming HTTP requests to the orders microservice.OrdersController
is in the orders/src/main/java/com/example/controllers/OrdersController.java file:package com.example.controllers; import com.example.models.Item; import com.example.models.Order; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import jakarta.validation.Valid; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @Controller("/orders") // <1> @Secured(SecurityRule.IS_AUTHENTICATED) // <2> class OrdersController { private final List<Order> orders = new ArrayList<>(); @Get("/{id}") // <3> public Order findById(int id) { return orders.stream() .filter(it -> it.id().equals(id)) .findFirst().orElse(null); } @Get // <4> public List<Order> getOrders() { return orders; } @Post // <5> public Order createOrder(@Body @Valid Order order) { if (order.itemIds() == null || order.itemIds().isEmpty()) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, "Items must be supplied"); } List<Item> items = order.itemIds().stream().map( x -> Item.items.stream().filter( y -> y.id().equals(x) ).findFirst().orElseThrow( () -> new HttpStatusException(HttpStatus.BAD_REQUEST, String.format("Item with id %s doesn't exist", x)) ) ).collect(Collectors.toList()); BigDecimal total = items.stream().map(Item::price).reduce(BigDecimal::add).orElse(new BigDecimal("0")); Order newOrder = new Order(orders.size() + 1, order.userId(), items, null, total); orders.add(newOrder); return newOrder; } }
1 The class is defined as a controller with the
@Controller
annotation mapped to the path/orders
.2 Annotate with
io.micronaut.security.Secured
to configure secured access. TheisAuthenticated()
expression will allow access only to authenticated users.3 The
@Get
annotation maps thefindById
method to an HTTP GET request on/orders/{id}
.4 The
@Get
annotation maps thegetOrders
method to an HTTP GET request on/orders
.5 The
@Post
annotation maps thecreateOrder
method to an HTTP POST request on/orders
. -
Create the
ItemsController
class in a file named orders/src/main/java/com/example/controllers/ItemsController.java, as follows:package com.example.controllers; import com.example.models.Item; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.security.annotation.Secured; import io.micronaut.security.rules.SecurityRule; import java.util.List; @Controller("/items") // <1> @Secured(SecurityRule.IS_AUTHENTICATED) // <2> class ItemsController { @Get("/{id}") // <3> public Item findById(int id) { return Item.items.stream() .filter(it -> it.id().equals(id)) .findFirst().orElse(null); } @Get // <4> public List<Item> getItems() { return Item.items; } }
1 The class is defined as a controller with the
@Controller
annotation mapped to the path/items
.2 Annotate with
io.micronaut.security.Secured
to configure secured access. TheisAuthenticated()
expression will allow access only to authenticated users.3 The
@Get
annotation maps thefindById
method to an HTTP GET request on/items/{id}
.4 The
@Get
annotation maps thegetItems
method to an HTTP GET request on/items
. -
Create the
Order
andItem
objects, used by theOrdersController
andItemsController
classes, to represent customer orders.Order
is in the orders/src/main/java/com/example/models/Order.java file:package com.example.models; import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; import java.math.BigDecimal; import java.util.List; @Serdeable // <1> public record Order( @Max(10000) @Nullable Integer id, // <2> @NotBlank @JsonProperty("user_id") Integer userId, @Nullable List<Item> items, // <3> @NotBlank @JsonProperty("item_ids") @Nullable List<Integer> itemIds, // <4> @Nullable BigDecimal total ) { }
1 Declare the
@Serdeable
annotation at the type level in your source code to allow the type to be serialized or deserialized.2 The ID will be generated by application.
3 The
List
ofItem
class will be populated by the server and will be only visible in sever responses.4 List of
itemIds
will be provided by client requests. -
The
Item
record is in the orders/src/main/java/com/example/models/Item.java file:package com.example.models; import io.micronaut.serde.annotation.Serdeable; import java.math.BigDecimal; import java.util.List; @Serdeable // <1> public record Item( Integer id, String name, BigDecimal price ) { public static List<Item> items = List.of( new Item(1, "Banana", new BigDecimal("1.5")), new Item(2, "Kiwi", new BigDecimal("2.5")), new Item(3, "Grape", new BigDecimal("1.25")) ); }
1 Declare the
@Serdeable
annotation at the type level in your source code to allow the type to be serialized or deserialized. -
Create the
Credentials
class to load and store credentials (username and password) from configuration files in a file named orders/src/main/java/com/example/auth/Credentials.java with the following contents:package com.example.auth; import io.micronaut.context.annotation.ConfigurationProperties; @ConfigurationProperties("authentication-credentials") // <1> public record Credentials (String username, String password) {}
1 The
@ConfigurationProperties
annotation takes the configuration prefix. -
Create a
CredentialsChecker
class in a file named orders/src/main/java/com/example/auth/CredentialsChecker.java, which, as the name suggests, will check if the provided credentials inside the HTTP request’sAuthorization
header are the same as those that are stored inside theCredentials
class created above:package com.example.auth; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.AuthenticationProvider; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import jakarta.inject.Singleton; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @Singleton // <1> class CredentialsChecker implements AuthenticationProvider<HttpRequest<?>> { private final Credentials credentials; CredentialsChecker(Credentials credentials) { this.credentials = credentials; } @Override public Publisher<AuthenticationResponse> authenticate(@Nullable HttpRequest<?> httpRequest, AuthenticationRequest<?, ?> authenticationRequest) { return Mono.<AuthenticationResponse>create(emitter -> { if ( authenticationRequest.getIdentity().equals(credentials.username()) && authenticationRequest.getSecret().equals(credentials.password()) ) { emitter.success(AuthenticationResponse.success((String) authenticationRequest.getIdentity())); } else { emitter.error(AuthenticationResponse.exception()); } }); } }
1 Use
jakarta.inject.Singleton
to designate a class as a singleton.
2.2. Tests to Verify Application Logic #
-
Create an
OrderItemClient
Micronaut HTTP inline client for testing in a file named orders/src/test/java/com/example/OrderItemClient.java, as follows:package com.example; import com.example.models.Item; import com.example.models.Order; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Header; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import java.util.List; @Client("/") // <1> interface OrderItemClient { @Get("/orders/{id}") Order getOrderById(@Header String authorization, int id); @Post("/orders") Order createOrder(@Header String authorization, @Body Order order); @Get("/orders") List<Order> getOrders(@Header String authorization); @Get("/items") List<Item> getItems(@Header String authorization); @Get("/items/{id}") Item getItemsById(@Header String authorization, int id); }
1 Use
@Client
to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theid
member to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTest
class, in a file named orders/src/test/java/com/example/HealthTest.java, that checks if there is/health
endpoint (required for service discovery), with the following contents:package com.example; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import org.junit.jupiter.api.Test; import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; @MicronautTest // <1> class HealthTest { @Inject @Client("/") HttpClient client; // <2> @Test public void healthEndpointExposed() { HttpStatus status = client.toBlocking().retrieve(HttpRequest.GET("/health"), HttpStatus.class); assertEquals(HttpStatus.OK, status); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information , see Micronaut Test.)2 Inject the
HttpClient
bean and point it to the embedded server. -
Create a
ItemsControllerTest
class, in the file named orders/src/test/java/com/example/ItemsControllerTest.java, to test endpoints inside theItemController
, with the following contents:package com.example; import com.example.auth.Credentials; import com.example.models.Item; import io.micronaut.http.client.exceptions.HttpClientException; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.Base64; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @MicronautTest // <1> class ItemsControllerTest { @Inject OrderItemClient orderItemClient; @Inject Credentials credentials; @Test void testUnauthorized() { HttpClientException exception = assertThrows(HttpClientException.class, () -> orderItemClient.getItems("")); assertTrue(exception.getMessage().contains("Unauthorized")); } @Test void getItem() { int itemId = 1; String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); Item item = orderItemClient.getItemsById(authHeader, itemId); assertEquals(itemId, item.id()); assertEquals("Banana", item.name()); assertEquals(new BigDecimal("1.5"), item.price()); } @Test void getItems() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); List<Item> items = orderItemClient.getItems(authHeader); assertNotNull(items); List<String> existingItemNames = List.of("Kiwi", "Banana", "Grape"); assertEquals(3, items.size()); assertTrue(items.stream() .map(Item::name) .allMatch(name -> existingItemNames.stream().anyMatch(x -> x.equals(name)))); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.) -
Create an
OrdersControllerTest
class to test endpoints inside theOrdersController
in a file named orders/src/test/java/com/example/OrdersControllerTest.javapackage com.example; import com.example.auth.Credentials; import com.example.models.Order; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientException; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.Base64; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @MicronautTest // <1> class OrdersControllerTest { @Inject OrderItemClient orderItemClient; @Inject Credentials credentials; @Test void testUnauthorized() { HttpClientException exception = assertThrows(HttpClientException.class, () -> orderItemClient.getOrders("")); assertTrue(exception.getMessage().contains("Unauthorized")); } @Test void multipleOrderInteraction() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); int userId = 1; List<Integer> itemIds = List.of(1, 1, 2, 3); Order order = new Order(0, userId, null, itemIds, null); Order createdOrder = orderItemClient.createOrder(authHeader, order); assertNotNull(createdOrder.items()); assertEquals(4, createdOrder.items().size()); assertEquals(new BigDecimal("6.75"), createdOrder.total()); assertEquals(userId, createdOrder.userId()); Order retrievedOrder = orderItemClient.getOrderById(authHeader, createdOrder.id()); assertNotNull(retrievedOrder.items()); assertEquals(4, retrievedOrder.items().size()); assertEquals(new BigDecimal("6.75"), retrievedOrder.total()); assertEquals(userId, retrievedOrder.userId()); List<Order> orders = orderItemClient.getOrders(authHeader); assertNotNull(orders); assertTrue(orders.stream() .map(Order::userId) .anyMatch(id -> id.equals(userId))); } @Test void itemDoesntExists() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); int userId = 1; List<Integer> itemIds = List.of(5); Order order = new Order(0, userId, null, itemIds, null); HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> orderItemClient.createOrder(authHeader, order)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); assertTrue(exception.getResponse().getBody(String.class).orElse("").contains("Item with id 5 doesn't exist")); } @Test void orderEmptyItems() { String authHeader = "Basic " + Base64.getEncoder().encodeToString((credentials.username() + ":" + credentials.password()).getBytes()); int userId = 1; Order order = new Order(0, userId, null, null, null); HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> orderItemClient.createOrder(authHeader, order)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); assertTrue(exception.getResponse().getBody(String.class).orElse("").contains("Items must be supplied")); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.) -
Edit the orders/src/main/resources/application-k8s.properties file as follows:
# <1> authentication-credentials.username=${username} # <2> authentication-credentials.password=${password}
1 Placeholder for a username that will be populated by Kubernetes.
2 Placeholder for a password that will be populated by Kubernetes.
-
Edit the orders/src/main/resources/bootstrap-k8s.properties file to enable distributed configuration. Modify the contents so that it matches the following:
micronaut.application.name=orders # <1> micronaut.config-client.enabled=true # <2> kubernetes.client.secrets.enabled=true # <3> kubernetes.client.secrets.use-api=true
1 Set
micronaut.config-client.enabled: true
to read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: true
to enable Kubernetes secrets as distributed source.3 Set
kubernetes.client.secrets.use-api: true
to use Kubernetes API to fetch configuration. -
Edit the orders/src/main/resources/application-dev.properties file, which is applied only for the
dev
environment, as follows:# <1> micronaut.server.port=8082 # <2> authentication-credentials.username=gdkdemo # <3> authentication-credentials.password=example-password
1 Configure the application to listen on port 8082.
2 Username for development environment.
3 Password for development environment.
-
Create a file named orders/src/main/resources/bootstrap-dev.properties to disable distributed configuration in the
dev
environment:# <1> kubernetes.client.secrets.enabled=false
1 Disable the Kubernetes secrets client.
-
Create a file named orders/src/test/resources/application-test.properties (to be used in the test environment) with the following contents:
micronaut.application.name=orders # <1> authentication-credentials.username=gdkdemo # <2> authentication-credentials.password=example-password
1 Username for development environment.
2 Password for development environment.
-
Run the unit test, as follows:
2.3. Run the Orders Microservice #
Run the orders microservice as follows:
MICRONAUT_ENVIRONMENTS=dev ./gradlew run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && gradlew run"
MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && mvnw mn:run""
3. Create the API (Gateway) Microservice #
Create the api microservice using the GDK Launcher.
-
Open the GDK Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Application (Default)
- Project Name: api
- Base Package: com.example (Default)
- Clouds: None
- Build Tool: Gradle (Groovy) or Maven
- Language: Java (Default)
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: Kubernetes
- Features: GraalVM Native Image, Kubernetes Service Discovery, Kubernetes Support, Micronaut Management, Micronaut Serialization Jackson Core, and Mockito Framework
- Sample Code: No
- Click Generate Project, then click Download Zip. GDK Launcher creates a directory named api containing a Micronaut application with a package named
com.example
. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.
Alternatively, use the GDK CLI as follows:
gdk create-app com.example.api \
--services=k8s \
--features=discovery-kubernetes,management,kubernetes,serialization-jackson,mockito,graalvm \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.api \
--services=k8s \
--features=discovery-kubernetes,management,kubernetes,serialization-jackson,mockito,graalvm \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
3.1. Controllers in the API (Gateway) Microservice #
-
Create a
GatewayController
class to handle incoming HTTP requests to the api microservice in a file named api/src/main/java/com/example/controllers/GatewayController.java as follows:package com.example.controllers; import com.example.clients.OrdersClient; import com.example.clients.UsersClient; import com.example.models.Item; import com.example.models.Order; import com.example.models.User; import io.micronaut.core.annotation.NonNull; import io.micronaut.http.HttpStatus; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Controller; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.exceptions.HttpStatusException; import io.micronaut.scheduling.TaskExecutors; import io.micronaut.scheduling.annotation.ExecuteOn; import io.micronaut.validation.Validated; import jakarta.validation.Valid; import java.util.ArrayList; import java.util.List; @Controller("/api") // <1> @Validated @ExecuteOn(TaskExecutors.IO) // <2> class GatewayController { private final OrdersClient orderClient; private final UsersClient userClient; GatewayController(OrdersClient orderClient, UsersClient userClient) { this.orderClient = orderClient; this.userClient = userClient; } @Get("/users/{id}") // <3> User getUserById(int id) { return userClient.getById(id); } @Get("/orders/{id}") // <4> Order getOrdersById(int id) { Order order = orderClient.getOrderById(id); return new Order(order.id(), null, getUserById(order.userId()), order.items(), order.itemIds(), order.total()); } @Get("/items/{id}") // <5> Item getItemsById(int id) { return orderClient.getItemsById(id); } @Get("/users") // <6> List<User> getUsers() { return userClient.getUsers(); } @Get("/items") // <7> List<Item> getItems() { return orderClient.getItems(); } @Get("/orders") // <8> List<Order> getOrders() { List<Order> orders = new ArrayList<>(); orderClient.getOrders().forEach(x-> orders.add(new Order(x.id(), null, getUserById(x.userId()), x.items(), x.itemIds(), x.total()))); return orders; } @Post("/orders") // <9> Order createOrder(@Body @Valid Order order) { User user = getUserById(order.userId()); if (user == null) { throw new HttpStatusException(HttpStatus.BAD_REQUEST, String.format("User with id %s doesn't exist", order.userId())); } Order createdOrder = orderClient.createOrder(order); return new Order(createdOrder.id(), null, user, createdOrder.items(), createdOrder.itemIds(), createdOrder.total()); } @Post("/users") // <10> User createUser(@Body @NonNull User user) { return userClient.createUser(user); } }
1 The class is defined as a controller with the
@Controller
annotation mapped to the path/api
.2 It is critical that any blocking I/O operations (such as fetching the data from the database) are offloaded to a separate thread pool that does not block the event loop.
3 The
@Get
annotation maps thegetUserById
method to an HTTP GET request on/users/{id}
.4 The
@Get
annotation maps thegetOrdersById
method to an HTTP GET request on/orders/{id}
.5 The
@Get
annotation maps thegetItemsById
method to an HTTP GET request on/items/{id}
.6 The
@Get
annotation maps thegetUsers
method to an HTTP GET request on/users
.7 The
@Get
annotation maps thegetItems
method to an HTTP GET request on/items
.8 The
@Get
annotation maps thegetOrders
method to an HTTP GET request on/orders
.9 The
@Post
annotation maps thecreateUser
method to an HTTP POST request on/users
.10 The
@Post
annotation maps thecreateOrder
method to an HTTP POST request on/orders
. -
Create the
User
,Order
, andItem
objects to represent customers, orders, and items. TheUser
record is in a file named api/src/main/java/com/example/models/User.java, as follows:package com.example.models; import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; @Serdeable // <1> public record User( @Nullable @Max(10000) Integer id, @NotBlank @JsonProperty("first_name") String firstName, @NotBlank @JsonProperty("last_name") String lastName, String username ) { }
1 Declare the
@Serdeable
annotation at the type level in your source code to allow the type to be serialized or deserialized. -
The
Order
record is in a file named api/src/main/java/com/example/models/Order.java, as follows:package com.example.models; import com.fasterxml.jackson.annotation.JsonProperty; import io.micronaut.core.annotation.Nullable; import io.micronaut.serde.annotation.Serdeable; import jakarta.validation.constraints.Max; import jakarta.validation.constraints.NotBlank; import java.math.BigDecimal; import java.util.List; @Serdeable // <1> public record Order( @Nullable @Max(10000) Integer id, @NotBlank @Nullable @JsonProperty("user_id") Integer userId, @Nullable User user, @Nullable List<Item> items, @NotBlank @Nullable @JsonProperty("item_ids") List<Integer> itemIds, @Nullable BigDecimal total ) { }
1 Declare the
@Serdeable
annotation at the type level in your source code to allow the type to be serialized or deserialized. -
The
Item
record is in a file named orders/src/main/java/com/example/models/Item.java, as follows:package com.example.models; import io.micronaut.serde.annotation.Serdeable; import java.math.BigDecimal; @Serdeable // <1> public record Item( Integer id, String name, BigDecimal price ) { }
1 Declare the
@Serdeable
annotation at the type level in your source code to allow the type to be serialized or deserialized. -
Create a
UserClient
for the users microservice in a file named api/src/main/java/com/example/clients/UsersClient.java, as follows:package com.example.clients; import com.example.models.User; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import java.util.List; @Client("users") // <1> public interface UsersClient { @Get("/users/{id}") User getById(int id); @Post("/users") User createUser(@Body User user); @Get("/users") List<User> getUsers(); }
1 Use
@Client
to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theid
member to provide a service identifier or specify the URL directly as the annotation’s value. -
Create an
OrdersClient
for the orders microservice in a file named api/src/main/java/com/example/clients/OrdersClient.java, as follows:package com.example.clients; import com.example.models.Item; import com.example.models.Order; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import java.util.List; @Client("orders") // <1> public interface OrdersClient { @Get("/orders/{id}") Order getOrderById(int id); @Post("/orders") Order createOrder(@Body Order order); @Get("/orders") List<Order> getOrders(); @Get("/items") List<Item> getItems(); @Get("/items/{id}") Item getItemsById(int id); }
1 Use
@Client
to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theid
member to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
Credentials
class, will load the username and password from configuration that will be needed for comparison when checking HTTP basic authentication credentials, in a file named api/src/main/java/com/example/auth/Credentials.java with the following contents:package com.example.auth; import io.micronaut.context.annotation.ConfigurationProperties; @ConfigurationProperties("authentication-credentials") // <1> public record Credentials (String username, String password) {}
1 The
@ConfigurationProperties
annotation takes the configuration prefix. -
Create an
AuthClientFilter
class that acts as a client filter applied to every client. It adds a HTTP basic authentication header with credentials that are stored in theCredentials
class, in a file named api/src/main/java/com/example/auth/AuthClientFilter.java with the following contents:package com.example.auth; import io.micronaut.http.HttpResponse; import io.micronaut.http.MutableHttpRequest; import io.micronaut.http.annotation.Filter; import io.micronaut.http.filter.ClientFilterChain; import io.micronaut.http.filter.HttpClientFilter; import org.reactivestreams.Publisher; @Filter(Filter.MATCH_ALL_PATTERN) class AuthClientFilter implements HttpClientFilter { private final Credentials credentials; AuthClientFilter(Credentials credentials) { this.credentials = credentials; } @Override public Publisher<? extends HttpResponse<?>> doFilter(MutableHttpRequest<?> request, ClientFilterChain chain) { return chain.proceed(request.basicAuth(credentials.username(), credentials.password())); } }
-
Create an
ErrorExceptionHandler
class that will propagate errors from the orders and users microservices in a file named api/src/main/java/com/example/ErrorExceptionHandler.java with the following contents:package com.example; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpResponse; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.http.server.exceptions.ExceptionHandler; import jakarta.inject.Singleton; @Singleton // <1> public class ErrorExceptionHandler implements ExceptionHandler<HttpClientResponseException, HttpResponse<?>> { @Override public HttpResponse<?> handle(HttpRequest request, HttpClientResponseException exception) { return HttpResponse .status(exception.getResponse().status()) .body(exception.getResponse().getBody(String.class).orElse(null)); } }
1 Use
jakarta.inject.Singleton
to designate a class as a singleton.
3.2. Write Tests to Verify Application Logic #
-
Create a
GatewayClient
Micronaut HTTP inline client for testing in a file named api/src/test/java/com/example/GatewayClient.javapackage com.example; import com.example.models.Item; import com.example.models.Order; import com.example.models.User; import io.micronaut.http.annotation.Body; import io.micronaut.http.annotation.Get; import io.micronaut.http.annotation.Post; import io.micronaut.http.client.annotation.Client; import java.util.List; @Client("/") // <1> public interface GatewayClient { @Get("/api/items/{id}") Item getItemById(int id); @Get("/api/orders/{id}") Order getOrderById(int id); @Get("/api/users/{id}") User getUsersById(int id); @Get("/api/users") List<User> getUsers(); @Get("/api/items") List<Item> getItems(); @Get("/api/orders") List<Order> getOrders(); @Post("/api/orders") Order createOrder(@Body Order order); @Post("/api/users") User createUser(@Body User user); }
1 Use
@Client
to use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theid
member to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTest
class to check that there is/health
endpoint (required for service discovery) in a file named api/src/test/java/com/example/HealthTest.java with the following contents:package com.example; import io.micronaut.http.HttpRequest; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.HttpClient; import io.micronaut.http.client.annotation.Client; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import org.junit.jupiter.api.Test; import jakarta.inject.Inject; import static org.junit.jupiter.api.Assertions.assertEquals; @MicronautTest // <1> class HealthTest { @Inject @Client("/") HttpClient client; // <2> @Test public void healthEndpointExposed() { HttpStatus status = client.toBlocking().retrieve(HttpRequest.GET("/health"), HttpStatus.class); assertEquals(HttpStatus.OK, status); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information , see Micronaut Test.)2 Inject the
HttpClient
bean and point it to the embedded server. -
Create a
GatewayControllerTest
class to test endpoints inside theGatewayController
in a file named api/src/test/java/com/example/GatewayControllerTest.java with the following contents:package com.example; import com.example.clients.OrdersClient; import com.example.clients.UsersClient; import com.example.models.Item; import com.example.models.Order; import com.example.models.User; import io.micronaut.http.HttpResponse; import io.micronaut.http.HttpStatus; import io.micronaut.http.client.exceptions.HttpClientResponseException; import io.micronaut.test.annotation.MockBean; import io.micronaut.test.extensions.junit5.annotation.MicronautTest; import jakarta.inject.Inject; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @MicronautTest // <1> class GatewayControllerTest { @Inject OrdersClient ordersClient; @Inject UsersClient usersClient; @Inject GatewayClient gatewayClient; @MockBean(OrdersClient.class) OrdersClient ordersClient() { return mock(OrdersClient.class); } @MockBean(UsersClient.class) UsersClient usersClient() { return mock(UsersClient.class); } @Test void getItemById() { int itemId = 1; Item item = new Item(itemId, "test", BigDecimal.ONE); when(ordersClient.getItemsById(1)).thenReturn(item); Item retrievedItem = gatewayClient.getItemById(item.id()); assertEquals(item.id(), retrievedItem.id()); assertEquals(item.name(), retrievedItem. name()); assertEquals(item.price(), retrievedItem.price()); } @Test void getOrderById() { Order order = new Order(1, 2, null, null, new ArrayList<>(), null); User user = new User(order.userId(), "firstName", "lastName", "test"); when(ordersClient.getOrderById(1)).thenReturn(order); when(usersClient.getById(user.id())).thenReturn(user); Order retrievedOrder = gatewayClient.getOrderById(order.id()); assertEquals(order.id(), retrievedOrder.id()); assertEquals(order.userId(), retrievedOrder.user().id()); assertNull(retrievedOrder.userId()); assertEquals(user.username(), retrievedOrder.user().username()); } @Test void getUserById() { User user = new User(1, "firstName", "lastName", "test"); when(usersClient.getById(1)).thenReturn(user); User retrievedUser = gatewayClient.getUsersById(user.id()); assertEquals(user.id(), retrievedUser.id()); assertEquals(user.username(), retrievedUser.username()); } @Test void getUsers() { User user = new User(1, "firstName", "lastName", "test"); when(usersClient.getUsers()).thenReturn(List.of(user)); List<User> users = gatewayClient.getUsers(); assertNotNull(users); assertEquals(1, users.size()); assertEquals(user.id(), users.get(0).id()); assertEquals(user.username(), users.get(0).username()); } @Test void getItems() { Item item = new Item(1, "test", BigDecimal.ONE); when(ordersClient.getItems()).thenReturn(List.of(item)); List<Item> items = gatewayClient.getItems(); assertNotNull(items); assertEquals(1, items.size()); assertEquals(item.name(), items.get(0).name()); assertEquals(item.price(), items.get(0).price()); } @Test void getOrders() { Order order = new Order(1, 2, null, null, new ArrayList<>(), null); User user = new User(order.userId(), "firstName", "lastName", "test"); when(ordersClient.getOrders()).thenReturn(List.of(order)); when(usersClient.getById(order.userId())).thenReturn(user); List<Order> orders = gatewayClient.getOrders(); assertNotNull(orders); assertEquals(1, orders.size()); assertNull(orders.get(0).userId()); assertEquals(user.id(), orders.get(0).user().id()); assertEquals(order.id(), orders.get(0).id()); assertEquals(user.username(), orders.get(0).user().username()); } @Test void createUser() { String firstName = "firstName"; String lastName = "lastName"; String username = "username"; User user = new User(0, firstName, lastName, username); when(usersClient.createUser(any())).thenReturn(user); User createdUser = gatewayClient.createUser(user); assertEquals(firstName, createdUser.firstName()); assertEquals(lastName, createdUser.lastName()); assertEquals(username, createdUser.username()); } @Test void createOrder() { Order order = new Order(1, 2, null, null, new ArrayList<>(), null); User user = new User(order.userId(), "firstName", "lastName", "test"); when(usersClient.getById(user.id())).thenReturn(user); when(ordersClient.createOrder(any())).thenReturn(order); Order createdOrder = gatewayClient.createOrder(order); assertEquals(order.id(), createdOrder.id()); assertNull(createdOrder.userId()); assertEquals(order.userId(), createdOrder.user().id()); assertEquals(user.username(), createdOrder.user().username()); } @Test void createOrderUserDoesntExists() { Order order = new Order(1, 2, null, null, new ArrayList<>(), new BigDecimal(0));; when(ordersClient.createOrder(any())).thenReturn(order); when(usersClient.getById(order.userId())).thenReturn(null); HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> gatewayClient.createOrder(order)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); assertTrue(exception.getResponse().getBody(String.class).orElse("").contains("User with id 2 doesn't exist")); } @Test void exceptionHandler() { User user = new User(1, "firstname", "lastname", "username"); String message = "Test error message"; when(usersClient.createUser(any())).thenThrow(new HttpClientResponseException("Test", HttpResponse.badRequest(message))); HttpClientResponseException exception = assertThrows(HttpClientResponseException.class, () -> gatewayClient.createUser(user)); assertEquals(HttpStatus.BAD_REQUEST, exception.getStatus()); assertTrue(exception.getResponse().getBody(String.class).orElse("").contains("Test error message")); } }
1 Annotate the class with
@MicronautTest
so the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.) -
Edit the api/src/main/resources/application-k8s.properties file as follows:
# <1> authentication-credentials.username=${username} # <2> authentication-credentials.password=${password}
1 Placeholder for a username that will be populated by Kubernetes.
2 Placeholder for a password that will be populated by Kubernetes.
-
Edit the api/src/main/resources/bootstrap-k8s.properties file to enable distributed configuration. Modify the contents so that it matches the following:
micronaut.application.name=api # <1> micronaut.config-client.enabled=true # <2> kubernetes.client.secrets.enabled=true # <3> kubernetes.client.secrets.use-api=true
1 Set
micronaut.config-client.enabled: true
to read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: true
to enable Kubernetes secrets as a distributed source.3 Set
kubernetes.client.secrets.use-api: true
to use the Kubernetes API to fetch configuration. -
Create a file named api/src/main/resources/application-dev.properties. This configuration file is applied only for the
dev
environment.# <1> authentication-credentials.username=gdkdemo # <2> authentication-credentials.password=example-password
1 Username for development environment.
2 Password for development environment.
-
Create a file named api/src/main/resources/bootstrap-dev.properties to disable distributed configuration in the
dev
environment:# <1> micronaut.http.services.users.urls=http://localhost:8081 # <2> micronaut.http.services.orders.urls=http://localhost:8082 # <3> kubernetes.client.secrets.enabled=false
1 URL of the users microservice.
2 URL of the orders microservice.
3 Disable the Kubernetes secrets client.
-
Create a file named api/src/test/resources/application-test.properties (to be used in the test environment) with the following contents:
# <1> authentication-credentials.username=gdkdemo # <2> authentication-credentials.password=example-password
1 Username for development environment.
2 Password for development environment.
-
Run the unit test, as follows:
3.3. Run the api Microservice #
Run the api microservice as follows:
MICRONAUT_ENVIRONMENTS=dev ./gradlew run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && gradlew run"
MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
Or if you use Windows:
cmd /C "set MICRONAUT_ENVIRONMENTS=dev && mvnw mn:run""
4. Test Integration Between Microservices #
-
Run a
curl
command to create a new user via the api microservice:curl -X "POST" localhost:8080/api/users \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
Your output should look like:
{ "id":1, "username":"nmikic", "first_name":"Nemanja", "last_name":"Mikic" }
-
Run a
curl
command to create a new order via the api microservice:curl -X "POST" localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "user_id": 1, "item_ids": [1,2] }'
Your output should include details of the order, as follows:
{ "id": 1, "user": { "first_name": "Nemanja", "last_name": "Mikic", "id": 1, "username": "nmikic" }, "items": [ { "id": 1, "name": "Banana", "price": 1.5 }, { "id": 2, "name": "Kiwi", "price": 2.5 } ], "total": 4.0 }
-
Run a
curl
command to list the orders:curl localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8'
You should see output that is similar to the following:
[ { "id": 1, "user": { "first_name": "Nemanja", "last_name": "Mikic", "id": 1, "username": "nmikic" }, "items": [ { "id": 1, "name": "Banana", "price": 1.5 }, { "id": 2, "name": "Kiwi", "price": 2.5 } ], "total": 4.0 } ]
-
Try to place an order for a user who doesn’t exist (with
id
100). Run the followingcurl
command:curl -X "POST" localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "user_id": 100, "item_ids": [1,2] }'
You should see the following error message:
{ "message": "Bad Request", "_links": { "self": [ { "href": "/api/orders", "templated": false } ] }, "_embedded": { "errors": [ { "message": "User with id 100 doesn't exist" } ] } }
5. Deploy Microservices to a Local Kubernetes Cluster #
This section describes how to create the necessary Kubernetes resources for your microservices. It shows how to build container images and deploy each of the microservices on the local Kubernetes cluster.
If you are using Docker make sure that Kubernetes is enabled.
If you are using Rancher Desktop make sure that Kubernetes is enabled and that the virtual machines have 5GB of memory.
Note: If you are using Gradle to build your application on AArch64, add the following to your settings.gradle file:buildscript { dependencies { classpath("com.github.docker-java:docker-java:3.3.1") classpath("com.github.docker-java:docker-java-transport-httpclient5:3.3.1") } repositories { mavenCentral() } }
5.1. Configure Service Roles and Secrets #
-
Create a filed named auth.yml that specifies a service role for microservices that have secret configurations.
apiVersion: v1 kind: Namespace # <1> metadata: name: gdk-k8s --- apiVersion: v1 kind: ServiceAccount # <2> metadata: namespace: gdk-k8s name: gdk-service --- kind: Role # <3> apiVersion: rbac.authorization.k8s.io/v1 metadata: namespace: gdk-k8s name: gdk_service_role rules: - apiGroups: [""] resources: ["services", "endpoints", "configmaps", "secrets", "pods"] verbs: ["get", "watch", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding # <4> metadata: namespace: gdk-k8s name: gdk_service_role_bind subjects: - kind: ServiceAccount name: gdk-service roleRef: kind: Role name: gdk_service_role apiGroup: rbac.authorization.k8s.io --- apiVersion: v1 kind: Secret # <5> metadata: namespace: gdk-k8s name: mysecret type: Opaque data: username: Z2NuZGVtbw== # <6> password: ZXhhbXBsZS1wYXNzd29yZA== # <7>
1 Create a namespace named
gdk-k8s
.2 Create a service account named
gdk-service
.3 Create a role named
gdk_service_role
.4 Bind the
gdk_service_role
role to thegdk-service
service account.5 Create a secret named
mysecret
.6 The base64 value of the username secret that will be used by the microservices.
7 The base64 value of the password secret that will be used by the microservices.
-
Run the next command to create the resources described above:
kubectl apply -f auth.yml
5.2. Deploy the Users Microservice #
Build a container image of the users microservice named “users”.
From inside the users/ directory.
Note: If you are having issues building a container image try:
- Gradle, from inside the users/build/docker/native-main/ directory run
docker build . -t users -f DockerfileNative
.- Maven, from inside the user/target/ directory run
docker build . -t users -f Dockerfile
.
Next, edit the file named k8s.yml as follows:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: gdk-k8s
name: "users"
spec:
selector:
matchLabels:
app: "users"
template:
metadata:
labels:
app: "users"
spec:
serviceAccountName: gdk-service # <1>
containers:
- name: "users"
image: users # <2>
imagePullPolicy: Never # <3>
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health/readiness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health/liveness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
failureThreshold: 10
---
apiVersion: v1
kind: Service
metadata:
namespace: gdk-k8s
name: "users" # <4>
spec:
selector:
app: "users"
type: NodePort
ports:
- protocol: "TCP"
port: 8080 # <5>
1 The service name that you created in the auth.yaml file.
2 The name of the container image for deployment.
3 The imagePullPolicy
is set to Never
. You will always use the local one that you built in previous step.
4 The name of the microservice, required for service discovery.
5 Micronaut’s default port on which the application is running.
Then run the next command to create the resources described above:
kubectl apply -f k8s.yml
5.3. Deploy the Orders Microservice #
Build a container image of the orders microservice named “orders”.
From inside the orders/ directory.
Note: If you are having issues building a container image try:
- Gradle, from inside the orders/build/docker/native-main/ directory run
docker build . -t orders -f DockerfileNative
.- Maven, from inside the orders/target/ directory run
docker build . -t orders -f Dockerfile
.
Next, edit the file named k8s.yml as follows:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: gdk-k8s
name: "orders"
spec:
selector:
matchLabels:
app: "orders"
template:
metadata:
labels:
app: "orders"
spec:
serviceAccountName: gdk-service # <1>
containers:
- name: "orders"
image: orders # <2>
imagePullPolicy: Never # <3>
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health/readiness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health/liveness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
failureThreshold: 10
---
apiVersion: v1
kind: Service
metadata:
namespace: gdk-k8s
name: "orders" # <4>
spec:
selector:
app: "orders"
type: NodePort
ports:
- protocol: "TCP"
port: 8080 # <5>
1 The service name that you created in the auth.yaml file.
2 The name of the container image for deployment.
3 The imagePullPolicy
is set to Never
. You will always use local one that you built in previous step.
4 The name of the microservice, required for service discovery.
5 Micronaut’s default port on which application is running.
Then run the next command to create the resources described above:
kubectl apply -f k8s.yml
5.4. Deploy the API (Gateway) Microservice #
Build a container image of the api microservice named “api”.
From inside the api/ directory.
Note: If you are having issues building a container image try:
- Gradle, from inside the api/build/docker/native-main/ directory run
docker build . -t api -f DockerfileNative
.- Maven, from inside the api/target/ directory run
docker build . -t api -f Dockerfile
.
Next, edit the file named k8s.yml as follows:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: gdk-k8s
name: "api"
spec:
selector:
matchLabels:
app: "api"
template:
metadata:
labels:
app: "api"
spec:
serviceAccountName: gdk-service # <1>
containers:
- name: "api"
image: api # <2>
imagePullPolicy: Never # <3>
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /health/readiness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health/liveness
port: 8080
initialDelaySeconds: 5
timeoutSeconds: 3
failureThreshold: 10
---
apiVersion: v1
kind: Service
metadata:
namespace: gdk-k8s
name: "api" # <4>
spec:
selector:
app: "api"
type: LoadBalancer
ports:
- protocol: "TCP"
port: 8080 # <5>
1 The service name that you created in the auth.yaml file.
2 The name of the container image for deployment.
3 The imagePullPolicy
is set to Never
. You will always use local one that you built in previous step.
4 The name of the microservice, required for service discovery.
5 Micronaut’s default port on which application is running.
Then run the next command to create the resources described above:
kubectl apply -f k8s.yml
6. Test Integration Between Microservices Deployed to Kubernetes #
-
Run the next command to check status of the pods and make sure that all of them have the status “Running”:
kubectl get pods -n=gdk-k8s
NAME READY STATUS RESTARTS AGE api-774fd667b9-dmws4 1/1 Running 0 24s orders-74ff4fcbc4-dnfbw 1/1 Running 0 19s users-9f46dd7c6-vs8z7 1/1 Running 0 13s
-
Run the next command to check the status of the microservices:
kubectl get services -n=gdk-k8s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE api LoadBalancer 10.110.42.201 192.168.0.106 8080:32601/TCP 18s orders NodePort 10.105.43.19 <none> 8080:31033/TCP 21s users NodePort 10.104.130.114 <none> 8080:31482/TCP 26s
-
Run a
curl
command to create a new user via the api microservice:curl -X "POST" localhost:8080/api/users \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
Your output should look like:
{ "id":1, "username":"nmikic", "first_name":"Nemanja", "last_name":"Mikic" }
-
Run a
curl
command to create a new order via the api microservice:curl -X "POST" localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "user_id": 1, "item_ids": [1,2] }'
Your output should include details of the order, as follows:
{ "id": 1, "user": { "first_name": "Nemanja", "last_name": "Mikic", "id": 1, "username": "nmikic" }, "items": [ { "id": 1, "name": "Banana", "price": 1.5 }, { "id": 2, "name": "Kiwi", "price": 2.5 } ], "total": 4.0 }
-
Run a
curl
command to list the orders:curl localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8'
You should see output that is similar to the following:
[ { "id": 1, "user": { "first_name": "Nemanja", "last_name": "Mikic", "id": 1, "username": "nmikic" }, "items": [ { "id": 1, "name": "Banana", "price": 1.5 }, { "id": 2, "name": "Kiwi", "price": 2.5 } ], "total": 4.0 } ]
-
Try to place an order for a user who doesn’t exist (with
id
100). Run acurl
command:curl -X "POST" localhost:8080/api/orders \ -H 'Content-Type: application/json; charset=utf-8' \ -d '{ "user_id": 100, "item_ids": [1,2] }'
You should see the following error message:
{ "message": "Bad Request", "_links": { "self": [ { "href": "/api/orders", "templated": false } ] }, "_embedded": { "errors": [ { "message": "User with id 100 doesn't exist" } ] } }
7. Clean Up Resources #
To delete all resources that you created in this guide run the next command:
kubectl delete namespaces gdk-k8s
To delete the Minikube local Kubernetes cluster run:
minikube delete
Summary #
This guide demonstrated how to create a microservices application, build a containerized version of the microservices, and deploy each to a local Kubernetes cluster. Thanks to the Micronaut Kubernetes module, you can connect the microservices using Kubernetes Service Discovery and Distributed Configuration.