Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Docker healthcheck and distroless base image #762

Open
dconlan opened this issue Dec 2, 2024 · 1 comment
Open

Docker healthcheck and distroless base image #762

dconlan opened this issue Dec 2, 2024 · 1 comment

Comments

@dconlan
Copy link
Contributor

dconlan commented Dec 2, 2024

This project is currently used to generate the docker image for hapiproject/hapi

It uses gcr.io/distroless/java17-debian12:nonroot as its base.

It also defaults to exposing the actuator health end point to allow the status to be tested.

This is nice, but when you go looking for help with how to use these endpoints as the health check for the docker container you see posts which nearly always involve curl or wget. But the distroless image does not contain either of these utilities by design.

I would like to propose the inclusion of a special distroless health check class which would be included in the application jar and can be run as the health check command in the docker build file or in the docker compose file.

This would look something like this:

package ca.uhn.fhir.jpa.starter.util;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.ApplicationContext;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Map;

@ConditionalOnProperty(name = "loader.main", havingValue = "ca.uhn.fhir.jpa.starter.util.DistrolessHealthCheck")
@SpringBootConfiguration
public class DistrolessHealthCheck {

	private static final org.slf4j.Logger LOGGER = org.slf4j.LoggerFactory.getLogger(DistrolessHealthCheck.class);

	public static void main(String[] args) {
		SpringApplication application = new SpringApplication(DistrolessHealthCheck.class);
		application.setWebApplicationType(WebApplicationType.NONE);
		application.run(args);
	}

	@ConditionalOnProperty(name = "loader.main", havingValue = "ca.uhn.fhir.jpa.starter.util.DistrolessHealthCheck")
	@Component
	static class HealthCheck {
		private final ApplicationContext appContext;

		private final URI healthUri;


		public HealthCheck(
				@Autowired ApplicationContext appContext,
				@Value("${server.port:8080}") int defaultPort,
				@Value("${management.server.port:#{null}}") Integer managementPort,
				@Value("${management.ssl.enabled:false}") boolean sslEnabled,
				@Value("${management.context-path:/actuator}") String managementContextPath,
				@Value("${endpoints.health.path:/health}") String healthEndpoint,
				@Value("${health.check.uri:#{null}}") URI overrideURI
				) {
			this.appContext = appContext;
			if (overrideURI != null) {
				this.healthUri = overrideURI;
			}
			else {
				// try to workout the health endpoint url using properties
				this.healthUri = URI.create(
					(sslEnabled ? "https" : "http") + "://localhost:" +
						(managementPort == null ? defaultPort : managementPort) +
						managementContextPath + healthEndpoint);
			}
		}


		@EventListener(ApplicationReadyEvent.class)
		public void doSomethingAfterStartup() {
			System.exit(SpringApplication.exit(appContext, this::doHealthCheck));
		}

		public int doHealthCheck() {
			int exitCode = 1;
			LOGGER.info("Starting health check on port {}", healthUri);
			HttpClient client = HttpClient.newHttpClient();
			HttpRequest request = HttpRequest.newBuilder()
				.uri(healthUri)
				.header("accept", "application/json")
				.build();

			HttpResponse<String> response;
			try {
				response = client.send(request, HttpResponse.BodyHandlers.ofString());
				if (response.statusCode() == 200) {
					// health check may contain the status of multiple components.
					// we just want the main status
					ObjectMapper mapper = new ObjectMapper();
					try {
						Map<String, Object> responseMap = mapper.readValue(response.body(),
							new TypeReference<Map<String, Object>>() {});
						if (responseMap.containsKey("status") && responseMap.get("status").equals("UP")) {
							LOGGER.info("Health check passed");
							exitCode = 0;
						}
						else {
							LOGGER.error("Health check failed with status {}", responseMap.get("status"));
						}
					} catch (Throwable t) {
						LOGGER.error("Health check failed to parse response", t);
					}
				}
				else {
					LOGGER.error("Health check failed with status code {}", response.statusCode());
				}
			} catch (Throwable t) {
				LOGGER.error("Health check failed", t);
			}
			return exitCode;
		}
	}
}

This would try to use the same properties as the main application to determine the healthcheck endpoint and parse its response.

You would either add

HEALTHCHECK CMD ["java", "--class-path", "/app/main.war", "-Dloader.path=main.war!/WEB-INF/classes/,main.war!/WEB-INF/", "-Dloader.main=ca.uhn.fhir.jpa.starter.util.DistrolessHealthCheck", "org.springframework.boot.loader.PropertiesLauncher"]

to the main Docker build file, or give instruction for the docker compose to include

healthcheck:
          test: java --class-path /app/main.war -Dloader.path=main.war!/WEB-INF/classes/,main.war!/WEB-INF/ -Dloader.main=ca.uhn.fhir.jpa.starter.util.DistrolessHealthCheck org.springframework.boot.loader.PropertiesLauncher

I could supply a pull request with this code, but I'm not sure if people would want the health check in the base docker image and I don't know if there are other obvious issues with this solution.

What is the recommended way to add a health check to the hapi server docker image?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants