Java / Maven

Kubernetes service discovery and distributed configuration

How to use Kubernetes service discovery and distributed configuration in a Micronaut application

Nemanja Mikic
On this guide
In this section

Getting Started

In this guide, we will create three microservices, build containerized versions, and deploy them with Kubernetes. We will use Kubernetes service discovery and distributed configuration to wire up our microservices.

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.

You will discover how the Micronaut framework eases Kubernetes integration.

What you will need

To complete this guide, you will need the following:

  • Some time on your hands

  • A decent text editor or IDE (e.g. IntelliJ IDEA)

  • JDK 17 or greater installed with JAVA_HOME configured appropriately

  • Docker.

  • Local Kubernetes cluster. We will use Minikube in this guide.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.

Writing the Apps

Let’s describe the microservices you will build through the guide.

  • users - This microservice contains customers data that can place orders on items, also a new customer can be created. Microservice requires Basic authentication to access it.

  • orders - This microservice contains all orders that customers have created as well as available items that customers can order. Also this microservice enables the creation of new orders. Microservice requires Basic authentication to access it.

  • api - This microservice acts as a gateway to the orders and users services. It combines results from both services and checks data when customers create a new order.

Initially we will hard-code the URLs of the orders and users services in the api service. Additionally, we will hard-code credentials (username and password) into every microservice configuration that are required for Basic authentication.

In the second part of this guide, we will use a Kubernetes discovery service and Kubernetes configuration maps to dynamically resolve the URLs of the orders and users microservices and get authentication credentials. The microservices call the Kubernetes API to register when they start up and then resolve placeholders inside the microservices' configurations.

Users Microservice

Create the users microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app          \
    --features=discovery-kubernetes,management,security,kubernetes,serialization-jackson,validation,graalvm \
    --build=maven             \
    --lang=java               \
    --jdk=21              \
    example.micronaut.users
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

If you use Micronaut Launch, select Micronaut Application as application type and add the yaml, discovery-kubernetes, management, security, serialization-jackson, kubernetes and graalvm features.

The previous command creates a directory named users containing Micronaut application with a package named example.micronaut.

Note
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.

Create a package named controllers and create a UsersController class to handle incoming HTTP requests for the users microservice:

users/java/src/main/java/example/micronaut/controllers/UsersController.java

Create a package named models where we will put our data beans.

The previous UsersController controller uses a User object to represent the customer. Create the User record

users/java/src/main/java/example/micronaut/models/User.java

Create a package named auth where you will check basic authentication credentials.

The Credentials class will load and store credentials (username and password) from a configuration file.

users/java/src/main/java/example/micronaut/auth/Credentials.java

The CredentialsChecker class, as the name suggests, will check if the provided credentials inside the HTTP request’s Authorization header are the same as those that are stored inside the Credentials class that we created above.

users/java/src/main/java/example/micronaut/auth/CredentialsChecker.java


Write tests to verify application logic

Create the UsersClient, a declarative Micronaut HTTP Client for testing:

users/java/src/test/java/example/micronaut/UsersClient.java

HealthTest checks that there is /health endpoint that is required for service discovery.

users/java/src/test/java/example/micronaut/HealthTest.java

UsersControllerTest tests endpoints inside the UserController.

users/java/src/test/java/example/micronaut/UsersControllerTest.java

Edit application.properties

users/src/main/resources/application.properties

Edit the bootstrap.properties file in the resources directory to enable distributed configuration. Change the default contents to the following:

users/src/main/resources/bootstrap.properties

Create src/main/resources/application-dev.properties. The Micronaut framework applies this configuration file only for the dev environment.

users/src/main/resources/application-dev.properties

Create a file named bootstrap-dev.properties to disable distributed configuration in the dev environment:

users/src/main/resources/bootstrap-dev.properties

Create a file named application-test.properties for use in the test environment:

users/src/test/resources/application-test.properties

Run the unit test:

users
./mvnw test


Running the application

Run the users microservice:

users
 MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8081

Orders Microservice

Create the orders microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app        \
  --features=discovery-kubernetes,management,security,kubernetes,serialization-jackson,validation,graalvm \
  --build=maven              \
  --lang=java                \
  --jdk=21               \
example.micronaut.orders
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

If you use Micronaut Launch, select Micronaut Application as application type and add the yaml, discovery-kubernetes, management, security, serialization-jackson, kubernetes and graalvm features.

The previous command creates a directory named orders containing a Micronaut application with a package named example.micronaut.

Note
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.

Create a package named controllers and create the OrdersController and ItemsController classes to handle incoming HTTP requests to the orders microservice:

orders/java/src/main/java/example/micronaut/controllers/OrdersController.java
orders/java/src/main/java/example/micronaut/controllers/ItemsController.java

Create a package named models where you will put your data beans.

