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 ともっと仲良くなれるといいですね!