Azure Functions Proxies の廃止に対応する

Azure Functions のランタイムに V4 が登場したので、自分で運用しているプロダクトをアップデートしました。V3 からの移行は、「Azure Functions ランタイム バージョンの概要 | Microsoft Docs - 3.x から 4.x への移行」 に記載の通りごく簡単な設定変更で済むのですが、最大の壁は Functions Proxies 廃止への対応です。

Function Proxies 廃止にどう対応するか

「Azure Functions ランタイム バージョンの概要 | Microsoft Docs - 3.x と 4.x の間の破壊的変更」 には、さらっと API Management に移行せよと書かれていますが、全てのシナリオが API Management にフィットするとは思えません。この件については @shibayan をはじめ、Azure のエキスパート達で開発チームへ Issue や SR を通じて相談してみましたが、特に方針に変わりなく V4 が GA されてしまいました。。。この対応については、事前にもう少し開発者とディスカッションする期間が欲しかったです(少なくとも Azure Functions は一応 OSS なので)。

HTTP API Extensions for Azure Functions への移行

自分が運用しているプロダクトは、API のほとんどが同一ホストのフロントエンド用であり、ほんの一部のエンドポイントのみ外部向けに公開しているという構成です。このようなケースでは、基本的に API は App Service / Azure Functions での公開で十分要件は満たされるので、わざわざ API Management を追加する必要はないと思っています(リソースの管理が増える、証明書の管理も必要になる等)。これまでは、外部公開用のエンドポイントのみ、Functions Proxies に切り出して運用することで非常に簡易に外部 API を構成できていました。そんな Functions Proxies の手軽さを引き継いで引き続き Azure Functions でプロキシを使えるようにしたライブラリ HTTP API Extensions for Azure Functions が公開されていたので、早速試してみました。

このライブラリの開発経緯や仕組みなどは、リポジトリ作者のブログである 「しばやん雑記 - Azure Functions v4 で廃止された Azure Functions Proxies の代替ソリューションを実装した」 を参照してもらうとして、ここでは具体的なシナリオに沿った移行方法を紹介したいと思います(実際に本番環境で移行を成功させたパターンです)。

Functions V4 へのアップデートと NuGet パッケージの導入

前提として、この方法が使えるのは .NET ランタイムで作っている Azure Functions のみが対象となります。とはいっても Proxies だけの Function App には実体がなく proxies.json のみが入っているだけの構成なことが多いので、その場合は .NET ランタイムの Azure Functions を新規に作ってしまいましょう。

まず、Functions Proxies を使っていたプロジェクトを VS か VS Code で開きます。V3 で作っていた場合は、V4 + .NET6 の構成に変更してしまいます。その上で、NuGet で WebJobs.Extensions.HttpApi をインストールしましょう。このライブラリは頻繁に更新されているので、かならず最新バージョンを入れてください(2.0.3 以上を推奨)。

dotnet add package WebJobs.Extensions.HttpApi

以下のような csproj の構成になっていれば良いと思います。

<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<AzureFunctionsVersion>v4</AzureFunctionsVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Sdk.Functions" Version="4.0.*" />
<PackageReference Include="WebJobs.Extensions.HttpApi" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="local.settings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</None>
</ItemGroup>
</Project>

ポイントは以下の通りです。

  • .NET6 アプリである: TargetFrameworknet6.0 になっている
  • Azure Functions V4 である: <AzureFunctionsVersion>v4 であること
  • 最新の HTTP API Extensions for Azure Functions がインストールされている: WebJobs.Extensions.HttpApi2.0.3 以上であること

プロキシ関数を実装するためのクラスを準備する

V3 以前では、 proxies.json を定義することでプロキシを実装していましたが、 HTTP API Extensions ライブラリでは通常の Http Trigger Function を実装していきます。

VS Code の場合は以下のコマンドで関数用クラスを追加します。

func new Proxy

func コマンドが通らない場合は、Azure Functions Core Tools の操作 を参考に Azure Functions のローカル開発環境をセットアップしましょう。

コマンドで生成された Proxy クラス(クラス名は任意)を開いて、デフォルトで生成される関数自体は削除してしまいましょう。そして以下のように HTTP API Extensions を使えるようにします。

