Configuring microservices running in Kubernetes

duration 15 minutes

Prerequisites:

Explore how to externalize configuration using MicroProfile Config and configure your microservices using Kubernetes ConfigMaps and Secrets.

What you’ll learn

You will learn how and why to externalize your microservice’s configuration. Externalized configuration is useful because configuration usually changes depending on your environment. You will also learn how to configure the environment by providing required values to your application using Kubernetes. Using environment variables allows for easier deployment to different environments.

MicroProfile Config provides useful annotations that you can use to inject configured values into your code. These values can come from any config sources, such as environment variables. To learn more about MicroProfile Config, read the Configuring microservices guide.

Furthermore, you’ll learn how to set these environment variables with ConfigMaps and Secrets. These resources are provided by Kubernetes and act as a data source for your environment variables. You can use a ConfigMap or Secret to set environment variables for any number of containers.

Additional prerequisites

Before you begin, you need a containerization software for building containers. Kubernetes supports various container runtimes. You will use Docker in this guide. For Docker installation instructions, refer to the official Docker documentation.

Use Docker Desktop, where a local Kubernetes environment is pre-installed and enabled. If you do not see the Kubernetes tab, then upgrade to the latest version of Docker Desktop.

Complete the setup for your operating system:

After you complete the Docker setup instructions for your operating system, ensure that Kubernetes (not Swarm) is selected as the orchestrator in Docker Preferences.

Use Docker Desktop, where a local Kubernetes environment is pre-installed and enabled. If you do not see the Kubernetes tab, then upgrade to the latest version of Docker Desktop.

Complete the setup for your operating system:

After you complete the Docker setup instructions for your operating system, ensure that Kubernetes (not Swarm) is selected as the orchestrator in Docker Preferences.

You will use Minikube as a single-node Kubernetes cluster that runs locally in a virtual machine. For Minikube installation instructions, see the Minikube documentation. Be sure to read the Requirements section, as different operating systems require different prerequisites to run Minikube.

Getting started

The fastest way to work through this guide is to clone the Git repository and use the projects that are provided inside:

git clone https://github.com/openliberty/guide-kubernetes-microprofile-config.git
cd guide-kubernetes-microprofile-config

The start directory contains the starting project that you will build upon.

The finish directory contains the finished project that you will build.

Starting and preparing your cluster for deployment

Start your Kubernetes cluster.

Start your Docker Desktop environment.

Run the following command from a command line:

minikube start

Next, validate that you have a healthy Kubernetes environment by running the following command from the command line.

kubectl get nodes

This command should return a Ready status for the master node.

You do not need to do any other step.

Run the following command to configure the Docker CLI to use Minikube’s Docker daemon. After you run this command, you will be able to interact with Minikube’s Docker daemon and build new images directly to it from your host machine:

eval $(minikube docker-env)

Deploying the microservices

The two microservices you will deploy are called system and inventory. The system microservice returns the JVM system properties of the running container and it returns the pod’s name in the HTTP header making replicas easy to distinguish from each other. The inventory microservice adds the properties from the system microservice to the inventory. This demonstrates how communication can be established between pods inside a cluster. To build these applications, navigate to the start directory and run the following command.

mvn clean package

Next, run the docker build commands to build container images for your application:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.

The -t flag in the docker build command allows the Docker image to be labeled (tagged) in the name[:tag] format. The tag for an image describes the specific image version. If the optional [:tag] tag is not specified, the latest tag is created by default.

Run the following command to deploy the necessary Kubernetes resources to serve the applications.

kubectl apply -f kubernetes.yaml

When this command finishes, wait for the pods to be in the Ready state. Run the following command to view the status of the pods.

kubectl get pods

When the pods are ready, the output shows 1/1 for READY and Running for STATUS.

NAME                                   READY     STATUS    RESTARTS   AGE
system-deployment-6bd97d9bf6-6d2cj     1/1       Running   0          34s
inventory-deployment-645767664f-7gnxf  1/1       Running   0          34s