The OrdersController and ItemsController classes uses Order and Item objects to represent customer orders. Create the Order record:

orders/java/src/main/java/example/micronaut/models/Order.java

Create the Item record:

orders/java/src/main/java/example/micronaut/models/Item.java

Create a package named auth where you will check basic authentication credentials.

The Credentials class will load and store credentials (username and password) from configuration files.

orders/java/src/main/java/example/micronaut/auth/Credentials.java

The CredentialsChecker class, as name suggests, will check if provided credentials inside an HTTP request’s Authorization header are the same as those that are stored inside Credentials class that we created above.

orders/java/src/main/java/example/micronaut/auth/CredentialsChecker.java


Write tests to verify application logic

Create the OrderItemClient, a declarative Micronaut HTTP Client for testing:

orders/java/src/test/java/example/micronaut/OrderItemClient.java

HealthTest checks that there is /health endpoint that is required for service discovery.

orders/java/src/test/java/example/micronaut/HealthTest.java

ItemsControllerTest tests endpoints inside the ItemController.

orders/java/src/test/java/example/micronaut/ItemsControllerTest.java

OrdersControllerTest tests endpoints inside the OrdersController.

orders/java/src/test/java/example/micronaut/OrdersControllerTest.java

Edit application.properties so it contains:

orders/src/main/resources/application.properties

Edit bootstrap.properties file in the resources directory to enable distributed configuration. Change it to the following:

orders/src/main/resources/bootstrap.properties

Create src/main/resources/application-dev.properties. The Micronaut framework applies this configuration file only for the dev environment.

orders/src/main/resources/application-dev.properties

Create a file named bootstrap-dev.properties to disable distributed configuration in the dev environment:

orders/src/main/resources/bootstrap-dev.properties

Create a file named application-test.properties to be used in the test environment:

orders/src/test/resources/application-test.properties

Run the unit test:

orders
./mvnw test


Running the application

Run the orders microservice:

orders
MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8082

API (Gateway) Microservice

Create the api microservice using the Micronaut Command Line Interface or with Micronaut Launch.

mn create-app         \
   --features=discovery-kubernetes,management,kubernetes,serialization-jackson,http-client,mockito,graalvm \
   --build=maven           \
   --lang=java             \
   --jdk=21            \
    example.micronaut.api
Note
If you don’t specify the --build argument, Gradle with the Kotlin DSL is used as the build tool.
If you don’t specify the --lang argument, Java is used as the language.
If you don’t specify the --test argument, JUnit is used for Java and Kotlin, and Spock is used for Groovy.

If you use Micronaut Launch, select Micronaut Application as application type and add the yaml, discovery-kubernetes, management, kubernetes, serialization-jackson, mockito, graalvm and http-client features.

The previous command creates a directory named api containing a Micronaut application with a package named example.micronaut.

Note
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features, and apply those changes to your application.

Create a package named controllers and create a GatewayController class to handle incoming HTTP requests to the api microservice:

api/java/src/main/java/example/micronaut/controllers/GatewayController.java

Create a package named models where you will put your data beans.

The GatewayController and ItemsController classes use User, Order, and Item to represent customer orders. Create the User record:

api/java/src/main/java/example/micronaut/models/User.java

Create the Order record:

api/java/src/main/java/example/micronaut/models/Order.java

Create the Item record:

api/java/src/main/java/example/micronaut/models/Item.java

Create a package named clients where you will put the HTTP Clients to call the users and orders microservices.

Create a UsersClient for the users microservice.

api/java/src/main/java/example/micronaut/clients/UsersClient.java

Create an OrdersClient for the orders microservice.

api/java/src/main/java/example/micronaut/clients/OrdersClient.java

Create a package named auth where we will check basic authentication credentials.

Create a Credentials class that will load the username and password from configuration that will be needed for comparison.

api/java/src/main/java/example/micronaut/auth/Credentials.java

Create an AuthClientFilter class that is a client filter applied to every client. It adds basic authentication header with credentials that are stored in the Credentials class.

api/java/src/main/java/example/micronaut/auth/AuthClientFilter.java
package example.micronaut.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 a class named ErrorExceptionHandler in the example.micronaut package. ErrorExceptionHandler will propagate errors from the orders and users microservices.

api/java/src/main/java/example/micronaut/ErrorExceptionHandler.java


Write tests to verify application logic

Create a GatewayClient, a declarative Micronaut HTTP Client for testing:

api/java/src/test/java/example/micronaut/GatewayClient.java

HealthTest checks that there is /health endpoint that is required for service discovery.

api/java/src/test/java/example/micronaut/HealthTest.java

GatewayControllerTest tests endpoints inside the GatewayController.

api/java/src/test/java/example/micronaut/GatewayControllerTest.java

Edit application.properties

api/src/main/resources/application.properties

Edit the bootstrap.properties file in the resources directory to enable distributed configuration so it looks like the following:

