銀の光と碧い空

クラウドなインフラとC#なアプリ開発の狭間にいるエンジニアの日々

ASP.NET Core 2.2のHealthchecks機能を試してみる

C# Advent CalendarとひとりAdvent Calendar 22日目です。

qiita.com

adventar.org

[追記]今日のエントリは昨日英語版C# Advent Calendarに投稿したエントリの日本語版です。

developers.redhat.com

ぜひ英語版C#アドベントカレンダーでも興味のあるエントリを読んでみてください。

crosscuttingconcerns.com

今回は、ASP.NET Core 2.2で導入されたHealthchecks機能をkubernetesのReadiness ProbeとLiveness ProbeといっしょにOpenShiftで試してみるという内容です。

Healthchecks 機能についてはPreview時点ですが、このブログに紹介があります。

blogs.msdn.microsoft.com

詳細は公式ドキュメントに説明されています。

docs.microsoft.com

[追記]日本語ではしばやんせんせーのブログをどうぞ。

blog.shibayan.jp

Healthchecks 機能は、アプリケーションのヘルスステータスを返すためのエンドポイントと、そのステータのチェック方法を定義するサービスに関するAPIを提供します。今迄だと、MVCなら適当なControllerを用意したりしていたと思いますが、それが専用になりました。アプリケーション固有のルーティングやUIから独立させることができ、より使いやすくなると思われます。

このHealthchecks機能、もちろんどんな環境でも使えます。OS問わず、セルフホストでもIISの上でも、ASP.NET Coreをサポートしている各種クラウドサービスの上で動きます。機能としては、レスポンスを返すだけなので、そのレスポンスを見てどう処理するかはHealthchecksを行う側の機能となります。kubernetesのLiveness ProbeとReadiness Probeともうまくあわせることができます。今回は、kubernetesの機能をそのまま使えるOpenShiftで試してみました。

Configure Liveness and Readiness Probes - Kubernetes

OpenShiftで.NET Core 2.2を有効にする方法はこちらをご覧ください。

developers.redhat.com

ASP.NET Core での実装

今回の動くサンプルコードはここにおいています。

github.com

kubernetesのLiveness ProbeとReadiness Probeはそれぞれ、Liveness Probeがコンテナの生存確認(=失敗したらコンテナ再起動)に、Readiness Probeがトラフィックの送信可否(=失敗したらトラフィックを送信しない)に使われます。Healthchecks機能は複数のHealthcheckのサービスを登録することができ、1つのエンドポイントに対し任意の数のHealthchecksのサービスを対応させることができます。今回はLivenessとReadiness用にそれぞれ1つずつサービスを作成しパスに登録することにします。その場合、Startup.csクラスを次のように記述します。

public void ConfigureServices(IServiceCollection services)
{
...
 
    services.AddHealthChecks()
            .AddCheck<LivenessHealthCheck>("Liveness", failureStatus: null)
            .AddCheck<ReadinessHealthCheck>("Readiness", failureStatus: null);
}
 
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
    app.UseHealthChecks("/health/live", new HealthCheckOptions()
    {
        Predicate = check => check.Name == "Liveness"
    });
 
    app.UseHealthChecks("/health/ready", new HealthCheckOptions()
    {
        Predicate = check => check.Name == "Readiness", 
    });
...
}

ConfigureServicesメソッドの中でHealthchecksのサービスを登録します。いくつかオーバーロードされたメソッドがあって、独自クラスでなくラムダ式で定義したりもできますが、今回は独自クラスであるLivenessHealthCheckReadinessHealthCheckを使っています。AddCheckメソッドでロジックを登録するときに第一引数で名前を指定しています。この名前は次のConfigureメソッドで使います。 Configureメソッドでは、Healthcheksを提供するエンドポイントと、そのエンドポイントに対してどのサービスを実行させるかをPredicateで指定します。ConfigureServicesメソッド内で登録したロジックのうち、Predicateでマッチしたロジックが実行されます。

ここでは、登録するときに使った名前でフィルタしていますが、公式ドキュメントのサンプルコードのようにタグを登録してフィルタに使うこともできます。1つのエンドポイントに複数のサービスを利用したい場合はタグの方が使いやすそうです。

using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Diagnostics.HealthChecks;

