xin9le.net

Microsoft の製品/技術が大好きな Microsoft MVP な管理人の技術ブログです。

Azure App Service の Always On リクエストにのみ応答する

Azure App Service を使っている場合、特に本番環境では Always On を有効化することになると思います。日本語の Azure Portal だと「常時接続」と表記されるもので、一定間隔でホストしている Web アプリに対してリクエストを投げることで、アプリがアイドル状態にならないようにするものです。Cold Start になると初速が出ないので、その対策に使われるものですね。

この Always On 設定を有効化していると、Azure App Service が定期的に Root URL (= /) に対して GET メソッドでアクセスしてきます。「それが何だよ」って話なのですが、Web アプリケーションの作りに依っては 404 (Not Found) を返してエラーとして検知してしまうことがあります。

UI のある Web サービスであれば GET / がメインの Landing Page になるため特段問題にならないのですが、Web API なサービスをホストしている場合には API 定義として GET / でアクセス可能な Endpoint を用意していないことが結構よくあるんですよね。ってゆーか用意しないですよね、ほぼ!

Always On は有効にしたいけど 404 は検知したくない!だって気持ち悪いもん!

となるわけです。なりませんか?w

どう対策するか

ということで 200 OK を返すだけの GET / な Endpoint を用意すればよいですね。以上終了。

とはならないです。エラーを回避するためだけに実際の運用要件として不要な GET / な API を公開するのは好ましくありません。特段問題にもなりにくいですが、不要なものを workaround として用意するのは気持ち悪いです。なので、可能であれば Azure App Service から飛んでくる Always On リクエストにのみ 200 OK で応答したい です。

そこで Always On リクエストについて調べてみると、かなり特殊なアクセスのされ方をしていることが分かりました。下記記事に詳細があるので引用します。

REQUEST_URI     = /
REQUEST_METHOD  = GET
SERVER_PROTOCOL = HTTP/1.1
REMOTE_ADDR     = ::1
REMOTE_PORT     = 21353
REMOTE_HOST     = ::1
HTTP_REFERER    =
HTTP_USER_AGENT = AlwaysOn
HTTP_CONNECTION = Keep-Alive

このうち以下であるかどうかを見分けられれば良さそうですね。

  • GET メソッドでのリクエスト
  • アクセス URL は /
  • リクエスト元が自分自身 (= Loopback アドレス)
  • User-Agent が AlwaysOn

実装してみる

Always On リクエストに対しては 200 OK を返すだけなので、Request Pipeline の早い段階で応答してしまうのが効率がよさそうです。ということで Middleware を実装しましょう。ASP.NET Core Middleware 自体やカスタム Middleware の作り方については公式ドキュメントをご覧ください。

internal sealed class AzureAppServiceAlwaysOnResponseMiddleware
{
    private RequestDelegate Next { get; }

    public AzureAppServiceAlwaysOnResponseMiddleware(RequestDelegate next)
        => this.Next = next;

    public async Task InvokeAsync(HttpContext http)
    {
        // Always On リクエストなら 200 OK を返す
        if (isAlwaysOn(http))
        {
            http.Response.StatusCode = (int)HttpStatusCode.OK;
            return;
        }

        // それ以外は Request Pipeline を継続
        await this.Next(http);

        // ローカル関数
        static bool isAlwaysOn(HttpContext http)
        {
            // アクセス元 IP を取得
            var ip = http.Connection.RemoteIpAddress;
            if (ip is null)
                return false;

            // Loopback アドレスか
            if (!IPAddress.IsLoopback(ip))
                return false;

            // GET でアクセスされているか
            var request = http.Request;
            if (!HttpMethods.IsGet(request.Method))
                return false;

            // 「/」へのリクエストか
            var path = request.Path;
            if (!path.HasValue)
                return false;

            const StringComparison comparison = StringComparison.Ordinal;
            if (!path.Value.AsSpan().Equals("/", comparison))
                return false;

            // User-Agent が「AlwaysOn」であるか
            foreach (var ua in request.Headers.UserAgent)
            {
                if (ua.AsSpan().Equals("AlwaysOn", comparison))
                    return true;  // カカロット、お前が Always On だ
            }

            return false;
        }
    }
}
public static class IApplicationBuilderExtensions
{
    public static IApplicationBuilder UseAzureAppServiceAlwaysOnResponse(this IApplicationBuilder builder)
        => builder.UseMiddleware<AzureAppServiceAlwaysOnResponseMiddleware>();
}

上記の実装ができたら、例えば以下のように Request Pipeline のそこそこ早い段階に差し込みます。これで完成です!

// Configure application
var builder = WebApplication.CreateBuilder(args);
builder.Host.ConfigureServices(static (context, services) =>
{
    services.AddControllers();
});

// Configure HTTP pipelines
var app = builder.Build();
app.UseHsts();
app.UseHttpsRedirection();
app.UseAzureAppServiceAlwaysOnResponse();  // 例えばこの辺とか
app.UseEndpoints(static endpoints =>
{
    endpoints.MapControllers();
});

// Run application
app.Run();

まとめ

いかがでしたか?今回は Azure App Service の Always On にのみ応答する ASP.NET Core Middleware を実装してみました。Azure App Service ともっと仲良くなれるといいですね!