.NET Worker Service 便利ですよね。 Generic Host が最高という話でもある。
この記事では dotnet new worker
コマンドを叩いて作成したテンプレのような、Generic Host を用いた console application における health check のお話をしたいと思います。
web application であれば /healthz
のような health check 用のエンドポイントを生やしてあげれば良いだけなんですけどね、console application だと別のアプローチを取る必要があったりします。
基本方針
web application であれば health check 用の WebAPI を生やして、health check する側がそのエンドポイントを叩くような形になります。
しかしながら先ほども書いた通り dotnet new worker
コマンドを叩いて作成したテンプレは console application ですから、Web API を生やすぞとはなりません。
なのでどうやって health check するべきなのだろうか、とか思うわけです。 Kubernetes を使っている場合、health check はどうするかというと livenessProbe を使うわけですが、公式ドキュメントにピンポイントな事が書いてあります。
livenessProbe: exec: command: - cat - /tmp/healthy initialDelaySeconds: 5 periodSeconds: 5
なるほど、healthy だったら何かファイルを作っておき、unhealthy だったらそのファイルが存在しないようにすれば良さそう。
問題はそれをどう実現するかなのですが、それには Microsoft.Extensions.Diagnostics.HealthChecks
を使っていきます。
Microsoft.Extensions.Diagnostics.HealthChecks
Generic Host には簡単に health check の仕組みを導入する事ができます。
そのためのパッケージが Microsoft.Extensions.Diagnostics.HealthChecks
です。
このパッケージは dotnet new webapi
などで作る web 系のテンプレートにはデフォルトで組み込まれていますが、dotnet new worker
で作成する worker テンプレートには組み込まれていないので、追加しましょう。
dotnet add package Microsoft.Extensions.Diagnostics.HealthChecks
Microsoft.Extensions.Diagnostics.HealthChecks
を用いた Generic Host における health check についてはここでは深く触れませんが、オススメのドキュメント等は以下の通り。
health check では背後にあるミドルウェアに対して疎通確認をします。 よく用いられるサービスについては AspNetCore.Diagnostics.HealthChecks が各ミドルウェア用にパッケージを用意してくれているので、それを用いると良いでしょう。今回の例では、PostgreSQL と Redis を使っていたとしましょう。
// 使ってるサービス等に合わせてパッケージを追加 dotnet add package AspNetCore.HealthChecks.Npgsql dotnet add package AspNetCore.HealthChecks.Redis
これらのパッケージを追加する事で、以下のようなコードが書けるようになります。
var host = Host.CreateDefaultBuilder(args) .ConfigureServices(static (context, services) => { services.AddHostedService<Worker>(); // Npgsql や StackExchange.Redis の構成は省略 services.AddHealthChecks() .AddNpgSql(serviceProvider => serviceProvider.GetRequiredService<NpgsqlDataSource>()) .AddRedis(serviceProvider => serviceProvider.GetRequiredService<IConnectionMultiplexer>()); }) .Build(); host.Run();
これで health check 用のサービスが追加されましたが、これだけだと何の意味もありません。
web application であれば、app.UseHealthChecks("/healthz")
とかを http pipeline に追加する事で /healthz
に HTTP リクエストがあった際に登録した health check が走り、その結果をリスポンスにのせるわけですが、今回の場合は console application なので同様の手段はとれません。
healthy だったら /tmp/healthy
みたいなファイルが存在するような状態を作りたいしたいわけですが、どうすればいいのでしょう?
答えは health check を内部で定期実行し、その結果に応じて /tmp/healthy
みたいなファイルを作ったり消したりするようにすれば良いわけです。
Microsoft.Extensions.Diagnostics.HealthChecks
はそれを実現するための機能が備わっています。
それが IHealthCheckPublisher
です。
IHealthCheckPublisher
使い方は簡単。
IHealthCheckPublisher
を実装したクラスと、そのオプションである HealthCheckPublisherOptions
を設定すればいいだけです。
HealthCheckPublisherOptions
では、どの程度の周期で health check を行い、それを publish するかを設定します。
var host = Host.CreateDefaultBuilder(args) .ConfigureServices(static (context, services) => { services.AddHostedService<Worker>(); // Npgsql や StackExchange.Redis の構成は省略 services.AddHealthChecks() .AddNpgSql(serviceProvider => serviceProvider.GetRequiredService<NpgsqlDataSource>()) .AddRedis(serviceProvider => serviceProvider.GetRequiredService<IConnectionMultiplexer>()); services.Configure<HealthCheckPublisherOptions>(options => { // 初回の health check / publish を起動から 5 秒遅延させる options.Delay = TimeSpan.FromSeconds(5); // 30 秒毎に health check / publish をする options.Period = TimeSpan.FromSeconds(30); }); // HealthCheckPublisherOptions により定期的に health check が走り // その結果が IHealthCheckPublisher.PublishAsync(...) に渡される services.AddSingleton<IHealthCheckPublisher, HealthCheckPublisher>(); }) .Build(); host.Run();
IHealthCheckPublisher
を実装した HealthCheckPublisher
はこんな感じ。
PublishAsync
が定期的に呼ばれる事になります。
internal class HealthCheckPublisher : IHealthCheckPublisher, IDisposable { private readonly string _path = "/tmp/healthy"; public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) { if (report.Status is not HealthStatus.Healthy) { if (File.Exists(_path)) { File.Delete(_path); } return Task.CompletedTask; } if (!File.Exists(_path)) { using var _ = File.Create(_path); } return Task.CompletedTask; } public void Dispose() { if (File.Exists(_path)) { File.Delete(_path); } } }
これで「application が healthy だったら何かファイルを作っておき、unhealthy だったらそのファイルが存在しないようする」という状況を作り出すことができました。
そのため Kubernetes の livenessProbe で定期的に /tmp/healthy
を cat して health check する、みたいな事ができるようになりました。
まとめ
dotnet new worker
コマンドを叩いて作成したテンプレのような Generic Host を用いた console application で health check がしたければ Microsoft.Extensions.Diagnostics.HealthChecks
パッケージを追加し、IHealthCheckPublisher
を実装したクラスと、そのオプションである HealthCheckPublisherOptions
を設定しましょう。
そうすることで health check を application 自身が定期的に実行し、その結果を IHealthCheckPublisher.PublishAsync(...)
に渡してくれるようになるので、実装次第で様々な事が実現できます。
今回の例では Kubernetes の livenessProbe で cat を用いた health check をするために、IHealthCheckPublisher.PublishAsync(...)
内で HealthReport.Status
が healthy ならファイルが存在し、unhealthy ならファイル存在しなようにする実装をしましたが、HealthReport
の結果を別のサービスに HTTP で投げるといった事も実現できます。