After the pods are ready, you will make requests to your services.

The default host name for Docker Desktop is localhost.

The default host name for minikube is 192.168.99.100. Otherwise it can be found using the minikube ip command.

Navigate to http://[hostname]:31000/system/properties and use the user name bob and the password bobpwd to authenticate. Replace [hostname] with the IP address or host name of your Kubernetes cluster. Open your browser’s developer console and examine the response headers. Observe that the X-App-Name header currently has the value system.

Similarly, navigate to http://[hostname]:32000/inventory/systems/system-service and the system will be added to your inventory.

Modifying system microservice

The system service is hardcoded to have system as the app name. You’ll make this name configurable by adding the appName member and X-App-Name header.

Replace the SystemResource class.
system/src/main/java/io/openliberty/guides/system/SystemResource.java

SystemResource.java

 1package io.openliberty.guides.system;
 2
 3// CDI
 4import javax.enterprise.context.RequestScoped;
 5import javax.inject.Inject;
 6import javax.ws.rs.GET;
 7// JAX-RS
 8import javax.ws.rs.Path;
 9import javax.ws.rs.Produces;
10import javax.ws.rs.core.MediaType;
11import javax.ws.rs.core.Response;
12
13import org.eclipse.microprofile.config.inject.ConfigProperty;
14
15@RequestScoped
16@Path("/properties")
17public class SystemResource {
18
19  @Inject
20  @ConfigProperty(name = "APP_NAME")
21  private String appName;
22
23  @Inject
24  @ConfigProperty(name = "HOSTNAME")
25  private String hostname;
26
27  @GET
28  @Produces(MediaType.APPLICATION_JSON)
29  public Response getProperties() {
30    return Response.ok(System.getProperties())
31      .header("X-Pod-Name", hostname)
32      .header("X-App-Name", appName)
33      .build();
34  }
35}

The appName member was added to the header value in getProperties().

These changes use MicroProfile Config and CDI to inject the value of an environment variable called APP_NAME into the appName member of the SystemResource class.

Modifying inventory microservice

The inventory service is hardcoded to use bob and bobpwd as the credentials to authenticate against the system service. You’ll make these credentials configurable.

Replace the SystemClient class.
inventory/src/main/java/io/openliberty/guides/inventory/client/SystemClient.java

