Miha Jakovac

Logo

.NET & DevOps Engineer | Cloud Specialist | Team Enabler

My name is Miha and I've been tinkering with computers for some time now. I remember getting Pentium 100 in the late '90s and that's how it all started.

Specialities:

15 February 2022

Kubernetes Startup probe with .NET 6 - part 2

by Miha J.

In part 1, I showed you how to create a liveness probe. In part 2, I will talk about startup probes.

The startup probe is a probe that pings an /startup health check endpoint on a target application when it starts. To capture that information, I’ve developed a solution to create an IHosteService implementation that will control the IHostApplicationLifetime state. The service will be registered as a singleton and started last in the ConfigureServices method.

With the solution below, we are taking care of the /startup health check endpoint and the service lifetime actions, like OnShutdown, OnStarted and on OnStopped.

Here is the code:

using System.Threading;
using System.Threading.Tasks;
using Common.HealthChecks.AppEvents;
using Microsoft.Extensions.Hosting;

namespace Project.HealthChecks
{
   public enum ServiceState
   {
	  Shutdown,
      Stopped,
	  Started
   }
	
   public class ServiceStatus
   {
	  public ServiceState State = ServiceState.Stopped;
   }
	
   public class HostApplicationLifetimeEventsHostedService : IHostedService
   {
      private readonly IHostApplicationLifetime _hostApplicationLifetime;
      public ServiceStatus ServiceStatus { get; private set; }
      
      public HostApplicationLifetimeEventsHostedService(IHostApplicationLifetime hostApplicationLifetime)
      {
         _hostApplicationLifetime = hostApplicationLifetime;
         ServiceStatus = new ServiceStatus();
      }

      public Task StartAsync(CancellationToken cancellationToken)
      {
         _hostApplicationLifetime.ApplicationStarted.Register(OnStarted);
         _hostApplicationLifetime.ApplicationStopping.Register(OnShutdown);
         _hostApplicationLifetime.ApplicationStopped.Register(OnStopped);

         return Task.CompletedTask;
      }

      public Task StopAsync(CancellationToken cancellationToken)
         => Task.CompletedTask;

      private void OnShutdown()
      {
         for (var i = 5; i > 0; i--)
         {
            ServiceStatus.State = ServiceState.Shutdown;
            Thread.Sleep(1000);
         }
      }

      private void OnStopped()
      {
         ServiceStatus.State = ServiceState.Stopped;
      }

      private void OnStarted()
      {
         ServiceStatus.State = ServiceState.Started;
      }
   }
}

Now let’s create a custom Health Check:

public class StartupHealthCheck : IHealthCheck
{
    private readonly IServiceProvider _serviceProvider;

    public StartupHealthCheck(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
		
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context,
        CancellationToken cancellationToken = default)
    {
        var hostApplicationLifetimeEventsHostedService =
            _serviceProvider.GetService<HostApplicationLifetimeEventsHostedService>();
			
        HealthCheckResult result;
        if (hostApplicationLifetimeEventsHostedService.ServiceStatus.State == ServiceState.Started)
        {
            result = HealthCheckResult.Healthy();
        }
        else
        {
            result = HealthCheckResult.Unhealthy("Service not started.");
        }

        return result;
    }
}

Next, let’s add that custom Health Check to set up the startup health check in the service container:

using System;
using Common.HealthChecks.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;

namespace Project.HealthChecks
{
   public static partial class HealthCheckExtensions
   {
      public static IServiceCollection AddHesStartupHealthChecks(this IServiceCollection services)
      {
         services.AddHealthChecks()
            .AddCheck<StartupHealthCheck>("Service Startup",
               HealthStatus.Unhealthy,
               new[] { Startup },
               TimeSpan.FromSeconds(3));

         return services;
      }
   }
}

The code registers a health check that returns healthy status if ServiceStatus.State == ServiceState.Started;. We also set the tag of the health check to Startup, which is helpful for grouping health checks together when exposing a /startup endpoint.

The code below exposes the /startup endpoint on our service and checks all health checks with the Startup tag.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;

namespace Project.Extensions.HealthChecks;

public static class RegisterProbesExtensions
{
    public static void RegisterHealthCheckProbes(this IApplicationBuilder app)
    {
        app.UseHealthChecks("/startup",
         new HealthCheckOptions { Predicate = check => check.Tags.Contains(HealthCheckExtensions.Startup) });
    }
}

Now let’s register health check and HostApplicationLifetimeEventsHostedService as a singleton in the startup:

public void ConfigureServices(IServiceCollection services)
{
    ...
    services.AddHesStartupHealthChecks();
    services.AddSingleton<HostApplicationLifetimeEventsHostedService>();
    services.AddHostedService(p => p.GetRequiredService<HostApplicationLifetimeEventsHostedService>());
}

public void Configure(IApplicationBuilder app)
{
    ...      
    app.RegisterHealthCheckProbes();
    ...
}

Now you can run the application and go to /startup endpoint, which should return 200 and Healthy if HostApplicationLifetimeEventsHostedService started successfully.

tags: .NET, - net6, - c#, - kubernetes, - startup - probe