using System;

using Azure.WebJobs.Extensions.HttpApi; // HTTP API Extensions ライブラリ

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.Http;

namespace AltFuncProxies
{
public class Proxy : HttpFunctionBase // static を外し HttpFunctionBase を継承
{
// コンストラクタ
public Proxy(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor)
{
}

}
}

ここでのポイントは以下です。

  • Azure.WebJobs.Extensions.HttpApi を using する
  • Class から static を外す
  • HttpFunctionBase を継承する
  • IHttpContextAccessor を引数に取るコンストラクタを定義する

プロキシの実装

あとは、実際のプロキシをルートごとに関数として実装していきます。従来の proxies.json のルート定義ひとつが関数に対応するイメージです。

proxies.json で以下のような定義をしていた場合を例にします。

{
"$schema": "http://json.schemastore.org/proxies",
"proxies": {
"External Proxy": {
"matchCondition": {
"route": "/{path}",
"methods": ["GET", "POST"]
},
"backendUri": "https://xxxxxxxx.azurewebsites.net/api/external/{path}"
}
}
}

これは、Proxy を設定した Funcrtion App に /{path} で入ってきたリクエストを API の実体があるバックエンドホストの api/external/{path} にプロキシしている例です。これを HTTP API Extensions で置き換えると以下のような関数の実装になります。

[FunctionName(nameof(ExternalProxy))]
public IActionResult ExternalProxy([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "{*path}")] HttpRequest req, ILogger log)
{
return Proxy("https://xxxxxxxx.azurewebsites.net/api/external");
}

実装としては、バックエンド毎にこのような関数を作成すれば OK です。個人的には JSON で定義するより開発体験が良い気がしました。

プロキシで使う設定など

あとは、定義したいプロキシの要件に合わせて以下を追加設定していきます。

API のアクセス制御

HTTP API Extensions を使う場合は、通常の Azure Functions と同じように API Key を使ったアクセス制御が使えます。上記の例では、認証をバックエンドに任せているので、 AuthorizationLevelAnonymous にしていますが、通常の Azure Functions と同じように AuthorizationLevel.Function なども設定が可能です。バックエンドにアクセス制御の仕組みが入っていないがプロキシ側でアクセス制御を追加したいなどのニーズに対応できそうです。ここは以前の Functions Proxies より自由度が増して便利になったと思います。

Route Prefix の変更

プロキシを使う場合、Azure Functions のデフォルトである /api を付加しないルートを定義するケースも多いと思います。この場合は、 host.jsonroutePrefix を以下のように空白にすればコード側で個別にルートを定義できるようになります。

{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": ""
}
}
}

App Settings を使ったバックエンドのホスト定義

実際の運用では、dev/production などの環境別にバックエンドのホストを変えたいケースが多いと思います。その場合は、App Settings からホスト名を取得すれば良いのですが、 Functions Proxies の時とは異なり、これも通常の Functions と同じように環境変数から取得するようにします。上記のサンプルの Proxy 部分を以下のように変更して、実際のホスト名は App Settings (ローカルの場合は local.settings.json) にセットします。

[FunctionName(nameof(ExternalProxy))]
public IActionResult ExternalProxy([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "{*path}")] HttpRequest req)
{
var backendHost = Environment.GetEnvironmentVariable("EXTERNAL_APP_HOST_NAME");
return Proxy("https://" + backendHost + "/api/external");
}

現時点の制限

HTTP API Extensions による Proxy はまだ実装されたばかりなので、Functions Proxies の全ての機能はカバーしていないようです。検証した限りでは、 Request や Response のオーバーライド機能には対応していないようでした。ただし、ヘッダー自体はそのままプロキシされるので、オリジナルのリクエストにヘッダーをいれておけば問題ないと思います。

一時はどうなることと思った Azure Functions Proxies の廃止ですが、今回は HTTP API Extensions に救われました。このライブラリを OSS として開発してくれている @shibayan に感謝しつつ、何かあればフィードバックや GitHub Sponsors などを通じて開発をサポートしていきたいと思います。