SystemClient.java

  1package io.openliberty.guides.inventory.client;
  2
  3import javax.enterprise.context.RequestScoped;
  4import javax.inject.Inject;
  5import javax.ws.rs.client.Client;
  6import javax.ws.rs.client.ClientBuilder;
  7import javax.ws.rs.client.Invocation.Builder;
  8import javax.ws.rs.core.HttpHeaders;
  9import javax.ws.rs.core.MediaType;
 10import javax.ws.rs.core.Response;
 11import javax.ws.rs.core.Response.Status;
 12
 13import org.eclipse.microprofile.config.inject.ConfigProperty;
 14
 15import java.util.Base64;
 16import java.util.Properties;
 17import java.net.URI;
 18
 19@RequestScoped
 20public class SystemClient {
 21
 22  // Constants for building URI to the system service.
 23  private final int DEFAULT_PORT = Integer.valueOf(System.getProperty("default.http.port"));
 24  private final String SYSTEM_PROPERTIES = "/system/properties";
 25  private final String PROTOCOL = "http";
 26
 27  // Basic Auth Credentials
 28  @Inject
 29  @ConfigProperty(name = "SYSTEM_APP_USERNAME")
 30  private String username;
 31
 32  @Inject
 33  @ConfigProperty(name = "SYSTEM_APP_PASSWORD")
 34  private String password;
 35
 36  // Wrapper function that gets properties
 37  public Properties getProperties(String hostname) {
 38    String url = buildUrl(PROTOCOL, hostname, DEFAULT_PORT, SYSTEM_PROPERTIES);
 39    Builder clientBuilder = buildClientBuilder(url);
 40    return getPropertiesHelper(clientBuilder);
 41  }
 42
 43  /**
 44   * Builds the URI string to the system service for a particular host.
 45   * @param protocol
 46   *          - http or https.
 47   * @param host
 48   *          - name of host.
 49   * @param port
 50   *          - port number.
 51   * @param path
 52   *          - Note that the path needs to start with a slash!!!
 53   * @return String representation of the URI to the system properties service.
 54   */
 55  protected String buildUrl(String protocol, String host, int port, String path) {
 56    try {
 57      URI uri = new URI(protocol, null, host, port, path, null, null);
 58      return uri.toString();
 59    } catch (Exception e) {
 60      System.err.println("Exception thrown while building the URL: " + e.getMessage());
 61      return null;
 62    }
 63  }
 64
 65  // Method that creates the client builder
 66  protected Builder buildClientBuilder(String urlString) {
 67    try {
 68      Client client = ClientBuilder.newClient();
 69      Builder builder = client.target(urlString).request();
 70      return builder
 71        .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
 72        .header(HttpHeaders.AUTHORIZATION, getAuthHeader());
 73    } catch (Exception e) {
 74      System.err.println("Exception thrown while building the client: " + e.getMessage());
 75      return null;
 76    }
 77  }
 78
 79  // Helper method that processes the request
 80  protected Properties getPropertiesHelper(Builder builder) {
 81    try {
 82      Response response = builder.get();
 83      if (response.getStatus() == Status.OK.getStatusCode()) {
 84        return response.readEntity(Properties.class);
 85      } else {
 86        System.err.println("Response Status is not OK.");
 87      }
 88    } catch (RuntimeException e) {
 89      System.err.println("Runtime exception: " + e.getMessage());
 90    } catch (Exception e) {
 91      System.err.println("Exception thrown while invoking the request: " + e.getMessage());
 92    }
 93    return null;
 94  }
 95
 96  private String getAuthHeader() {
 97    String usernamePassword = username + ":" + password;
 98    String encoded = Base64.getEncoder().encodeToString(usernamePassword.getBytes());
 99    return "Basic " + encoded;
100  }
101}

The changes introduced here use MicroProfile Config and CDI to inject the value of the environment variables SYSTEM_APP_USERNAME and SYSTEM_APP_PASSWORD into the SystemClient class.

Creating a ConfigMap and Secret

There are several ways to configure an environment variable in a Docker container. You can set it directly in the Dockerfile with the ENV command. You can also set it in your kubernetes.yaml file by specifying a name and a value for the environment variable you want to set for a specific container. With these options in mind, you’re going to use a ConfigMap and Secret to set these values. These are resources provided by Kubernetes that are used as a way to provide configuration values to your containers. A benefit is that they can be reused across many different containers, even if they all require different environment variables to be set with the same value.

Create a ConfigMap to configure the app name with the following kubectl command.

kubectl create configmap sys-app-name --from-literal name=my-system

This command deploys a ConfigMap named sys-app-name to your cluster. It has a key called name with a value of my-system. The --from-literal flag allows you to specify individual key-value pairs to store in this ConfigMap. Other available options, such as --from-file and --from-env-file, provide more versatility as to what you want to configure. Details about these options can be found in the Kubernetes CLI documentation.

Create a Secret to configure the credentials that inventory will use to authenticate against system with the following kubectl command.

kubectl create secret generic sys-app-credentials --from-literal username=bob --from-literal password=bobpwd

This command looks very similar to the command to create a ConfigMap, one difference is the word generic. It means you’re creating a Secret that is generic, in other words it stores information that isn’t specialized in any way. There are different types of secrets, such as secrets to store Docker credentials and secrets to store public and private key pairs.

