ROMANCE DAWN for the new world

Microsoft Azure を中心とした技術情報を書いています。

Azure Container Apps Preview の Reverse Proxy に YARP を使ってみた

この記事は、NEXTSCAPE Advent Calendar 2021 の 1 日目 の記事です。
qiita.com

先日の Microsoft Ignite で発表された Azure Container Apps で、Reverse Proxy に YARP を使ってみました。
ショッピングサイトのマイクロサービスを想定して、Item Service、Pos Service、Stock Service を API Gateway パターンで公開するシナリオで試したいと思います。

Azure Container Apps における Reverse Proxy の必要性

Azure Container Apps は、サーバーレスなプラットフォームであり、複数のコンテナアプリケーションで構成されるマイクロサービスの構築を支援するサービスです。
HTTP エンドポイントを公開するすべてのマイクロサービスで external な Ingress を構成することは考えにくく、一般的には external な Reverse Proxy を経由させてバックエンドにある internal なマイクロサービスを公開する API Gateway パターンが多いのではないかと思います。

先日の Hack Azure にお邪魔して、このほかにも Container Apps の使いドコロをいろいろと話してきましたので、よかったらご覧ください。

www.youtube.com

YARP とは

YARP ( Yet Another Reverse Proxy ) は、.NET で作られた Reverse Proxy です。
Reverse Proxy や API Gateway には Nginx や Ocelot などのたくさんのソリューションがあります。YARP は ASP.NET Core に統合されているため、C# を使うエンジニアが要件に合わせたカスタマイズや調整を簡単にできるところが嬉しいです。Envoy よりもパフォーマンスが速いという測定結果もでています。
devblogs.microsoft.com

Container Apps Environment を作成する

まずは、Bicep を使って、App Service Plan にあたる Environment を作成します。
詳細は、こちらの記事を参照してください。
gooner.hateblo.jp

マイクロサービスを作成する

マイクロサービスを ASP.NET Core Web API で作成します。Item Service の API を実装します。API の実装は今回の本題ではないので割愛し、ポイントを絞って説明します。

Reverse Proxy を経由して呼び出すため、https://localhost:xxx/item/api/items のように URL に PathBase (item) が含まれることへの対応が必要です。おもに、Swagger UI への対応となりますが、X-Forwarded-Host ヘッダーと PathBase を使って構成を変更しています。

var pathBase = builder.Configuration.GetValue<string>("PathBase");
if (!String.IsNullOrEmpty(pathBase))
{
    app.UsePathBase(pathBase);
}

app.UseSwagger(c =>
{
    c.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
    {
        if (httpReq.Headers.TryGetValue("X-Forwarded-Host", out var host))
        {
            swaggerDoc.Servers = new List<OpenApiServer> { new OpenApiServer { Url = $"https://{host}{pathBase}" } };
        }
    });
});

app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint($"{pathBase}/swagger/v1/swagger.json", "Item Service API V1");
    c.RoutePrefix = string.Empty;
});

PathBase は、appsettings.json で定義しておきます。ローカル開発では PathBase を空にしておき、Azure 環境では環境変数で設定するようにします。

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "PathBase": ""
}

詳細は割愛しますが、サービス間の呼び出し履歴を確認したいので、Application Insights の SDK も入れました。最後に、Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。
同じ要領で、Pos Service と Stock Service も作っておきます。

マイクロサービスをデプロイする

Azure CLI を使って、Container Apps をデプロイします。
まだ Preview 中なので、拡張モジュールをインストールする必要があります。

$ az extension add --source https://workerappscliextension.blob.core.windows.net/azure-cli-extension/containerapp-0.2.0-py2.py3-none-any.whl
$ az provider register --namespace Microsoft.Web

まずは、Item Service です。環境変数で、PathBase と Application Insights の InstrumentationKey を指定しています。
同一 Environment に配置された Reverse Proxy からのみ呼び出されることを想定しているので、ingress を internal にしています。