public void ConfigureServices(IServiceCollection services)
{
    services.AddHealthChecks()
        .AddCheck("Foo", () => 
            HealthCheckResult.Healthy("Foo is OK!"), tags: new[] { "foo_tag" })
        .AddCheck("Bar", () => 
            HealthCheckResult.Unhealthy("Bar is unhealthy!"), tags: new[] { "bar_tag" })
        .AddCheck("Baz", () => 
            HealthCheckResult.Healthy("Baz is OK!"), tags: new[] { "baz_tag" });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseHealthChecks("/health", new HealthCheckOptions()
    {
        // Filter out the 'Bar' health check. Only Foo and Baz execute.
        Predicate = (check) => check.Tags.Contains("foo_tag") || 
            check.Tags.Contains("baz_tag")
    });
}

Health checks in ASP.NET Core | Microsoft Docs

LivenessHealthCheckReadinessHealthCheckクラスは、今回はサンプルなので下のような単純なロジックを実装しています。ここはアプリの要件にあわせて変更してください。ステータスコードやレスポンスのコンテンツなど様々なカスタマイズが可能になっているので、ドキュメントを参考にしましょう。

internal class LivenessHealthCheck : IHealthCheck
{
    private HealthStatusData data;
 
    public LivenessHealthCheck(HealthStatusData data)
    {
        this.data = data;
    }
 
    public Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default(CancellationToken))
    {
        if (data.IsLiveness)
        {
            return Task.FromResult(HealthCheckResult.Healthy());
        }
        else
        {
            return Task.FromResult(HealthCheckResult.Unhealthy("Error"));
        }
    }
}

このサンプルではHealthStatusDataというのはDIによってinjectされますが、用意したボタンをclickするとfalseに設定できるようにしています。詳しくはサンプルコードを見てください。

f:id:tanaka733:20181217145946p:plain

ここまででASP.NET Core側の実装は完了です。

OpenShift (kubernetes)側での実装

サンプルアプリをそのままOpenShiftにデプロイする場合は、最初に紹介したリンクで.NET Core 2.2のイメージをインポートした後に次のコマンドを実行します。

$ oc new-project aspnetcore22
$ oc new-app --name=healthcheckexample 'dotnet:2.2~https://github.com/tanaka-takayoshi/HealthCheckExample.git' --build-env DOTNET_STARTUP_PROJECT=HealthCheckExample
$ oc expose svc/healthcheckexample

アプリがデプロイされたら、OpenShiftであればWeb ConsoleからProbeを設定できます。

f:id:tanaka733:20181217151026p:plain

Deployment ConfigのYAMLを直接編集することもできます。

spec:
...
  template:
  ...
    spec:
      containers:
        - image: >-
            ...
          livenessProbe:
            failureThreshold: 3
            httpGet:
              path: /health/live
              port: 8080
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          readinessProbe:
            failureThreshold: 3
            httpGet:
              path: /health/ready
              port: 8080
              scheme: HTTP
            periodSeconds: 10
            successThreshold: 1
            timeoutSeconds: 1
          ...

OpenShiftのダッシュボードでもProbeを確認できます。

f:id:tanaka733:20181217151145p:plain

それでは、用意したボタンからまずはReadiness Probe側をfalseに設定してみます。すると、ConsoleにReadiness checkに失敗したというメッセージが出て、oc get eventなどにも表示されます。

f:id:tanaka733:20181217151446p:plain

f:id:tanaka733:20181217151731p:plain

f:id:tanaka733:20181217151745p:plain

Readiness checkに失敗したPodにはルーティングがされないので、先程のURLにアクセスするとルーティングできないというエラーが表示されます。

f:id:tanaka733:20181217151839p:plain

これは1つのPodしかない状態でそのPodのReadiness checkが失敗しているためです。ためしにPodを2つにスケールさせると、追加した1つは正常にReadiness checkをパスするので、そちらにルーティングされます。

f:id:tanaka733:20181217152057p:plain

f:id:tanaka733:20181217152126p:plain

それでは、再度Podを1つにスケールダウンして戻して、今度はLiveness Probeをfalseに設定してみます。いったんUnhealthの表示がされますが、

f:id:tanaka733:20181217152257p:plain

ダッシュボードやoc get eventを見ると、Liveness probeの失敗が閾値を超えるとすぐに再起動がかかっているのがわかります。

f:id:tanaka733:20181217152309p:plain

f:id:tanaka733:20181217152321p:plain

今回はkubernetes (OpenShift)のProbeにあわせたHealthchecksの使い方を紹介しましたが、デプロイする環境にあわせたHealthchecksのロジックが組めるような仕組みになっていました。ぜひ使ってみましょう。