Saturday, April 2, 2022

Docker Compose Java Healthcheck

Oracle Java HealthCheck, Core Java, Oracle Java Exam Prep, Oracle Java Learning, Oracle Java Tutorial and Materials

Docker compose is often used to run locally a development stack. Even if I would recommend to use minikube/microk8s/…​ + Yupiik Bundlebee, it is a valid option to get started quickly.

One trick is to handle dependencies between services.

A compose descriptor often looks like:

docker-compose.yaml

version: "3.9" (1)

services: (2)

  postgres: (3)

    image: postgres:14.2-alpine

    restart: always

    ports:

      - "5432:5432"

    environment:

      POSTGRES_USERNAME: postgres

      POSTGRES_PASSWORD: postgres

  my-app-1: (4)

    image: my-app

    restart: always

    ports:

      - "18080:8080"

  my-app-2: (4)

    image: my-app

    restart: always

    depends_on: (5)

      - my-app-1

1. the descriptor version

2. the list of services (often containers if there is no replicas)

3. some external images (often databases or transversal services like gateways)

4. custom application images

5. dependencies between images

for web services it is not recommended having dependencies between services but it is insanely useful if you have a batch provisioning your database and you want it to run only when a web service is ready. It is often the case if you have a Kubernetes CronJob calling one of your Deployment/Service.

Previous descriptor works but it can happen the web service is not fully started before the second app (simulating a batch/job) is launched.

To solve that we need to add a healthcheck on the first app and depend on the state of the application in the batch. Most of the examples will use curl or wget but it has the drawback to be forced to add these dependencies – and their dependencies – to the base image – don’t forget we want the image to be light – a bit for the size but generally more for security reasons – so that it shouldn’t be there.

So the overall trick will be to write a custom main based on plain Java – since we already have a Java application.

Here is what can look like the modified docker-compose.yaml file:

"my-app-1:

        ...

        healthcheck: (1)

          test: [

            "CMD-SHELL", (2)

            "_JAVA_OPTIONS=", (3)

            "java", "-cp", "/opt/app/libs/my-jar-*.jar", (4)

            "com.app.health.HealthCheck", (5)

            "http://localhost:8080/api/health" (6)

          ]

          interval: 30s

          timeout: 10s

          retries: 5

          start_period: 5s

 

    my-app-2:

        ...

        depends_on:

          my-app-1:

            condition: service_healthy (7)

1. we register a healthcheck for the web service

2. we use CMD-SHELL and not CMD to be able to set environment variables in the command

3. we force the base image _JAVA_OPTION to be resetted to avoid to inherit the environment of the service (in particular if there is some debug option there)

4. we set the java command to use the jar containing our healthcheck main

5. we set the custom main we will write

6. we reference the local container health endpoint

7. on the batch service, we add the condition that the application must be service_healthy which means we control the state with the /health endpoint we have in the first application (and generally it is sufficient since initializations happen before it is deployed)

Now, the only remaining step is to write this main com.app.health.HealthCheck. Here is a trivial main class:

package com.app.health;

import java.io.IOException;

import java.net.URI;

import java.net.http.HttpClient;

import java.net.http.HttpRequest;

import static java.net.http.HttpResponse.BodyHandlers.discarding;

public final class HealthCheck {

    private HealthCheck() {

        // no-op

    }

    public static void main(final String... args)

        throws IOException, InterruptedException {

        final var builder = HttpRequest.newBuilder()

                .GET()

                .uri(URI.create(args[0]));

        for (int i = 1; i < 1 + (args.length - 1) / 2; i++) {

            final var base = 2 * (i - 1) + 1;

            builder.header(args[base], args[base + 1]);

        }

        final var response = HttpClient.newHttpClient()

            .send(builder.build(), discarding());

        if (response.statusCode() < 200 || response.statusCode() > 299) {

            throw new IllegalStateException("Invalid status: HTTP " + response.statusCode());

        }

    }

}

Nothing crazy there, we just do a GET request on the based on the args of the main. What is important to note there is you control that logic since you code the healthcheck so you can also check a file is present for example.

Last but not least you have to ensure the jar containing this class is in your docker image (generally the class can be included in a app-common.jar) which will enable to reference it as classpath in the healthcheck command.

Indeed you can use any dependency you want if you also add them in the classpath of the healthcheck, but generally just using the JDK is more than sufficient and enables a simpler healthcheck command.

you can also build a dedicated healthcheck-main.jar archive and add it in your docker to use it directly. This option enables to set in the jar the Main-Class which provides your the facility to use java -jar healthcheck-main.jar <url>

Source: javacodegeeks.com

Related Posts

0 comments:

Post a Comment