A Secret is similar to a ConfigMap. A key difference is that a Secret is used for confidential information such as credentials. One of the main differences is that you must explicitly tell kubectl to show you the contents of a Secret. Additionally, when it does show you the information, it only shows you a Base64 encoded version so that a casual onlooker doesn’t accidentally see any sensitive data. Secrets don’t provide any encryption by default, that is something you’ll either need to do yourself or find an alternate option to configure.

kubernetes.yaml

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: system-deployment
 5  labels:
 6    app: system
 7spec:
 8  selector:
 9    matchLabels:
10      app: system
11  template:
12    metadata:
13      labels:
14        app: system
15    spec:
16      containers:
17      - name: system-container
18        image: system:1.0-SNAPSHOT
19        ports:
20        - containerPort: 9080
21        # Set the APP_NAME environment variable
22        env:
23        - name: APP_NAME
24          valueFrom:
25            configMapKeyRef:
26              name: sys-app-name
27              key: name
28---
29apiVersion: apps/v1
30kind: Deployment
31metadata:
32  name: inventory-deployment
33  labels:
34    app: inventory
35spec:
36  selector:
37    matchLabels:
38      app: inventory
39  template:
40    metadata:
41      labels:
42        app: inventory
43    spec:
44      containers:
45      - name: inventory-container
46        image: inventory:1.0-SNAPSHOT
47        ports:
48        - containerPort: 9080
49        # Set the SYSTEM_APP_USERNAME and SYSTEM_APP_PASSWORD environment variables
50        env:
51        - name: SYSTEM_APP_USERNAME
52          valueFrom:
53            secretKeyRef:
54              name: sys-app-credentials
55              key: username
56        - name: SYSTEM_APP_PASSWORD
57          valueFrom:
58            secretKeyRef:
59              name: sys-app-credentials
60              key: password
61---
62apiVersion: v1
63kind: Service
64metadata:
65  name: system-service
66spec:
67  type: NodePort
68  selector:
69    app: system
70  ports:
71  - protocol: TCP
72    port: 9080
73    targetPort: 9080
74    nodePort: 31000
75---
76apiVersion: v1
77kind: Service
78metadata:
79  name: inventory-service
80spec:
81  type: NodePort
82  selector:
83    app: inventory
84  ports:
85  - protocol: TCP
86    port: 9080
87    targetPort: 9080
88    nodePort: 32000

Dockerfile

1FROM open-liberty
2
3ADD --chown=1001:0 target/system.tar.gz /opt/ol

Updating Kubernetes resources

Next, you will update your Kubernetes deployments to set the environment variables in your containers based on the values configured in the ConfigMap and Secret created previously. The env section under the system-container is where the APP_NAME environment variable will be set. The env section under the inventory-container is where the SYSTEM_APP_USERNAME and SYSTEM_APP_PASSWORD environment variables will be set.

Replace the kubernetes file.
kubernetes.yaml

kubernetes.yaml

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: system-deployment
 5  labels:
 6    app: system
 7spec:
 8  selector:
 9    matchLabels:
10      app: system
11  template:
12    metadata:
13      labels:
14        app: system
15    spec:
16      containers:
17      - name: system-container
18        image: system:1.0-SNAPSHOT
19        ports:
20        - containerPort: 9080
21        # Set the APP_NAME environment variable
22        env:
23        - name: APP_NAME
24          valueFrom:
25            configMapKeyRef:
26              name: sys-app-name
27              key: name
28---
29apiVersion: apps/v1
30kind: Deployment
31metadata:
32  name: inventory-deployment
33  labels:
34    app: inventory
35spec:
36  selector:
37    matchLabels:
38      app: inventory
39  template:
40    metadata:
41      labels:
42        app: inventory
43    spec:
44      containers:
45      - name: inventory-container
46        image: inventory:1.0-SNAPSHOT
47        ports:
48        - containerPort: 9080
49        # Set the SYSTEM_APP_USERNAME and SYSTEM_APP_PASSWORD environment variables
50        env:
51        - name: SYSTEM_APP_USERNAME
52          valueFrom:
53            secretKeyRef:
54              name: sys-app-credentials
55              key: username
56        - name: SYSTEM_APP_PASSWORD
57          valueFrom:
58            secretKeyRef:
59              name: sys-app-credentials
60              key: password
61---
62apiVersion: v1
63kind: Service
64metadata:
65  name: system-service
66spec:
67  type: NodePort
68  selector:
69    app: system
70  ports:
71  - protocol: TCP
72    port: 9080
73    targetPort: 9080
74    nodePort: 31000
75---
76apiVersion: v1
77kind: Service
78metadata:
79  name: inventory-service
80spec:
81  type: NodePort
82  selector:
83    app: inventory
84  ports:
85  - protocol: TCP
86    port: 9080
87    targetPort: 9080
88    nodePort: 32000

