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());
}
}
}
0 comments:
Post a Comment