$ az containerapp create -n item-service -g <ResourceGroup Name> \
  -e <Environment Name> -i thara0402/item-service:0.6.0 \
  -v PathBase=/item,APPINSIGHTS_INSTRUMENTATIONKEY=<InstrumentationKey> \
  --ingress internal --target-port 80 \
  --revisions-mode single --scale-rules ./deploy/httpscaler.json \
  --max-replicas 10 --min-replicas 1

httpscaler.json には、HTTP リクエスト数によるスケーリングルールを定義しました。

[{
    "name": "http-scaling-rule",
     "type": "http",
    "metadata": {
        "concurrentRequests": "10"
    }
}]

同じ要領で、Pos Service と Stock Service もデプロイしておきます。

Reverse Proxy を作成する

ASP.NET Core の空のプロジェクトを作成し、YARP の NuGet ライブラリをインストールします。

$ Install-Package Yarp.ReverseProxy -Version 1.0.0

www.nuget.org

Program.cs で、YARP の初期化処理を実装します。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddReverseProxy()
    .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
builder.Services.AddApplicationInsightsTelemetry();
var app = builder.Build();
app.MapReverseProxy();
app.Run();

appsettings.json において、YARP のルーティングの構成を定義しておきます。RouteConfig と ClusterConfig に分かれており、HTTP ヘッダーの書き換えや認証など詳細にカスタマイズした構成を組むことができます。
今回は、 Reverse Proxy からバックエンドのマイクロサービスにルーティングするだけのシンプルな構成にしました。

{
  "ReverseProxy": {
    "Routes": {
      "minimumroute": {
        "ClusterId": "minimumcluster",
        "Match": {
          "Path": "{**catch-all}"
        }
      },
      "item-route": {
        "ClusterId": "item-cluster",
        "Match": {
          "Path": "/item/{**catch-all}"
        },
        "Transforms": [
          {
            "PathRemovePrefix": "/item"
          }
        ]
      }
    },
    "Clusters": {
      "minimumcluster": {
        "Destinations": {
          "example.com": {
            "Address": "https://example.com/"
          }
        }
      },
      "item-cluster": {
        "Destinations": {
          "item-service": {
            "Address": "https://item-service.internal.livelysea-b3bd75f6.canadacentral.azurecontainerapps.io"
          }
        }
      }
    }
  }
}

Reverse Proxy にも、Application Insights の SDK を入れました。最後に、Dockerfile を追加し、Ducker Hub に Image をプッシュしておきます。

Reverse Proxy をデプロイする

Azure CLI を使って、Item Service と同じ要領で Reverse Proxy もデプロイします。

$ az containerapp create -n reverse-proxy -g <ResourceGroup Name> \
  -e <Environment Name> -i thara0402/reverse-proxy:0.3.0 \
  -v APPINSIGHTS_INSTRUMENTATIONKEY=<InstrumentationKey> \
  --ingress external --target-port 80 \
  --revisions-mode single --scale-rules ./deploy/httpscaler.json \
  --max-replicas 10 --min-replicas 1

環境変数で、Application Insights の InstrumentationKey を指定しています。
外部からアクセスされる Reverse Proxy なので、ingress を external にしています。

結果確認

ブラウザで Reverse Proxy の URL で Swagger UI を開いて、マイクロサービスの API を呼び出すことができます。

Application Insights の SDK を入れたので、Application Map でこのような結果が表示されます。


まとめ

Azure Container Apps の Reverse Proxy に YARP を使ってみました。このアーキテクチャでの API Gateway パターンはありだと思います。
そうなると、Dapr sidecar を使って YARP からバックエンドのマイクロサービスを呼び出したくなりますが、現段階では非対応のようです。とりあえず、フィードバックしておきました。
github.com

今回のソースコードは、こちらのリポジトリで公開しています。
github.com
github.com