api/src/main/resources/bootstrap.properties

Create src/main/resources/application-dev.properties. The Micronaut framework applies this configuration file only for the dev environment.

api/src/main/resources/application-dev.properties

Create a file named bootstrap-dev.properties to disable distributed configuration in the dev environment:

api/src/main/resources/bootstrap-dev.properties

Create a file named application-test.properties to be used in the test environment:

api/src/test/resources/application-test.properties

Run the unit test:

api
./mvnw test


Running the application

Run api microservice:

api
MICRONAUT_ENVIRONMENTS=dev ./mvnw mn:run
14:28:34.034 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 499ms. Server Running: http://localhost:8080

Test integration between applications

Store the URL of the api microservice in the API_URL environment variable.

export API_URL=http://localhost:8080

Run a cURL command to create a new user via the api microservice:

curl -X "POST" "$API_URL/api/users" -H 'Content-Type: application/json; charset=utf-8' -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
{"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" "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8' -d '{ "user_id": 1, "item_ids": [1,2] }'
{"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 created orders:

curl "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8'
[{"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}]

We can try to place an order for a user who doesn’t exist (with id 100). Run a cURL command:

curl -X "POST" "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8' -d '{ "user_id": 100, "item_ids": [1,2] }'
{"message":"Bad Request","_links":{"self":[{"href":"/api/orders","templated":false}]},"_embedded":{"errors":[{"message":"User with id 100 doesn't exist"}]}}

Kubernetes and the Micronaut framework

In this chapter we will first create the necessary Kubernetes resources for our microservices that will make them work properly then we will configure build container images and deploy each of the microservices that we created on the local Kubernetes cluster.

Create a file named auth.yml that will configure the service role for microservices that have secret configurations.

auth.yml

Run the next command to create the resources described above:

kubectl apply -f auth.yml

Before we start deploying each service, ensure that Docker daemon is configured to use Kubernetes. If you are using Minikube run the next command to switch the docker daemon to use Minikube.

eval $(minikube docker-env)

Users Microservice

Build a Docker image of the users service with the name users.

Edit the file named k8s.yml inside the users microservice.

/users/k8s.yml

Run the next command to create the resources described above:

kubectl apply -f users/k8s.yml
deployment.apps/users created
service/users created

Orders Microservice

Build a Docker image of the orders service with the name orders.

Edit the file named k8s.yml inside the orders microservice.

/orders/k8s.yml

Run the next command to create the resources described above:

kubectl apply -f orders/k8s.yml

API (Gateway) Microservice

Build a Docker image of the api service with the name api.

Edit the file named k8s.yml inside the api microservice.

/api/k8s.yml

Run the next command to create the resources described above:

kubectl apply -f api/k8s.yml

Test integration between applications deployed on 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=micronaut-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=micronaut-k8s

Minikube

For Minikube the output should be similar to the following:

NAME     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
api      LoadBalancer   10.110.42.201    <pending>     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
Note
By default, the EXTERNAL-IP address of the LoadBalancer service inside Minikube will be in the <pending> state. If you want to assign an external IP, you have to run the minikube tunnel command.

Run the next command to retrieve the URL of the api microservice:

export API_URL=$(minikube service api -n=micronaut-k8s --url)

Docker Desktop

For Docker Desktop’s Kubernetes integration the output should be similar to the following. Notice the external-ip is localhost:

NAME     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
api      LoadBalancer   10.108.205.248   localhost     8080:31516/TCP   9m23s
orders   NodePort       10.98.120.224    <none>        8080:31566/TCP   9m39s
users    NodePort       10.109.155.86    <none>        8080:30545/TCP   10m

So for Docker Desktop the API_URL should be set to http://localhost:8080.

Run a cURL command to create a new user via the api microservice:

curl -X "POST" "$API_URL/api/users" -H 'Content-Type: application/json; charset=utf-8' -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
{"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" "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8' -d '{ "user_id": 1, "item_ids": [1,2] }'
{"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 created orders:

curl "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8'
[{"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}]

We can try to place an order for a user who doesn’t exist (with id 100). Run a cURL command:

curl -X "POST" "$API_URL/api/orders" -H 'Content-Type: application/json; charset=utf-8' -d '{ "user_id": 100, "item_ids": [1,2] }'
{"message":"Bad Request","_links":{"self":[{"href":"/api/orders","templated":false}]},"_embedded":{"errors":[{"message":"User with id 100 doesn't exist"}]}}

Cleaning Up

To delete all resources that were created in this guide, run the next command.

kubectl delete namespaces micronaut-k8s

Next Steps

Read more about Kubernetes.

Read more about Micronaut Kubernetes module.

License

Note
All guides are released with an Apache License 2.0 for the code and a Creative Commons Attribution 4.0 license for the writing and media (images).