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.
The Micronaut Kubernetes project provides integration between Micronaut and Kubernetes. It adds support for the following features:
Service Discovery
Configuration client for config maps and secrets
Kubernetes blocking and non-blocking clients built on top of the official Kubernetes Java SDK
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.
Follow the steps below to create the application from scratch. However, you can also download the completed example:
The application ZIP file will be downloaded in your default downloads directory. Unzip it and proceed to the next steps.
A note regarding your development environment
Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit for Micronaut Extension Pack.
Note: If you use IntelliJ IDEA, enable annotation processing.
Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.
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.
1. Create the users Microservice
Create an application 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 Serialization Jackson Core, Micronaut Security
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package
com.examplein a directory named users. 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=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,security \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.users \
--services=k8s \
--features=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,security \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
Open the micronaut-cli.yml file, you can see what features are packaged with the application:
features: [app-name, discovery-kubernetes, gdk-bom, gdk-k8s, gdk-license, gdk-platform-independent, graalvm, http-client, java, java-application, jib, junit, kubernetes, kubernetes-client-openapi, logback, management, maven, maven-enforcer-plugin, micronaut-http-validation, netty-server, properties, readme, security, security-annotations, serialization-jackson, shade, static-resources]
1.1. Controllers in the Users Microservice
-
Create a
UsersControllerclass 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
@Controllerannotation mapped to the path/users.2 Annotate with
io.micronaut.security.Securedto configure secured access. TheisAuthenticated()expression will allow access only to authenticated users.3 The
@Postannotation maps theaddmethod to an HTTP POST request on/users.4 The
@Getannotation maps thefindByIdmethod to an HTTP GET request on/users/{id}.5 The
@Getannotation maps thegetUsersmethod to an HTTP GET request on/users. -
Create a
Userrecord 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
@Serdeableannotation at the type level to enable the type to be serialized or deserialized.2 The ID will be generated by the application.
-
Create a
Credentialsclass 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
@ConfigurationPropertiesannotation takes the configuration prefix. -
Create a
CredentialsCheckerclass 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’sAuthorizationheader are the same as those that are stored inside theCredentialsclass created above:package com.example.auth; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.AuthenticationFailureReason; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider; import jakarta.inject.Singleton; @Singleton (1) class CredentialsChecker<B> implements HttpRequestAuthenticationProvider<B> { private final Credentials credentials; CredentialsChecker(Credentials credentials) { this.credentials = credentials; } @Override public AuthenticationResponse authenticate(@Nullable HttpRequest<B> httpRequest, @NonNull AuthenticationRequest<String, String> authenticationRequest) { return authenticationRequest.getIdentity().equals(credentials.username()) && authenticationRequest.getSecret().equals(credentials.password()) ? AuthenticationResponse.success(authenticationRequest.getIdentity()) : AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH); } }1 Use
jakarta.inject.Singletonto designate a class as a singleton.
1.2. Tests to Verify Application Logic
-
Create a
UsersClientMicronaut 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
@Clientto use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theidmember to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTesttest that checks there is/healthendpoint (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
@MicronautTestso the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.)2 Inject the
HttpClientbean and point it to the embedded server. -
Create a
UsersControllerTestclass to test endpoints inside theUserControllerin 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
@MicronautTestso 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:
micronaut.application.name=users (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=true1 Set
micronaut.config-client.enabled: trueto read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: trueto enable Kubernetes secrets as distributed source.3 Set
kubernetes.client.secrets.use-api: trueto 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
devenvironment.(1) micronaut.server.port=8081 (2) authentication-credentials.username=gdkdemo (3) authentication-credentials.password=example-password1 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
devenvironment:(1) kubernetes.client.secrets.enabled=false1 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-password1 Username for the test environment.
2 Password for the test environment.
Run the tests:
Then open the file build/reports/tests/test/index.html in a browser to view the results.
1.3. Run the users Microservice
Run the users microservice as follows:
If you use Windows, run:
2. Create the orders Microservice
Create an application 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 Serialization Jackson Core, Micronaut Security
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package
com.examplein a directory named orders. 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=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,security \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.orders \
--services=k8s \
--features=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,security \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
Open the micronaut-cli.yml file, you can see what features are packaged with the application:
features: [app-name, discovery-kubernetes, gdk-bom, gdk-k8s, gdk-license, gdk-platform-independent, graalvm, http-client, java, java-application, jib, junit, kubernetes, kubernetes-client-openapi, logback, management, maven, maven-enforcer-plugin, micronaut-http-validation, netty-server, properties, readme, security, security-annotations, serialization-jackson, shade, static-resources]
2.1. Controllers in the Orders Microservice
-
Create the
OrdersControllerandItemsControllerclasses to handle incoming HTTP requests to the orders microservice.OrdersControlleris 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
@Controllerannotation mapped to the path/orders.2 Annotate with
io.micronaut.security.Securedto configure secured access. TheisAuthenticated()expression will allow access only to authenticated users.3 The
@Getannotation maps thefindByIdmethod to an HTTP GET request on/orders/{id}.4 The
@Getannotation maps thegetOrdersmethod to an HTTP GET request on/orders.5 The
@Postannotation maps thecreateOrdermethod to an HTTP POST request on/orders. -
Create the
ItemsControllerclass 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
@Controllerannotation mapped to the path/items.2 Annotate with
io.micronaut.security.Securedto configure secured access. TheisAuthenticated()expression will allow access only to authenticated users.3 The
@Getannotation maps thefindByIdmethod to an HTTP GET request on/items/{id}.4 The
@Getannotation maps thegetItemsmethod to an HTTP GET request on/items. -
Create the
OrderandItemobjects, used by theOrdersControllerandItemsControllerclasses, to represent customer orders.Orderis 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
@Serdeableannotation 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
ListofItemclass will be populated by the server and will be only visible in sever responses.4 List of
itemIdswill be provided by client requests. -
The
Itemrecord 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
@Serdeableannotation at the type level in your source code to allow the type to be serialized or deserialized. -
Create the
Credentialsclass 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
@ConfigurationPropertiesannotation takes the configuration prefix. -
Create a
CredentialsCheckerclass 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’sAuthorizationheader are the same as those that are stored inside theCredentialsclass created above:package com.example.auth; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; import io.micronaut.http.HttpRequest; import io.micronaut.security.authentication.AuthenticationFailureReason; import io.micronaut.security.authentication.AuthenticationRequest; import io.micronaut.security.authentication.AuthenticationResponse; import io.micronaut.security.authentication.provider.HttpRequestAuthenticationProvider; import jakarta.inject.Singleton; @Singleton (1) class CredentialsChecker<B> implements HttpRequestAuthenticationProvider<B> { private final Credentials credentials; CredentialsChecker(Credentials credentials) { this.credentials = credentials; } @Override public AuthenticationResponse authenticate(@Nullable HttpRequest<B> httpRequest, @NonNull AuthenticationRequest<String, String> authenticationRequest) { return authenticationRequest.getIdentity().equals(credentials.username()) && authenticationRequest.getSecret().equals(credentials.password()) ? AuthenticationResponse.success(authenticationRequest.getIdentity()) : AuthenticationResponse.failure(AuthenticationFailureReason.CREDENTIALS_DO_NOT_MATCH); } }1 Use
jakarta.inject.Singletonto designate a class as a singleton.
2.2. Tests to Verify Application Logic
-
Create an
OrderItemClientMicronaut 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
@Clientto use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theidmember to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTestclass, in a file named orders/src/test/java/com/example/HealthTest.java, that checks if there is/healthendpoint (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
@MicronautTestso the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.)2 Inject the
HttpClientbean and point it to the embedded server. -
Create a
ItemsControllerTestclass, 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
@MicronautTestso the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.) -
Create an
OrdersControllerTestclass to test endpoints inside theOrdersControllerin 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
@MicronautTestso 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=true1 Set
micronaut.config-client.enabled: trueto read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: trueto enable Kubernetes secrets as distributed source.3 Set
kubernetes.client.secrets.use-api: trueto use the Kubernetes API to fetch the configuration. -
Create a file named orders/src/main/resources/application-dev.properties. This configuration file is applied only for the
devenvironment.(1) micronaut.server.port=8082 (2) authentication-credentials.username=gdkdemo (3) authentication-credentials.password=example-password1 Configure the application to listen on port 8081.
2 Username for the development environment.
3 Password for the development environment.
-
Create a file named orders/src/main/resources/bootstrap-dev.properties to disable distributed configuration in the
devenvironment:(1) kubernetes.client.secrets.enabled=false1 Disable the Kubernetes secrets client.
-
Create a file named orders/src/test/resources/application-test.properties for use in the test environment:
micronaut.application.name=orders (1) authentication-credentials.username=gdkdemo (2) authentication-credentials.password=example-password1 Username for the test environment.
2 Password for the test environment.
Run the tests:
Then open the file build/reports/tests/test/index.html in a browser to view the results.
2.3. Run the orders Microservice
Run the orders microservice as follows:
If you use Windows, run:
3. Create the api Microservice
Create an application 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, Mockito Framework
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates an application with the package
com.examplein a directory named api. 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=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,mockito \
--example-code=false \
--build=gradle \
--jdk=17 \
--lang=java
gdk create-app com.example.api \
--services=k8s \
--features=graalvm,discovery-kubernetes,kubernetes,management,serialization-jackson,mockito \
--example-code=false \
--build=maven \
--jdk=17 \
--lang=java
Open the micronaut-cli.yml file, you can see what features are packaged with the application:
features: [app-name, discovery-kubernetes, gdk-bom, gdk-k8s, gdk-license, gdk-platform-independent, graalvm, http-client, java, java-application, jib, junit, kubernetes, kubernetes-client-openapi, logback, management, maven, maven-enforcer-plugin, micronaut-http-validation, mockito, netty-server, properties, readme, serialization-jackson, shade, static-resources]
3.1. Controllers in the API (Gateway) Microservice
-
Create a
GatewayControllerclass 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
@Controllerannotation 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
@Getannotation maps thegetUserByIdmethod to an HTTP GET request on/users/{id}.4 The
@Getannotation maps thegetOrdersByIdmethod to an HTTP GET request on/orders/{id}.5 The
@Getannotation maps thegetItemsByIdmethod to an HTTP GET request on/items/{id}.6 The
@Getannotation maps thegetUsersmethod to an HTTP GET request on/users.7 The
@Getannotation maps thegetItemsmethod to an HTTP GET request on/items.8 The
@Getannotation maps thegetOrdersmethod to an HTTP GET request on/orders.9 The
@Postannotation maps thecreateUsermethod to an HTTP POST request on/users.10 The
@Postannotation maps thecreateOrdermethod to an HTTP POST request on/orders. -
Create the
User,Order, andItemobjects to represent customers, orders, and items. TheUserrecord 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
@Serdeableannotation at the type level in your source code to allow the type to be serialized or deserialized. -
The
Orderrecord 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
@Serdeableannotation at the type level in your source code to allow the type to be serialized or deserialized. -
The
Itemrecord 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
@Serdeableannotation at the type level in your source code to allow the type to be serialized or deserialized. -
Create a
UserClientfor 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
@Clientto use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theidmember to provide a service identifier or specify the URL directly as the annotation’s value. -
Create an
OrdersClientfor 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
@Clientto use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theidmember to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
Credentialsclass, 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
@ConfigurationPropertiesannotation takes the configuration prefix. -
Create an
AuthClientFilterclass that acts as a client filter applied to every client. It adds a HTTP basic authentication header with credentials that are stored in theCredentialsclass, 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(patterns = Filter.MATCH_ALL_PATTERN, serviceId = { "orders", "users" } ) 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
ErrorExceptionHandlerclass 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.Singletonto designate a class as a singleton.
3.2. Write Tests to Verify Application Logic
-
Create a
GatewayClientMicronaut 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
@Clientto use declarative HTTP Clients. You can annotate interfaces or abstract classes. You can use theidmember to provide a service identifier or specify the URL directly as the annotation’s value. -
Create a
HealthTestclass to check that there is/healthendpoint (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
@MicronautTestso the Micronaut framework will initialize the application context and the embedded server. (For more information, see Micronaut Test.)2 Inject the
HttpClientbean and point it to the embedded server. -
Create a
GatewayControllerTestclass to test endpoints inside theGatewayControllerin 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
@MicronautTestso 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=true1 Set
micronaut.config-client.enabled: trueto read and resolve configuration from distributed sources.2 Set
kubernetes.client.secrets.enabled: trueto enable Kubernetes secrets as distributed source.3 Set
kubernetes.client.secrets.use-api: trueto use the Kubernetes API to fetch the configuration. -
Create a file named api/src/main/resources/application-dev.properties. This configuration file is applied only for the
devenvironment.(1) authentication-credentials.username=gdkdemo (2) authentication-credentials.password=example-password1 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
devenvironment:(1) micronaut.http.services.users.urls=http://localhost:8081 (2) micronaut.http.services.orders.urls=http://localhost:8082 (3) kubernetes.client.secrets.enabled=false1 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-password1 Username for the test environment.
2 Password for the test environment.
Run the tests:
Then open the file build/reports/tests/test/index.html in a browser to view the results.
3.3. Run the api Microservice
Run the api microservice as follows:
If you use Windows, run:
4. Test Integration Between Microservices
-
Run a
curlcommand to create a new user via the api microservice:curl -X "POST" "localhost/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
curlcommand to create a new order via the api microservice:curl -X "POST" "localhost/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
curlcommand to list the orders:curl "localhost/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 does not exist (with
id100). Run acurlcommand:curl -X "POST" "localhost/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: YWRtaW4= (6) password: bWljcm9uYXV0aXNhd2Vzb21l (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_rolerole to thegdk-serviceservice 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 users/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 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.4. Deploy the api 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 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
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-k8sNAME 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-k8sNAME 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
curlcommand to create a new user via the api microservice:curl -X "POST" "localhost/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
curlcommand to create a new order via the api microservice:curl -X "POST" "localhost/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
curlcommand to list the orders:curl "localhost/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 does not exist (with
id100). Run acurlcommand:curl -X "POST" "localhost/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.
