はじめに
こんにちは。
App Service、利用していますか?
Webアプリをデプロイするだけで動作するという、典型的なPaaSなのですが、運用をするとなると回復性の面で問題が発生します。App Serviceをデプロイした状態では、アプリに対する正常性確認が有効となっていないため、アプリが正常に応答していなくても内部ロードバランサーが正常でないアプリにリクエストを振り分け続けてしまう、という問題です。このような状況が発生すると、サービス正常性監視で問題が起こったり、特定のユーザのリクエストが正常でないインスタンスに振り分け続けられたりと、トラブルの種になりかねません。
そこで、今回試すのは「Health Check」です。
プレビューですが現在デプロイできるAzure環境上で直ぐに試すことができますので、こちらの機能を検証していきます。
ドキュメントはこちら:
https://github.com/projectkudu/kudu/wiki/Health-Check-(Preview)
Health Check の準備
Health Check は、アプリのインスタンス毎にHTTP正常性の確認を行い、応答が不正であればインスタンスを切り離す、という機能です。現在、検知の閾値を設定することはできませんが、5回に渡りHTTP Pingが成功しない状態が続いた場合には、リクエストの振り分け先から除外され、異常なインスタンスは自動的に再起動される、という挙動になります。HTTP Pingの成否は、特定のURIに対してGETリクエストを送り、2分以内に200番台のステータスコードで応答があるか、という基準で判定されます。
この機能を利用するには、Azure Resource Explorer( https://resources.azure.com/ )で対象のApp Serviceリソースを直接編集する必要があります。(現状では、プレビューの機能を利用するためにプレビューのツールを使用する、という些か面白いことになってます)
Resource Explorerにアクセスし、"subscriptions"->"対象のサブスクリプション"->"resourceGroups"->"対象のリソースグループ"->"providers"->"Microsoft.Web"->"sites"->"対象のAppService"->"config"とツリーを展開します。
configリソースのEditモードに入り、下記の"healthCheckPath"を書き換えます。既定ではnullになっていて、こちらに任意のパスを定義することでHealth Check機能が有効となります。
今回は、/statusとしました。
次に、アプリ側で/statusに対応するURIを実装します。こちらは、アプリが正常に動作している状態であるかをチェックし、問題が無ければ200ステータスコードを返すREST APIの実装です。内容については、アプリ毎に異なるのですが、一般的にはアプリが利用している外部サービス(Storage BlobやSQL Databaseなど)との疎通を確認して、正常であればOK、と言うようなAPIを実装します。
今回は検証ですので、PUTで戻り値となるステータスコードを指定できるようなSpring Bootアプリを実装しました。以下のようなサンプルコードです。
package com.example.springboot;
import java.net.InetAddress;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@RestController
@RequestMapping(value = "/status")
public class StatusController {
private static HttpStatus status = HttpStatus.OK;
@GetMapping
public ResponseEntity<String> get() {
return new ResponseEntity<String>(this.getLocalhost() + "/" + status.getReasonPhrase(), status);
}
@PutMapping
public ResponseEntity<String> put(@RequestParam(name="code", defaultValue = "200") Integer code) {
switch (code.intValue()) {
case 200:
status = HttpStatus.OK;
break;
case 500:
status = HttpStatus.INTERNAL_SERVER_ERROR;
break;
case 400:
status = HttpStatus.BAD_REQUEST;
break;
case 502:
status = HttpStatus.BAD_GATEWAY;
break;
default:
status = HttpStatus.OK;
break;
}
return new ResponseEntity<String>(this.getLocalhost() + "/" + status.getReasonPhrase(), status);
}
private String getLocalhost() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch(Exception e) {
e.printStackTrace();
return "0.0.0.0";
}
}
}
動作確認
ごにょごにょ(詳細割愛)っとして、App Serviceにアプリをデプロイした後、Chromeでアプリにアクセスします。
素っ気ないアプリですが、IPアドレス(172.16.1.5)が表示されています。こちらのIPアドレスは、App Serviceの各インスタンス毎の内部的なIPになるので、このセッションがどのインスタンスに接続しているかを特定するための情報となります。
今度はEdgeからアクセスしてみました。
アドレスだけ、Chromeで表示したものと異なっています。App Serviceの内部ロードバランサーはStikyに動作するので、バックエンドのインスタンスが複数ある場合でも原則として毎回同じインスタンスに接続しに行きます。(ちなみに、ロードバランサーの振り分けの重み付けはアクセス量が関係している様子で、異なるインスタンスに接続するためにChrome側で10回ほどリロードを行いました)
今度はPostmanを使用して、/statusにアクセスします。
200ステータスコードで返ってきているのが確認出来ました。こちらは、172.16.1.5のインスタンスに接続しています。
では、/statusからの応答を500ステータスコードが戻るように変更してみます。
この検証用アプリでは、/statusから返却されるHTTPステータスコードを動的に設定することで、アプリの異常が検知され、App Service側で異常なインスタンスの排除と再起動が行われることを期待しています。
10分程待って、Chrome側を更新してみます。
IPが172.16.1.2に変わっていて、どうやらインスタンスが再起動されたようです。
念のため、Postmanでも確認してみます。
同様に、172.16.1.2にアクセス先が変わりました。
このことから、Health Check機能が動作し、異常と判定されたインスタンスが切り離され、再起動されたことが確認出来ます。
Azure Portal側でも確認をしてみます。
オレンジの線が今回故意にHTTP Pingへの応答が異常となるように変更したインスタンスです。
メトリック上では、14:59から15:14ごろまで、インスタンスが応答していなかったことが判断できます。その後、自動的に再起動がかかったためにサービスが復旧しています。
Health Check機能はアプリ側での実装が必要となりますが、サービスの回復性を高めるために有効な機能ですね。
今回は基本的な挙動の確認だけにとどまりましたが、また時間を作って詳細な挙動を確認していきたいと思います。
以上、App ServiceでHealth Checkを試す、でした。