In the kubernetes.yaml file where the containers are defined, you can see the valueFrom field that allows you to specify the value of an environment variable from various sources. These sources include a ConfigMap, a Secret, and information about the cluster. In this example configMapKeyRef gets the value name from the ConfigMap sys-app-name. Similarly, secretKeyRef gets the values username and password from the Secret sys-app-credentials.

Deploying your changes

Rebuild the application using mvn clean package.

mvn clean package

Run the docker build commands to rebuild container images for your application:

docker build -t system:1.0-SNAPSHOT system/.
docker build -t inventory:1.0-SNAPSHOT inventory/.

Run the following commands to deploy your changes to the Kubernetes cluster.

kubectl delete -f kubernetes.yaml
kubectl apply -f kubernetes.yaml

Navigate to http://[hostname]:31000/system/properties and examine the response headers in your developer console. You will see that the app name has changed from system to my-system. Verify that http://[hostname]:32000/inventory/systems/system-service is working as intended. If it is not, then check the configuration of the credentials.

Testing the microservices

Run the integration tests against a cluster running with appName my-system:

mvn verify -Ddockerfile.skip=true -Dsystem.appName=my-system

Run the integration tests against a cluster running at Minikube’s IP address and with appName my-system:

mvn verify -Ddockerfile.skip=true -Dcluster.ip=`minikube ip` -Dsystem.appName=my-system

The tests check that the system service responds with a header containing the configured name. The tests for inventory verify that the service may communicate with system using the configured credentials. If the credentials are misconfigured, then the inventory test will fail, so the inventory test indirectly verifies the credentials are correctly configured.

After the tests succeed, you should see output similar to the following in your console.

-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.system.SystemEndpointTest
Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.709 s - in it.io.openliberty.guides.system.SystemEndpointTest

Results:

Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
-------------------------------------------------------
 T E S T S
-------------------------------------------------------
Running it.io.openliberty.guides.inventory.InventoryEndpointTest
Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.696 s - in it.io.openliberty.guides.inventory.InventoryEndpointTest

Results:

Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

Tearing down the environment

Run the following commands to delete all the resources that you created.

kubectl delete -f kubernetes.yaml
kubectl delete configmap sys-app-name
kubectl delete secret sys-app-credentials

Nothing more needs to be done for Docker Desktop.

Perform the following steps to return your environment to a clean state.

  1. Point the Docker daemon back to your local machine:

    eval $(minikube docker-env -u)
  2. Stop your Minikube cluster:

    minikube stop
  3. Delete your cluster:

    minikube delete

Great work! You’re done!

You have used MicroProfile Config to externalize the configuration of two microservices, and then you configured them by creating a ConfigMap and Secret in your Kubernetes cluster.

Guide Attribution

Configuring microservices running in Kubernetes by Open Liberty is licensed under CC BY-ND 4.0

Copied to clipboard
Copy code block
Copy file contents

Prerequisites:

Nice work! Where to next?

What did you think of this guide?

Extreme Dislike Dislike Like Extreme Like

What could make this guide better?

Raise an issue to share feedback

Create a pull request to contribute to this guide

Need help?

Ask a question on Stack Overflow

Like Open Liberty? Star our repo on GitHub.

Star