1. Overview

In this tutorial, we’re going to see how Spring Boot 2.3 integrates with Kubernetes probes to create an even more pleasant cloud-native experience.

First, we’ll start with a little bit of a background on Kubernetes probes. Then we’ll switch gears and see how Spring Boot 2.3 supports those probes.

2. Kubernetes Probes

When using Kubernetes as our orchestration platform, the kubelet in each node is responsible for keeping the pods in that node healthy.

For instance, sometimes our apps may need a little bit of time before being able to accept requests. The kubelet can make sure that the application receives requests only when it’s ready. Also, if the main process of a pod crashes for any reason, the kubelet will restart the container.

In order to fulfill these responsibilities, Kubernetes has two probes: liveness probes and readiness probes.

The kubelet will use the readiness probe to determine when the application is ready to accept requests. More specifically, a pod is ready when all of its containers are ready.

Similarly, the kubelet can check if a pod is still alive through liveness probes. Basically, the liveness probe helps the kubelet know when it should restart a container.

Now that we are familiar with the concepts, let’s see how the Spring Boot integration works.

3. Liveness and Readiness in Actuator

As of Spring Boot 2.3, LivenessStateHealthIndicator and ReadinessStateHealthIndicator classes will expose the liveness and readiness state of the application. When we deploy our application to Kubernetes, Spring Boot will automatically register these health indicators.

As a result, we can use /actuator/health/liveness and /actuator/health/readiness endpoints as our liveness and readiness probes, respectively.

For instance, we can add these to our pod definition to configure the liveness probe as an HTTP GET request:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 8080
    initialDelaySeconds: 3
    periodSeconds: 3

We’ll usually let Spring Boot decide when to stand up these probes for us. But, if we want to, we can enable them manually in our application.properties.

If we’re working with Spring Boot 2.3.0 or 2.3.1, we can enable the mentioned probes through a configuration property:

management.health.probes.enabled=true

However, since Spring Boot 2.3.2, this property is deprecated due to configuration confusion.

If we work with Spring Boot 2.3.2, we can use the new properties to enable liveness and readiness probes:

management.endpoint.health.probes.enabled=true
management.health.livenessState.enabled=true
management.health.readinessState.enabled=true

3.1. Readiness and Liveness State Transitions

Spring Boot uses two enums to encapsulate different readiness and liveness states. For readiness state, there is an enum called ReadinessState with the following values:

  • The ACCEPTING_TRAFFIC state represents that the application is ready to accept traffic
  • The REFUSING_TRAFFIC state means that the application is not willing to accept any requests yet

Similarly, the LivenessState enum represents the liveness state of the app with two values:

  • The CORRECT value means the application is running and its internal state is correct
  • On the other hand, the BROKEN value means the application is running with some fatal failures

Here’s how readiness and liveness state changes in terms of application lifecycle events in Spring:

  1. Registering listeners and initializers
  2. Preparing the Environment
  3. Preparing the ApplicationContext
  4. Loading bean definitions
  5. Changing the liveness state to CORRECT
  6. Calling the application and command-line runners
  7. Changing the readiness state to ACCEPTING_TRAFFIC

Once the application is up and running, we (and Spring itself) can change these states by publishing appropriate AvailabilityChangeEvents.

4. Managing the Application Availability

Application components can retrieve the current readiness and liveness state by injecting the ApplicationAvailability interface:

@Autowired private ApplicationAvailability applicationAvailability;

Then we can use it as follows:

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.CORRECT);
assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);
assertThat(applicationAvailability.getState(ReadinessState.class))
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);

4.1. Updating the Availability State

We can also update the application state by publishing an AvailabilityChangeEvent event:

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.CORRECT);
mockMvc.perform(get("/actuator/health/liveness"))
  .andExpect(status().isOk())
  .andExpect(jsonPath("$.status").value("UP"));

AvailabilityChangeEvent.publish(context, LivenessState.BROKEN);

assertThat(applicationAvailability.getLivenessState())
  .isEqualTo(LivenessState.BROKEN);
mockMvc.perform(get("/actuator/health/liveness"))
  .andExpect(status().isServiceUnavailable())
  .andExpect(jsonPath("$.status").value("DOWN"));

As shown above, before publishing any event, the /actuator/health/liveness endpoint returns a 200 OK response with the following JSON:

{
    "status": "OK"
}

Then after breaking the liveness state, the same endpoint returns a 503 service unavailable response with the following JSON:

{
    "status": "DOWN"
}

When we change to a readiness state of REFUSING_TRAFFIC, the status value will be OUT_OF_SERVICE:

assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.ACCEPTING_TRAFFIC);
mockMvc.perform(get("/actuator/health/readiness"))
  .andExpect(status().isOk())
  .andExpect(jsonPath("$.status").value("UP"));

AvailabilityChangeEvent.publish(context, ReadinessState.REFUSING_TRAFFIC);

assertThat(applicationAvailability.getReadinessState())
  .isEqualTo(ReadinessState.REFUSING_TRAFFIC);
mockMvc.perform(get("/actuator/health/readiness"))
  .andExpect(status().isServiceUnavailable())
  .andExpect(jsonPath("$.status").value("OUT_OF_SERVICE"));

4.2. Listening to a Change

We can register event listeners to be notified when an application availability state changes:

@Component
public class LivenessEventListener {
    
    @EventListener
    public void onEvent(AvailabilityChangeEvent<LivenessState> event) {
        switch (event.getState()) {
        case BROKEN:
            // notify others
            break;
        case CORRECT:
            // we're back
        }
    }
}

Here we’re listening to any change in application liveness state.

5. Auto-Configurations

Before wrapping up, let’s see how Spring Boot automatically configures these probes in Kubernetes deployments. The AvailabilityProbesAutoConfiguration class is responsible for registering the liveness and readiness probes conditionally.

As a matter of fact, there is a special condition there that registers the probes when one of the following is true:

When an application meets either of these conditions, the auto-configuration registers beans of  LivenessStateHealthIndicator and ReadinessStateHealthIndicator.

6. Conclusion

In this article, we saw how we can use Spring Boot provides two health probes for Kubernetes integration.

As usual, all the examples are available over on GitHub.