しばやん雑記

Azure とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure Static Web Apps と Front Door の組み合わせが正式にサポートされたので試した

少し前に App Service Authentication と Front Door や Application Gateway を組み合わせた時の問題と解決法を書きましたが、ほぼ同じアーキテクチャの Static Web Apps では当時は回避方法が無く利用困難でした。

Static Web Apps はグローバルに分散されて配信されますが、CDN ではないので Front Door を入れてさらに高度なルーティングとキャッシュを行いたくなることは多いです。

当然ながら非常に困る挙動なので GitHub に Issue を作成していたのですが、先日対応完了したというコメントを貰ったので再度検証することにしました。

まずは適当に Static Web App を Standard で作成して、カスタム認証として Azure AD B2C を設定したアプリケーションを GitHub 経由でデプロイしていきます。

その後 Front Door Standard を作成して、ログインを行った結果が以下になります。

まだ設定を入れていないので、これまで通りリダイレクト URL が Static Web App のものになってしまい、Azure AD B2C 側でミスマッチエラーが出ていることが分かります。

ここからが新しい話になります。この問題は Static Web App が X-Forwarded-Host を正しく扱ってくれないことが原因でしたが、最近のアップデートで staticwebapp.config.jsonforwardingGateway プロパティが追加されました。ドキュメントも更新されています。

今回 allowedForwardedHosts に配列で許可するホスト名を設定すれば、X-Forwarded-Host の値が使われるようになりました。今回の例では Front Door のホスト名を指定すれば良いので以下のようになります。

{
  "$schema": "https://json.schemastore.org/staticwebapp.config.json",
  "forwardingGateway": {
    "allowedForwardedHosts": [
      "afd-swa.z01.azurefd.net"
    ]
  }
}

この設定を使って再度デプロイを行って、Front Door 経由でログインを行うと今度はリダイレクト URL が Static Web App ではなく Front Door のままになっていることが確認できます。もちろん Azure AD B2C のログイン画面も正しく表示されるようになりました。

App Service の場合は X-Forwarded-Host の値を無条件で受け入れるものでしたが、Static Web App は許可制になったという点だけ注意したいですね。

今回のアップデートに伴って Static Web App の前に Front Door を置く方法が正式にサポートされたようで、関連するドキュメントも追加されるようになりました。若干書きかけ感はありますが、今回の肝は forwardingGateway の設定だけなので難しいことはありません。

ほぼチュートリアル通りにはなりますが、以下が Front Door 向けに networkingforwardingGateway を追加した設定になります。auth は長くなるので省略していますが、必要に応じて追加してください。

ひっそりと allowedIpRanges に名前に反して IP アドレス以外に Service Tag を指定できるようになっているので、App Service と同様に AzureFrontDoor.Backend を指定することで、アクセス元を Front Door に制限することができます。これで Static Web App にインターネットから直接アクセスは出来なくなります。

{
  "$schema": "https://json.schemastore.org/staticwebapp.config.json",
  "networking": {
    "allowedIpRanges": [
      "AzureFrontDoor.Backend"
    ]
  },
  "forwardingGateway": {
    "allowedForwardedHosts": [
      "afd-swa.z01.azurefd.net"
    ],
    "requiredHeaders": {
      "X-Azure-FDID": "00000000-0000-0000-0000-000000000000"
    }
  }
}

今回 forwardingGateway に追加された requiredHeaders を使うと必須ヘッダーとしてヘッダー名と値を定義できるので、これで App Service と同じように X-Azure-FDID と Front Door ID を指定すれば、特定の Front Door からのアクセスのみに限定できます。

この辺りは Front Door と App Service での組み合わせ時にも書いているので、そっちを参照してください。

設定を追加してデプロイすると、Front Door 経由以外はアクセス出来なくなったことが確認できます。直接 Static Web App にアクセスしようとすると 403 が返ってくるようになっています。

Service Tag と Front Door ID を組み合わせた方法が許容されないような場合には、Front Door Premium を利用すると Private Endpoint 経由でのアクセスに限定できるので、よりセキュアにできます。

Private Endpoint を使った方法に興味がある方は以前のエントリを参照してください。Private Endpoint を使う場合にも allowedForwardedHosts の設定だけは必要になるので注意してください。

カスタムドメインを設定する場合は Static Web App 側に追加するのではなく、Front Door に対してカスタムドメインを追加して allowedForwardedHosts にホスト名を指定する形になります。

この辺り勘違いされやすい部分ですが、今回のアップデートによって Static Web App にカスタムドメインを追加する必要は完全に無くなったので、デフォルトのドメインと HTTPS のままで問題ありません。

Cosmos DB .NET SDK v2 の廃止と Azure Functions Cosmos DB Extension v4 プレビュー

少し前に Azure Cosmos DB .NET SDK v2 が 2024 年 8 月 31 日で廃止されることが発表されました。これで API 設計がイケてない Microsoft.Azure.DocumentDB 関連のパッケージがついに廃止されます。

移行先となる Microsoft.Azure.Cosmos は十分な実績があり、API 設計も v2 SDK と比べて格段に洗練されたライブラリなので、まだ v2 SDK を使っているアプリケーションがある場合は、廃止を待たずに早めに移行しておくのをお勧めしています。

Cosmos DB .NET SDK v2 は多くのパッケージで参照されているので、それなりに影響範囲は広いことが予想されます。以下のパッケージを参照していれば廃止の影響を受けると考えて良いです。

  • Microsoft.Azure.DocumentDB
  • Microsoft.Azure.DocumentDB.Core
  • Microsoft.Azure.DocumentDB.ChangeFeedProcessor
  • Microsoft.Azure.CosmosDB.BulkExecutor
  • Microsoft.Azure.CosmosDB.Table
  • Microsoft.Azure.Cosmos.Table
  • Microsoft.Azure.WebJobs.Extensions.DocumentDB
  • Microsoft.Azure.WebJobs.Extensions.CosmosDB

これらのパッケージに分離されていた機能は v3 SDK でサポートされているので、移行の障壁となる問題は特にないはずです。ただし API は全くの別物になっているので、対応には多少手間はかかると思います。

特に Change Feed Processor と Bulk Executor を使っている場合は、かなり変更が必要になるので以下のドキュメントを参照しながら慎重に進める必要があります。

ひっそりと混ざっている Microsoft.Azure.Cosmos.Table などの Table API を使うライブラリですが、これらに関しては Table Storage 用に新しい SDK Azure.Data.Tables が既に正式リリースされているので、こちらを使うように変更するのが良いです。

今回の v2 SDK 廃止予告によって、2024 年 8 月 31 日以降も Table Storage の SDK でサポートされるのは Azure.Data.Tables だけとなりました。

地味に Table Storage を使っているアプリケーションは多そうなので、これを機に WindowsAzure.Storage などのサポートが終了した SDK からの移行計画を考えても良いと思います。

Cosmos DB Extension v4 プレビュー

ほとんどは v3 SDK への移行が行えますが Azure Functions の Cosmos DB Extension だけはユーザー側で独自対応が出来ません。このままだと廃止の影響を受けるのですが、最近 v3 SDK ベースに更新された Cosmos DB Extension v4 がプレビューとして公開されました。

確認した範囲では Change Feed の Lease 含めて互換性は保たれているので、Extension v3 から v4 への移行は想像以上にスムーズに行えるかと思います。Cosmos DB .NET SDK v3 の恩恵をフルに受けることが出来るので、正式リリースされたら素早く移行したいですね。

Extension v4 ではベースとなっている Cosmos DB .NET SDK が v2 から v3 に更新されているので、一部のプロパティ名が変更されています。コンパイルエラーになるので気が付くと思いますが、v2 から v3 へのマイグレーションに関するドキュメントがあるので目を通しておくと楽です。

ほとんどは 1 対 1 での置き換えが出来ますが、Change Feed のサンプルでよく使われていた Document クラスは消滅しているので、必要に応じてモデルクラスを追加する必要があります。

今回はサンプルコード全体で使うために、以下のようなシンプルなクラスを作成しました。

public class TodoItem
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("title")]
    public string Title { get; set; }
}

特に説明が必要ないクラスだと思いますが、これを使って Cosmos DB Extension v4 プレビューを試しました。基本的には CosmosClient が取得出来て、v4 への移行後も続きから CosmosDBTrigger を使って Change Feed が処理できれば良いというスタンスです。

ここからはよく使われるであろう機能について個別に試していきます。これまでの不満を綺麗に解消できているので個人的にはかなり満足度が高く、すぐに移行したい欲が高まっています。

キャッシュされた CosmosClient を Binding 経由で取得

これまでも DocumentClient を Binding 経由で受け取れていたように、新しい Extension v4 でも同じ書き方で v3 SDK の CosmosClient を取得できるようになっています。

最近だと DI が使えるので自前で CosmosClient をシングルトンで登録して利用することも多いですが、パフォーマンスの観点から同一アカウントへのクライアントはシングルトンで扱うことが推奨されているので、既に CosmosDBTrigger を使っている場合は Binding 経由で受け取った方が効率的です。

public class Function1
{
    [FunctionName("Function1")]
    public async Task<IActionResult> Run(
        [HttpTrigger(AuthorizationLevel.Function, "get", Route = "todoitems/{id}")] HttpRequest req,
        string id,
        [CosmosDB(Connection = "CosmosConnection")] CosmosClient cosmosClient)
    {
        var container = cosmosClient.GetContainer("SampleDB", "TodoItem");

        var todoItem = await container.ReadItemAsync<TodoItem>(id, new PartitionKey(id));

        return new OkObjectResult(todoItem.Resource);
    }
}

特に説明もいらないと思いますが Binding で CosmosClient を取得して直接 API を利用しています。この時 CosmosClient は接続先ごとにキャッシュされているので安心して使えます。

作成した HttpTrigger を適当に実行してみれば、動作することが確認できます。

f:id:shiba-yan:20210902164159p:plain

この程度であれば CosmosDB Binding を使った方が早いですが、複雑なクエリは CosmosClient を扱う必要があるので知っていて損はないでしょう。

CosmosDBTrigger でカスタム型のコレクションを利用

Extension v3 では CosmosDBTrigger の入力として IReadOnlyList<Document>JArray を使う必要があり、直接必要とするクラスへのデシリアライズは行えないという若干謎で不便な仕様になっていました。

公式ドキュメントでも IReadOnlyList<Document> を使う以外の方法は書かれていません。

正直なところ CosmosDBTrigger において一番の不満はこの挙動でしたが、Extension v4 からは以下のように独自のモデルクラスを指定できるようになりました。

前述したように Document クラスは消滅しているので、v4 への移行時には必ず書き換える必要がある部分ですが、大体はクラスにマッピングしていると思うのでスムーズに対応できるはずです。

public class Function2
{
    [FunctionName("Function2")]
    public void Run([CosmosDBTrigger(
                        databaseName: "SampleDB",
                        containerName: "TodoItem",
                        Connection = "CosmosConnection",
                        LeaseContainerName = "leases")]
                    IReadOnlyList<TodoItem> items, ILogger log)
    {
        if (items != null && items.Count > 0)
        {
            log.LogInformation("Documents modified " + items.Count);
            log.LogInformation($"First document Id = {items[0].Id}, Title = {items[0].Title}");
        }
    }
}

実行して適当に Cosmos DB のデータを変更すると、以下のように用意したクラスへ正しくマッピングされていることが確認できます。ちょっとした改善ではありますが、かなり使い勝手が改善しました。

f:id:shiba-yan:20210902195409p:plain

シリアライザーはデフォルトの状態では Json.NET が使われるので属性には注意する必要があります。カスタマイズしたい場合は後述するインターフェースを実装して対応します。

Managed Identity / RBAC を利用したアクセス

長らく Cosmos DB ではアクセスキーを使った認証が使われてきましたが、少し前から SQL API に関しては Azure AD 認証 + RBAC に対応しています。Managed Identity を使ってアプリケーション単位で細かく権限を割り当てできるのと、アクセスキーの管理を不要に出来るのでセキュリティ向上にも役立ちます。

少し前に話題になった Cosmos DB の Jupyter Notebook 周りの脆弱性では、Managed Identity と RBAC を使っていれば影響を最小限に抑えることが可能でした。脆弱性の詳細はブチザッキを参照してください。

本来であれば Managed Identity と RBAC を全面的に使った設計にしておきたいのですが、Azure Functions 周りがネックとなって RBAC への移行が行えない状態でした。しかし Extension v4 から Managed Identity と RBAC に対応しています。

RBAC への移行はシンプルで、これまでの接続文字列ではなく Cosmos DB のエンドポイント URL を <CONNECTION_NAME_PREFIX>__accountEndpoint で指定するだけで終わります。当然ながら Managed Identity と RBAC の設定は別で必要です。

{
  "IsEncrypted": false,
  "Values": {
    "AzureWebJobsStorage": "UseDevelopmentStorage=true",
    "FUNCTIONS_WORKER_RUNTIME": "dotnet",
    "CosmosConnection__accountEndpoint": "https://***.documents.azure.com:443/"
  }
}

この App Settings での指定方法は Azure Functions の RBAC 対応で共通となっています。他にも Blob や Queue、Event Hubs でも使えるので、興味のある方は以下のエントリを参照してください。

ローカル開発環境でも Azure サービス認証が有効な状態かつ、RBAC でログイン中のユーザーに権限を割り当てていればアクセスキー無しで動作するようになります。

ブチザッキにも書いてあるように ARM レベルで disableLocalAuth を設定するとアクセスキーを使った認証を完全に無効化出来るので、RBAC への移行完了後には設定しておきたいですね。

JSON シリアライザーのカスタマイズ

デフォルトでは JSON シリアライザーとして Json.NET が使われていますが、最近だと System.Text.Json を使ってモデルクラスの属性を統一したい場合もあります。

そういったカスタマイズ用に Extension v4 では ICosmosDBSerializerFactory インターフェースが追加されています。以下のように Cosmos DB .NET SDK v3 で追加された CosmosSerializer を返す実装を用意すれば、任意のシリアライザーに変更可能です。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        builder.Services.AddSingleton<ICosmosDBSerializerFactory, MyCosmosDBSerializerFactory>();
    }
}

public class MyCosmosDBSerializerFactory : ICosmosDBSerializerFactory
{
    public CosmosSerializer CreateSerializer()
    {
        var options = new JsonSerializerOptions
        {
            PropertyNamingPolicy = JsonNamingPolicy.CamelCase
        };

        return new CosmosSystemTextJsonSerializer(options);
    }
}

v3 SDK 自体には System.Text.Json を使う CosmosSerializer は提供されていませんが、サンプルコードとしては用意されているので以下の実装をそのまま使うのが簡単です。

これで JSON シリアライザーを System.Text.Json に置き換えることが出来ました。

まだプレビューなので Bulk サポートなど若干足りない機能はありますが、まずは Extension v3 からのスムーズな移行が目的になっているようなので、そういった意味では十分すぎるアップデートだと思います。

そろそろ Azure Functions v4 のリリースも近づいているので、検証をしつつ GA を待ちましょう。

Azure Front Door や Application Gateway と App Service Authentication を組み合わせて利用する

App Service を利用したアーキテクチャでは、フロントエンドに Front Door や Application Gateway といった L7 のロードバランサーを追加する構成を組むことがそれなりにあります。

特に Front Door は CDN と L7 ロードバランサが組み合わされたサービスなので、静的コンテンツをキャッシュしつつ API などのリクエストはバックエンドプールに分散させることが簡単に実現出来ます。

実際に Front Door のチュートリアルでも地理分散された App Service の前に Front Door を 1 つ作成して、可用性を高める構成を取っています。DNS レベルでルーティングを行う Traffic Manager よりも、Front Door の方が制御しやすくバックエンド選択のパラメータが多いです。

設定が反映されるまで地味に時間がかかるのが難点ではあるのですが、その辺りは若干 Front Door Standard / Premium で改善されそうな気配があるので期待しています。

Front Door 自体の話はこれぐらいにして、本題である Front Door や Application Gateway といった L7 ロードバランサと App Service Authentication を組み合わせた時に発生する問題と、解決策の話をします。

Front Door を作成し App Service Authentication を設定

適当に Front Door を作成して、バックエンドには App Service を追加しておきます。App Service では予め App Service Authentication を使って Azure AD 認証を有効にしてあります。

f:id:shiba-yan:20210908001211p:plain

今回は Front Door にカスタムドメインの設定までは行っていませんが、フロントに Front Door を追加すると当然ながらホスト名が App Service のデフォルトではなくなるので、Azure AD のアプリケーション登録からリダイレクト URI を変更します。

単純にホスト名を azurewebsites.net から azurefd.net に置き換えるだけで良いです。カスタムドメインを設定した場合は、それに置き換えれば問題ありません。

f:id:shiba-yan:20210907234213p:plain

Front Door と Azure AD アプリケーションの設定はこれだけです。作業自体は難しくないはずです。

実際にアクセスしてみると、Azure AD のログイン画面にリダイレクトしたタイミングで、以下のようにリダイレクト URI のミスマッチエラーが出るようになります。

f:id:shiba-yan:20210907234301p:plain

OpenID Connect に対応したプロバイダーを使っていると、たまに設定忘れで表示されるものにはなりますが、今回は正しいリダイレクト URI を設定しているのに表示されます。

原因は非常に単純で Azure AD のログイン画面にリダイレクトされる時のクエリパラメータを確認するとわかります。重要な redirect_uri には見てわかる通り、アクセスに使用した azurefd.net ではなくバックエンド App Service の azurewebsites.net が渡されています。

f:id:shiba-yan:20210907234547p:plain

この時点で詳しい方は原因が分かったと思いますが、App Service Authentication が X-Forwarded-Host を正しく扱っていないことが原因です。

以下のエントリで書いたように、Front Door は一般的な L7 ロードバランサと同様に X-Forwarded-* 系ヘッダーをバックエンドへのリクエストに追加しますが、App Service Authentication は Host ヘッダーのみを見てリダイレクト先を作成しているようです。

App Service Authentication の問題なので ASP.NET Core などで自前で実装すると、大体は X-Forwarded-Host を正しく扱えるのでこのような問題は発生しないのですが、この手の認証を自前で実装するのは避けているのでコードを書かずに解決します。

これまでは解決しようがなかったのですが、App Service Authentiation の V2 からロードバランサー下にある場合の処理が改善され、設定の変更で対応できるようになりました。

新しい App Service Authentication V2 の設定を有効化

Azure Portal に新しい Authentication 設定が追加されたタイミングで、プラットフォームと ARM の両方で実装が新しくなり、今回のようなロードバランサー下のシナリオに対応できるようになりました。

リファレンスでは存在を知っていたのですが、ドキュメント化がまともにされていなかったので App Service Team Blog の記事によって、初めて動作と設定方法が公開されました。

具体的には forwardProxy の中にある convention プロパティの値を、デフォルトの NoProxy から Standard に変更すると、標準となっている X-Forwarded-Host を扱うようになります。

App Service Authentication のランタイムは V1 と V2 で共通ですが、この設定を有効化するためには V2 に移行する必要があります。V2 への移行は Azure Portal からは簡単に行えますが、ARM Template や Terraform を使っている場合は手間がかかります。

今回は最初から Auth Settings V2 で作っているので、移行の手順に関しては省略します。

Auth Settings V2 のエンドポイントは PATCH リクエストを受け付けてくれないので、以下のように ARM Explorer 上でサクッと編集するのが一番簡単です。

f:id:shiba-yan:20210907235451p:plain

Application Gateway のように X-Forwarded-Host 以外のヘッダーを送ってくる場合は、Custom に設定した上で customHostHeaderName に利用するヘッダー名を設定すればよいです。関係ないですが apiPrefix を変更すればエンドポイントを .auth 以外に変更することも出来ます。

設定を変更後に最初と同じように azurefd.net にアクセスすると、今度は redirect_uri が Front Door を指したままになっているので、Azure AD 認証も問題なく通るようになりました。

f:id:shiba-yan:20210907235839p:plain

今回のようにバックエンドの App Service に対してアクセス制限を設定していない場合は、リダイレクト先が異なっていても Azure AD 側の設定漏れによってはログイン出来てしまうケースもあるので、アクセス制限込みで設定を忘れないようにしたいです。

Static Web Apps では現時点で問題あり

App Service Environment がバックエンドで使われている Static Web Apps でも、Custom Authentiation を有効化しつつ Front Door などの L7 ロードバランサーを組み合わせて利用すると、同様の問題が発生します。

残念ながら Static Web Apps では App Service のように設定変更が行えないため、現時点では回避できない問題となっていますが、来月あたりには対応するという返事は貰っています。

どのように設定するのかはまだ分からないですが、恐らく staticwebapp.config.json に同じような設定が追加されるのだと思います。解消されるまでは上の Issue をウォッチしていきたいと思います。

Azure App Service から Private Endpoint を利用する際の設定がシンプルになった

先日 Azure App Service のゾーン冗長対応を確認していた時に気が付いたのですが Regional VNET Integration を Azure Portal から設定すると、デフォルトで App Service からの Outbound トラフィック全てを VNET にルーティングする設定が有効になっていました。

以前は App Settings から WEBSITE_VNET_ROUTE_ALL を追加する必要がありましたが、Azure Portal から 1 クリックでオンオフが切り替えられるようになりました。

f:id:shiba-yan:20210826110925p:plain

ドキュメントも 8 月の頭に大幅にアップデートされて WEBSITE_VNET_ROUTE_ALL を使った方法が綺麗さっぱりなくなり、代わりに ARM レベルで追加された vnet_route_all_enabled を使う方法に変わっています。

これまでの App Settings を使った方法も問題なく使えるようですが、どこかのタイミングで既存のリソースも ARM レベルのプロパティに移行しておいた方が良さそうな雰囲気です。

最新版の Azure CLI を使っていれば --vnet-route-all-enabled オプションで簡単に有効化出来ます。

az webapp config set --resource-group RG_NAME --name APP_NAME --vnet-route-all-enabled true

当然ながら ARM Template では設定できますし、Terraform でも v2.74.0 から vnet_route_all_enabled プロパティが site_config に追加されたので、ARM Template と同様に利用できます。

ちなみに Terraform で利用する場合のサンプルは以下のようになります。当たり前ですが VNET と Regional VNET Integration の設定も必要です。

resource "azurerm_app_service" "default" {
  name                = "app-routeall-test"
  location            = azurerm_resource_group.default.location
  resource_group_name = azurerm_resource_group.default.name
  app_service_plan_id = azurerm_app_service_plan.default.id

  site_config {
    vnet_route_all_enabled = true
  }
}

resource "azurerm_app_service_virtual_network_swift_connection" "default" {
  app_service_id = azurerm_app_service.default.id
  subnet_id      = azurerm_subnet.default.id
}

App Settings から分離されたので入力補完も効くようになり、扱いやすくなりました。出来れば App Settings にあるものは ARM レベルに移動してほしいのですが、追加は時間がかかるようなので難しそうです。

新しい設定を使って Private Endpoint を利用する

前述の通り 8 月の頭に Regional VNET Integration のドキュメントが大幅にアップデートされたのですが、このタイミングでこれまで Private Endpoint*1 を使うのに必要だった WEBSITE_DNS_SERVER に関する記述も全て消滅していました。代わりに以下の制約が追加されていました。

For Linux Apps Azure DNS private zones only works if Route All is enabled.

Integrate your app with an Azure virtual network - Azure App Service | Microsoft Docs

これまで App Service では Windows / Linux に関係なく Private Endpoint を使う際には WEBSITE_VNET_ROUTE_ALLWEBSITE_DNS_SERVER の設定が必要でしたが、最近のアップデートによって Windows の場合は設定なしで、Linux の場合は Route All 設定のみで利用可能になったようです。

以前の設定に関しては以下のエントリで紹介しているので、興味のある方は参照してください。

確認のために Windows の Azure Functions を作成して Regional VNET Integration を設定し、同時に Route All をオフにした環境を用意しました。

以前は Private Endpoint が必要とする Private DNS Zones を解決できず、エラーになっていた設定です。

f:id:shiba-yan:20210830020041p:plain

App Settings に WEBSITE_VNET_ROUTE_ALLWEBSITE_DNS_SERVER が存在していないことを確認しました。

f:id:shiba-yan:20210830020115p:plain

この状態から Kudu の Debug Console から Private Endpoint を有効化した Cosmos DB のホスト名を解決できるか確認すると、問題なく Private IP が返ってくることが確認できます。

f:id:shiba-yan:20210826112031p:plain

Cosmos DB Trigger を追加すると、問題なく lease に項目が作成されて Change Feed を読み取ることが出来ました。これで Windows の場合は Regional VNET Integration を有効化するだけで、Private Endpoint が使えることを確認出来ました。

Linux の App Service で利用する場合

一応 Linux の App Service も作成してドキュメント通り Private Endpoint が DNS サーバーの設定なしで使えるか確認しておきます。自分は知りませんでしたが、今年の頭から 1 つのリソースグループに Windows と Linux の App Service を混在出来るようになっていました。

Linux and Windows App Service plans can now share resource groups. This limitation has been lifted from the platform and existing resource groups have been updated to support this.

Overview - Azure App Service | Microsoft Docs

実際に検証用に Linux の App Service Plan を同じリソースグループに作成できました。

これまで同じ VNET に入れたい場合でも Windows と Linux を混在出来ず、リソースグループを分ける必要があったので面倒でしたが、これでかなり改善されました。

f:id:shiba-yan:20210830195301p:plain

話を戻して Linux の App Service からの Private Endpoint を使った Cosmos DB への接続ですが、まずは Route All 設定をオフにした状態で試すと、Private IP ではなく通常の Public IP が返ってきました。

f:id:shiba-yan:20210830191310p:plain

Private DNS Zones を使った名前解決が行われていないことが確認出来ました。

そして Route All 設定をオンに切り替えてから、同じように Cosmos DB の名前解決を試すと、今度は意図した通り Private IP が返ってくるようになりました。

f:id:shiba-yan:20210830191318p:plain

もちろん Azure Functions を作成して Cosmos DB Trigger を追加すると、Private Endpoint 経由で Cosmos DB の Change Feed を読み取ることが出来ています。

Windows と Linux で若干差があるので、個人的には Private Endpoint を使う場合には Route All をオンに統一するようにした方が混乱が無いかと思います。Windows / Linux の両方とも WEBSITE_DNS_SERVER の追加は必要なくなっているので、設定ミスではまることは減るかと思います。

*1:正確には Azure DNS の Private Zones

Application Insights の Azure AD 認証 / Managed Identity 対応を試した

久し振りに Application Insights SDK for .NET のリリースノートを眺めていたら Azure AD 認証に対応したと記載があり、興味を持ったので設定と動作を試しておきました。

これまで InstrumentationKey があればテレメトリを送信出来ていましたが、Azure AD 認証を有効化すると RBAC での許可をアプリケーション単位で付けないと送信できなくなります。

現在 Azure AD 認証は Public Preview として公開されていて制約も多いので、ドキュメントにはしっかりと目を通しておきましょう。最新の SDK でしか利用できないのと、App Service や Azure Functions に組み込まれている方法の場合は利用できません。手動での SDK インストールが必要です。

ダウンタイム無しで Azure AD 認証に切り替える場合は、以下の手順で行う必要があります。

  1. Application Insights SDK を v2.18.0 以上に更新
  2. Azure AD 認証を利用するように設定 (Token Credential / Managed Identity / RBAC など)
  3. アプリケーションをデプロイ
  4. Application Insights 側で Local Authentication を無効化

ASP.NET Core の場合は以下の SDK をインストールすれば、Azure AD 認証に対応した v2.18.0 が入ります。

追加で Managed Identity を使うために必要な Azure.Identity もインストールしておきます。既にインストールされている場合も最近では多くなっているかと思います。

これで Application Insights SDK で Azure AD 認証を有効に出来るようになりました。Azure AD 認証を使ったとしても InstrumentationKey は必要になるので、これまで通り接続文字列を設定しましょう。

ドキュメントでは以下のように InstrumentationKey が全て 0 になっていますが、このままだと動作しないので注意してください。Azure Portal から接続文字列をコピーしてください。

{
  "ApplicationInsights": {
    "ConnectionString": "InstrumentationKey=00000000-0000-0000-0000-000000000000;IngestionEndpoint=https://xxxx.applicationinsights.azure.com/"
  }
}

後はコード側で Azure AD 認証を使うように設定すれば完了です。新しく SetAzureTokenCredential メソッドが追加されているので、一般的には DefaultAzureCredential を渡せばよいです。

以下のようにコードの修正は非常に少なく済んでいます。接続文字列で指定出来れば更に便利でした。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<TelemetryConfiguration>(config =>
    {
        config.SetAzureTokenCredential(new DefaultAzureCredential());
    });

    services.AddApplicationInsightsTelemetry();

    services.AddControllersWithViews();
}

次は Managed Identity を有効化済みの App Service に RBAC で必要なロールを割り当てていきます。新しく Monitoring Metrics Publisher というロールが増えているので、これを割り当てるだけで終わります。

ローカルから使う機会はほぼ無いと思いますが、Azure サービス認証で設定しているユーザーに対しても RBAC でロールを割り当てれば、Managed Identity 以外からでもテレメトリを送信できます。

これでアプリケーションをデプロイすれば、Azure AD 認証付きでテレメトリが送信されるようになります。

最後に Application Insights のローカル認証を無効化して、Azure AD 認証を必須にして確認します。設定は Workspace と同様に Properties の中にあるので少しわかりにくいです。

突然ローカル認証という言葉が出てきましたが、これまでの InstrumentationKey だけを使った方法のことを指しています。選択すると以下のように無効化するための画面が表示されます。

ここで無効化すれば Azure AD 認証が付いていないとエラーになるようになります。この際のエラーは .NET の場合は EventSource で書き出されるのでキャプチャするのが若干面倒です。なので Live Metrics が送信されているかで確認するのが一番早いです。

無効化した状態で RBAC でロールを削除すれば、少し後にテレメトリが送信されなくなることまで確認しました。例によって RBAC 設定には少しタイムラグが発生するので、焦って弄らないようにしたいです。

更に高い可用性を実現する Azure App Service の Availability Zones 対応がリリース

これまで App Service Environment は Availability Zones に対応していましたが、ついにマルチテナント型の App Service でも Availability Zones に対応しました。これで複数のゾーンに分散してアプリケーションをデプロイすることで、マルチリージョンより簡単に高可用性を実現出来るようになりました。

Availability Zones は東日本にも用意されているので、珍しく最初から日本でも利用可能です。

まだ公式ドキュメントの更新とリリースアナウンスは行われていないですが、近日中に諸々発表されると思われます。もしかしたら SLA が若干変わってくるかも知れないので期待したいところです。

元々 App Service のアーキテクチャは単一インスタンスでも高い可用性を実現できる仕組みになっていますが、基本は単一のリージョンやゾーンに属しているのでデータセンターの障害時には、複数インスタンス構成にしていても影響を受ける可能性はありました。

今回の Availability Zones 対応によって App Service のインスタンスを複数ゾーンに分散出来るようになったため、単一データセンター障害の影響を最小限に抑えることが出来ます。

その代わり以下のように利用には若干の制約も存在しています。主に料金面や Premium V3 (VMSS Worker) に影響する部分が大きいですが、それによって得られるメリットも十分大きいです。

  • Premium V2 / Premium V3 のみ対応
    • 既に Premium V3 を使っている場合は同一 RG に展開できる
    • VMSS Worker で動いている Premium V2 / V3 が必要という話
  • 最低でも 3 インスタンスが必要
    • 自動的に 3 つの AZ に分散される
    • インスタンス数は 3 の倍数にする必要はない
  • App Service Plan の作成時のみ設定可能
    • 後から設定変更することは出来ない
  • ゾーン障害時には必要な数のインスタンスをベストエフォートで自動確保する
    • キャパシティ不足の場合には確保できない場合もある
    • 単一ゾーン障害を考慮しキャパシティを事前に設定するのが良い
  • 現在は Azure Portal からデプロイは出来ない
    • ARM Template を使う必要がある

一般的にマルチリージョン構成を組もうとすると、ピアリング含めた VNET とアクセス制御の設計、ストレージが非同期レプリケーションで切り替え時にデータ損失が発生すること、アプリケーションのデプロイ自体が複数に対して行う必要があるなど、設計面と運用面の両方で負荷が高まりやすいです。

しかし Availability Zones を利用した設計ではマルチリージョンに匹敵する高可用性を、単一リージョンと同等の設計で得ることが出来るので、今回の App Service の AZ 対応は今後の設計を大きく変えます。

実際にゾーン冗長に対応した App Service を試していきます。今は Azure Portal から作成出来ないので、以下のような ARM Template を作成して Azure Portal の Template Deployment に食わせるのが一番簡単です。

{
  "$schema": "http://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "hostingPlanName": {
      "type": "string"
    },
    "sku": {
      "type": "string"
    },
    "skuCode": {
      "type": "string"
    },
    "numberOfWorkers": {
      "type": "string"
    }
  },
  "resources": [
    {
      "apiVersion": "2020-12-01",
      "name": "[parameters('hostingPlanName')]",
      "type": "Microsoft.Web/serverfarms",
      "location": "[resourceGroup().location]",
      "kind": "app",
      "properties": {
        "zoneRedundant": true
      },
      "sku": {
        "tier": "[parameters('sku')]",
        "name": "[parameters('skuCode')]",
        "capacity": "[parameters('numberOfWorkers')]"
      }
    }
  ]
}

リソースグループは事前に AZ が利用可能なリージョンを指定して作成しておきます。今回は折角なので Japan East を使うことにしました。

Template Deployment を使うとフォーム形式で必要な情報を入力できるの簡単です。制約に従う形で Premium V2P1v2 を選択し、最低インスタンス数は 3 を指定しました。

f:id:shiba-yan:20210826130919p:plain

これでデプロイを行うと、以下のように Zone redundant が Enabled になった App Service Plan が作成されます。この点以外はこれまでの App Service Plan と全く違いはありません。

f:id:shiba-yan:20210826115124p:plain

後は Web App や Azure Functions の作成時にこのゾーン冗長に対応した App Service Plan を指定すると、自動的に東日本の場合は 3 つのゾーンに分散されてデプロイされます。App Service Plan の上で動く Web App や Azure Functions は AZ を意識する必要が無いのでシンプルです。

ARM Template 以外では、近日中にリリースされる予定の Terraform Provider for Azure の v2.74.0 から、ゾーン冗長に対応した App Service Plan を作成可能になるはずです。

ここからは複数ゾーンにデプロイされた App Service をもうちょっと深堀していきます。

いつも通り Kudu や ARM Explorer を使って 3 つのゾーンに分散してデプロイされたインスタンスの詳細を確認してみましたが、インスタンス ID と IP アドレス以外の違いはありませんでした。これまで通り VM や AZ を意識することなく利用できることを意味しています。

ぶっちゃけ App Service からはどのゾーンにデプロイされているのかを把握する方法は存在しないようで、Home Stamp は 3 インスタンス共に同じ値で違いはありませんでした。ただし CPU の世代が 3 インスタンスでバラバラだったので、どれが古いデータセンターなのか予想は付きそうです。

Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz
Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz
Intel(R) Xeon(R) Platinum 8272CL CPU @ 2.60GHz

今回は Premium V2 を選んだのでばらけましたが、Premium V3 を選ぶと結果は変わってくるはずです。ちなみにゾーン冗長を行っていてもスケールアップ・スケールアウトはこれまで通り高速に行えるので、ASE よりも確実に有利だと感じています。

割り当てられる Virtual IP address はこれまで通り 1 つですが、裏側の ARR はゾーン冗長されていることを期待しています。ファイル共有も全て同じ IP を指していたので、ゾーン冗長されていないと困ります。*1

App Service としての機能は全く同じで、何も気にせずに Regional VNET Integration を使って Service Endpoint や Private Endpoint 経由での通信が行えます。既に SQL Database や Cosmos DB など多くのサービスはゾーン冗長に対応しているので、組み合わせることでシンプルな構成を保ったまま高可用性と高いセキュリティを実現出来るでしょう。

f:id:shiba-yan:20210826123544p:plain

残っていたコンピューティング部分の弱みが App Service の AZ 対応で埋まったと感じています。

最後に Azure Functions に関してですが、作成時に App Service Plan を選択すればゾーン冗長の App Service Plan 上で実行することは出来ますが、同時に作成される Azure Storage はデフォルトで Storage V1 の LRS となるので単一データセンターでホストされます。

この Azure Storage は Azure Functions の実行に必要で、データセンター障害が発生するともちろん影響を受けるものなので、価格は上がりますが Storage V2 (GPv2) の ZRS に変更するとゾーン冗長になります。

ZRS ではゾーン間で同期レプリケーションが行われるので、Azure Functions からも問題なく利用できます。ZRS の利用は公式にサポートされている使い方となるので安心してください。*2

ただし Azure Portal を使った Azure Functions の作成時には Azure Storage の冗長性を指定できないので、あらかじめ ZRS の Azure Storage を別に作成しておく必要があります。

f:id:shiba-yan:20210826124740p:plain

これで Azure Functions 自体もゾーン冗長で動かすことが出来るようになります。

Azure Functions は Azure Storage と紐づくため、複数リージョンを使って可用性を高めたい場合には若干手間が多かったですが、Availability Zones を利用することでシンプルに実現出来ます。

App Service の歴史の中でかなりインパクトのあるアップデートだと思っているので、これまで以上に高い可用性が必要な場面では積極的に使っていきたいですね。

追記

リリースされた当初は App Service Plan の Premium V2 / V3 のみ対応でしたが、数日前に更新があり Azure Functions の Premium Plan (Elastic Premium や Functions Premium とも呼ばれる) でもゾーン冗長を有効化出来るようになったようです。

Additionally, AZ support for the Azure Functions Premium plan is now available. More information for that feature to be released within the coming weeks.

App Service Support for Availability Zones - Azure App Service

数週間以内に更なる情報が公開されるようなので、そのタイミングで Azure Functions をゾーン冗長にするドキュメントや Azure Portal での対応が公開されるのかもしれません。

特に Azure Portal では ZRS のストレージを予め作っておくのは面倒なので期待しています。

*1:確認する方法がないので公式のアナウンスを信じるしかない

*2:Azure Functions support ZRS storage account · Issue #69119 · MicrosoftDocs/azure-docs · GitHub

Azure Front Door Premium から Static Web Apps へのアクセスを Private Endpoint で保護する

少し前に Static Web Apps の Private Endpoint 対応がプレビューとして公開されました。Static Web Apps が GA のタイミングで使えるという話だったのが遅れてリリースされたものになります。

Private Endpoint を使うことで Express Route を経由して Private IP でアクセス出来るようになりますが、Static Web Apps にそのようなユースケースが存在しているのかは若干謎です。

個人的には Front Door Premium の Private Link との組み合わせが一番求められていると感じるので、Private Endpoint が使えるようになった記念で試しておきます。

以前に App Service と Blob Storage で試した時の記事は以下になります。結局は Private Endpoint が使えるようになっていれば良いので、設定方法はほぼ同じです。

設定方法ほぼ同じとはいえ、Front Door Premium と Static Web Apps の Private Endpoint 対応は両方ともプレビュー中なので、Azure Portal では完全にサポートされていません。

実際に Private Link を有効にしたオリジンを追加しようとすると、Target sub resource が選択できません。

これは Azure Portal 側の問題となるので、オリジンを作成後に ARM Explorer などを使って直接 Private Link を有効化することで回避できます。

ARM Explorer で作成されたオリジンに対して sharedPrivateLinkResource を設定していきます。

PUT /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/***/providers/Microsoft.Cdn/profiles/***/originGroups/default-origin-group/origins/default-origin?api-version=2020-09-01
{
  "properties": {
    "originGroupName": "default-origin-group",
    "hostName": "***.azurestaticapps.net",
    "httpPort": 80,
    "httpsPort": 443,
    "originHostHeader": "***.azurestaticapps.net",
    "priority": 1,
    "weight": 1000,
    "enabledState": "Enabled",
    "sharedPrivateLinkResource": {
      "privateLink": {
        "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/***/providers/Microsoft.Web/staticSites/***"
      },
      "groupId": "staticSites",
      "privateLinkLocation": "westus2",
      "requestMessage": "Please approve this connection."
    },
    "provisioningState": "Succeeded",
    "deploymentStatus": "NotStarted"
  }
}

サブスクリプション ID やリソース名は適宜読み替えてください。id には Private Endpoint を有効化したいリソース ID を、groupId には Target sub resource 名を指定します。Static Web Apps の場合は staticSites が Target sub resource 名になります。

リソースを更新すると Azure Portal からも Private Link が有効化されているのが確認できます。

まだこの状態では Front Door から Private Endpoint 経由での接続は確立されていません。

この後 Static Web Apps 側の Private Endpoint connections を確認すると Front Door からの Pending 状態の接続リクエストが表示されているはずです。自分が試した限りでは少し時間がかかりました。

Front Door からの接続リクエストを Approve してあげることで Front Door から接続可能になります。

Approve してからも少し接続完了までに少し不安定な時がありますが、しばらく待つと安定して Front Door から Static Web Apps へのアクセスが Private Endpoint 経由で行えるようになります。

以下は実際に Private Endpoint が有効になっている Static Web Apps にはアクセス出来ていないですが、Front Door 経由では問題なくアクセス出来ていることが確認できます。

これで App Service や Blob Storage と同じように Front Door Premium と Private Endpoint でオリジンを保護しつつアクセスできるようになりました。Premium は高いので必要となる場面は少なそうですが、セキュリティを重視する場合に選択肢になると思います。

Azure Cosmos DB を ASP.NET Core の分散キャッシュのバックエンドとして利用する

少し前に Cosmos DB を ASP.NET Core の Distributed Cache Provider として使うライブラリの正式バージョンがリリースされました。ASP.NET 向けにも同じようなライブラリは出ていましたが、こちらは Cosmos DB SDK v3 で構築された最新版です。

重要なことは全て公式ブログに書いているので特に説明しませんが、実際に利用する際に気になるポイントを調べたのでメモとして残しておきます。

基本となる設定は AddCosmosCache を呼び出して IDistributedCache として登録する部分です。ASP.NET Core はこのインターフェースをベースとしてセッション機能が構築されています。

オプション指定すれば自動的に Cosmos DB の Database / Container を作成出来ますが、検証や開発初期段階ぐらいでの利用に留めておきましょう。Container レベルのスループットで作成されるので、Database レベルでスループットを共有したい場合や Serverless で利用したい場合は手動で作る必要があります。

public void ConfigureServices(IServiceCollection services)
{
    services.AddCosmosCache(options =>
    {
        options.ClientBuilder = new CosmosClientBuilder(Configuration.GetConnectionString("CosmosConnection"));

        options.DatabaseName = "AspNetCore";
        options.ContainerName = "Session";

        // Database / Container を自動作成してほしい場合はコメントアウト
        //options.ContainerThroughput = 400;
        //options.CreateIfNotExists = true;
    });

    // 必要に応じて使う
    services.AddSession(options =>
    {
        options.IdleTimeout = TimeSpan.FromMinutes(10);

        options.Cookie.HttpOnly = true;
        options.Cookie.IsEssential = true;
    });
}

ここで ClientBuilder を指定すると Cache 向けに専用の CosmosClient が作成されるようになります。専用の Cosmos DB アカウントを指定する場合には ClientBuilder を設定したほうが良いです。

アプリケーションでも Cosmos DB を利用していて、Cache とアカウントを共有したい場合は以下のように CosmosClient 自体を共有したほうが効率が良いです。

public void ConfigureServices(IServiceCollection services)
{
    // Cosmos DB Account を共有する場合は CosmosClient も共有した方が良い
    var cosmosClient = new CosmosClient(Configuration.GetConnectionString("CosmosConnection"), new CosmosClientOptions
    {
        SerializerOptions = new CosmosSerializationOptions
        {
            PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
        }
    });

    // アプリケーション向けに CosmosClient を DI に追加
    services.AddSingleton(cosmosClient);

    // AddCosmosCache でも作成済みの CosmosClient をセット
    services.AddCosmosCache(options =>
    {
        options.CosmosClient = cosmosClient;

        options.DatabaseName = "AspNetCore";
        options.ContainerName = "Session";

        // Database / Container を自動作成してほしい場合はコメントアウト
        //options.ContainerThroughput = 400;
        //options.CreateIfNotExists = true;
    });
}

今回は指定していませんが、オプションにある DefaultTimeToLiveInMs は名前からはミリ秒単位で指定する雰囲気が出ていますが、実際には秒単位での指定となるので注意が必要です。*1

自動生成される Container はパーティションキーが /id 指定となっているので、適当に ASP.NET Core でセッションを生成させると以下のようなデータが入ります。

f:id:shiba-yan:20210823183730p:plain

パーティションキーのカスタマイズは出来るっぽいですが、特に行う必要もないでしょう。

データへのアクセスは id の値のみあれば問題ないので、自動生成される Container では他のプロパティがインデックスから除外されるように最適化されています。Change Feed メインで使う時の設定と同じです。

{
    "indexingMode": "consistent",
    "automatic": true,
    "includedPaths": [],
    "excludedPaths": [
        {
            "path": "/*"
        },
        {
            "path": "/\"_etag\"/?"
        }
    ]
}

案外インデックスの作成で RU を食われることが多いので、必要なプロパティのみに絞った方が効率的です。

Terraform を使って自動生成される Container と同じ設定のものを作成する場合は、以下のような定義が必要になります。もし DefaultTimeToLiveInMs を指定している場合は default_ttl の値も合わせておきます。

resource "azurerm_cosmosdb_sql_container" "session" {
  name                = "Session"
  resource_group_name = azurerm_cosmosdb_account.example.resource_group_name
  account_name        = azurerm_cosmosdb_account.example.name
  database_name       = azurerm_cosmosdb_sql_database.example.name
  partition_key_path  = "/id"
  default_ttl         = -1

  indexing_policy {
    indexing_mode = "Consistent"

    excluded_path {
      path = "/*"
    }
  }
}

元のブログにも分散環境で重要な設定として Cosmos DB の Consistency Level が挙げられている通り、利用環境に合わせて適切な Consistency Level を選択しておかないと、書き込んだはずのデータが読み込めなかったという事態が発生します。

多くの場合は Consistency Level としてデフォルトの Session が使われていると思いますが、名前の通り同一のセッション*2の場合のみ、書き込んだデータが直後に読み込めることが保証されています。

つまり Consistency Level が Session の場合、確実にデータを読み込むためにはユーザーは同じインスタンスにアクセスし続ける必要があります。

Front Door / Application Gateway / App Service はクッキーベースの Session Affinity が提供されているので、これらを利用可能な場合は Consistency Level は Session でも問題ないです。*3

Session Affinity が利用できない場合には Cosmos DB の Consistency Level を Session より高くすることで対応します。つまり Strong か Bounded Staleness のどちらかを選択することになります。

Strong を選択するのが一番手っ取り早く確実ではありますが、書き込みコストが高くレイテンシも悪化するので Bounded Staleness が現実的な選択肢となります。単一リージョンで使う分には Bounded Staleness は Strong と変わらないので安心です。

Consistency for clients in the same region for an account with single write region = Strong

Consistency levels in Azure Cosmos DB | Microsoft Docs

複数インスタンス環境下で Session のまま使っていると、セッションに書き込んだデータが消失したという報告が上がってきて悩むことになるので、Consistency Level はしっかりと検討しておきましょう。

実際に遭遇したことがありますが、稀に発生するので調査の難易度が高い問題になりがちです。

*1:この問題は次のリリースで修正されます `DefaultTimeToLiveInMs` specified in milliseconds? · Issue #51 · Azure/Microsoft.Extensions.Caching.Cosmos · GitHub

*2:具体的には Session Token を持つリクエスト

*3:ただしインスタンスが入れ替わった場合には読み込めない可能性もある

GitHub Actions / Azure Pipelines 上で Azurite と Cosmos DB Emulator を使ったテストを実行する

GitHub や Azure DevOps を使った開発フローにテスト実行を組み込むのは一般的に行われていると思いますが、Azure Storage や Cosmos DB などに依存するテストを実行する際には、実際のリソースにアクセスさせるのではなくローカルで完結させたいことが多いです。

特にテストケースによってはデータが実行毎に揮発してくれた方が都合の良いことが多いので、実際のリソースより気軽に起動とデータの全削除が行える Emulator が便利です。Azure Storage と Cosmos DB には Docker ベースの Emulator が提供されているので、CI と組み合わせるのが簡単になっています。

Azure Storage の Emulator は最近は Azurite が主流になっているので、古い Storage Emulator ではなくこちらを使って行くようにしていきます。Node.js ベースなので使いやすいです。

ビルド済みの Docker Image が MCR で提供されているので、利用するのも捨てるのも簡単です。

ちなみに Visual Studio 2022 からは Azurite が npm でグローバルインストールされていれば、自動的に使われるようになっているので今後は自然と移行する形になるとは思います。

Cosmos DB Emulator は Windows と Linux 両方の Docker Image が公開されていますが、基本は Linux 向けの Docker Image を使えばよいです。自分は WSL 2 から使って試しました。

公式ドキュメントでは Direct モードでの接続に対応した手順が紹介されていますが、GitHub Actions や Azure Pipelines では Gateway モードを使うようにした方が簡単です。

気を付けないといけない点としては Emulator が発行する自己署名証明書の扱いがあります。クライアントの設定を変更して署名エラーを無視する方法もありますが、今回は証明書をインストールして対応します。

.NET ではマシンの証明書ストアを見てくれますが、利用する言語によっては独自のストアに追加する必要があります。特に Java を利用する場合には気を付けたい部分です。

クライアントを Emulator 向けに初期化

Azure Storage と Cosmos DB の各サービス向けクライアントは以下のドキュメントにある通りスレッドセーフが保証されていて、基本はシングルトンで扱うことが推奨されています。

多くの ASP.NET Core や Azure Functions を使ったアプリケーションでは DI を使って、コンストラクタで各インスタンスを受け取るようになっているはずなので、カスタマイズした各クライアントを簡単に渡せます。

Azure Storage の場合は以下のように接続文字列を見慣れたものに変えるだけで良いです。

// ほとんどの Azure SDK で対応
var connectionString = "UseDevelopmentStorage=true";

// 実際の Emulator 向け接続文字列
// Azure SDK for Python は UseDevelopmentStorage=true に対応していないので注意
// https://github.com/Azure/azure-sdk-for-python/issues/10040
// var connectionString = "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;";

// Blob / Queue / Table の各クライアントを Emulator 向けに初期化
var blobServiceClient = new BlobServiceClient(connectionString);
var queueServiceClient = new QueueServiceClient(connectionString);
var tableServiceClient = new TableServiceClient(connectionString);

多くの言語向けの SDK では UseDevelopmentStorage が使えますが、Python に関しては使えないのでフルの接続文字列を指定する必要があります。新しい Azure SDK は各言語でデザインが統一されているので、同じクラス名で書けるはずです。

次は Cosmos DB ですが Azure Storage のように省略された接続文字列は存在していないので、エンドポイントとアカウントキーが含まれた接続文字列を指定します。基本的には固定です。

// Cosmos DB Emulator の接続文字列(固定)
var connectionString = "AccountEndpoint=https://localhost:8081/;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==";

var cosmosClient = new CosmosClient(connectionString, new CosmosClientOptions
{
    // Gateway モードを使った方が簡単
    ConnectionMode = ConnectionMode.Gateway
});

今回は Emulator の自己署名証明書を信頼するように設定するので、変更点はほぼ接続文字列ぐらいで済んでいます。ドキュメントには署名エラーを無視する設定方法も記載されているので、参考にしてください。

これで Azurite と Cosmos DB Emulator をアプリケーションから使う準備が出来たので、後は GitHub Actions と Azure Pipelines で Emulator を使うように定義します。

GitHub Actions を使う例

GitHub Actions には任意の Docker Image をサービスとして実行する機能が用意されているので、これを使って Azurite と Cosmos DB Emulator を立ち上げます。

注意点としてはポートのマッピングをローカルとコンテナーの両方で明示しないと、ローカル側は自動的に空いているポートが割り当てられることです。ローカルからアクセスするポートは固定する必要があるので、しっかりと指定しておきます。

作成した Workflow は全体として以下のようになりますが、重要なのは services の定義と証明書をインストールしている部分ぐらいです。証明書のダウンロードエンドポイントは Readiness としても使えるらしいので、リトライすることで起動するまで待つようにしています。

name: Tests

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

env:
  DOTNET_VERSION: 5.0.x

jobs:
  tests:
    runs-on: ubuntu-latest
    services:
      azurite:
        image: mcr.microsoft.com/azure-storage/azurite
        ports:
        - 10000:10000
        - 10001:10001
        - 10002:10002
      cosmos:
        image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator
        ports:
        - 8081:8081
        - 10251:10251
        - 10252:10252
        - 10253:10253
        - 10254:10254
        env:
          AZURE_COSMOS_EMULATOR_PARTITION_COUNT: 3
          AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: false
        options: -m 3g --cpus=2.0
    steps:
    - uses: actions/checkout@v2

    - name: Import emulator certificate
      run: |
        while ! curl -k -f https://localhost:8081/_explorer/emulator.pem > ~/emulatorcert.crt; do sleep 5; done
        sudo cp ~/emulatorcert.crt /usr/local/share/ca-certificates/
        sudo update-ca-certificates

    - name: Use .NET Core ${{ env.DOTNET_VERSION }}
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: ${{ env.DOTNET_VERSION }}

    - name: Build project
      run: dotnet build

    - name: Run unit tests
      run: dotnet test --no-build

Cosmos DB Emulator の設定は AZURE_COSMOS_EMULATOR_PARTITION_COUNT を最低値の 3 に変更しています。パーティション数を多くすると起動パフォーマンスが悪化するので最低限にしています。8081 以外のポートは Gateway だと不要な気もしましたが、とりあえずそのまま入れています。

適当にコミットして GitHub Actions 上で実行すると、以下の通りテストケース含め全て成功します。

証明書のインストールを行っている部分を削除すると、ちゃんと Cosmos DB のテストケースが落ちるようになります。証明書のインストール部分以外は特に難しいことはないですね。

Azure Pipelines を使う例

Azure Pipelines にも同じように Docker Image をサービスとして起動する機能があるので、ほぼ同じように扱えます。書き方が少し違いますが機能的には全く同じです。

こちらも同じようにポート番号はローカル側も定義しないと自動で割り振られるので注意が必要です。

trigger:
- master

pool:
  vmImage: ubuntu-latest

variables:
  DOTNET_VERSION: 5.0.x

resources:
  containers:
  - container: azurite
    image: mcr.microsoft.com/azure-storage/azurite
    ports:
    - 10000:10000
    - 10001:10001
    - 10002:10002
  - container: cosmos
    image: mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator
    ports:
    - 8081:8081
    - 10251:10251
    - 10252:10252
    - 10253:10253
    - 10254:10254
    env:
      AZURE_COSMOS_EMULATOR_PARTITION_COUNT: 3
      AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE: false
    options: -m 3g --cpus=2.0

services:
  azurite: azurite
  cosmos: cosmos

steps:
- script: |
    while ! curl -k -f https://localhost:8081/_explorer/emulator.pem > ~/emulatorcert.crt; do sleep 5; done
    sudo cp ~/emulatorcert.crt /usr/local/share/ca-certificates/
    sudo update-ca-certificates
  displayName: Import emulator certificate

- task: UseDotNet@2
  inputs:
    packageType: 'sdk'
    version: $(DOTNET_VERSION)
  displayName: Use .NET Core $(DOTNET_VERSION)

- task: DotNetCoreCLI@2
  inputs:
    command: 'build'
  displayName: Build project

- task: DotNetCoreCLI@2
  inputs:
    command: 'test'
    arguments: '--no-build'
  displayName: Run unit tests

実行すると GitHub Actions の時と同じように、各コンテナーが起動されてから実際の処理が行われます。VM には差はないはずですが、若干 GitHub Actions で実行したほうが早く感じたのは謎です。

.NET との組み合わせでは自動的にテスト結果を収集して、見やすく表示してくれるので結構便利です。

サービスを色々動かそうとすると Hosted Runner では若干リソース不足な感が否めないので、もうちょっと大きな VM を選べるようになると利用範囲が広がりそうです。

今回の Service Containers を Self-Hosted Runner を使う場合には Docker 周りを自分で構築する必要があり、それなりに手間がかかるのであまりお勧めはしないです。この辺りは Virtual Environment のビルド済み Image が公開されると圧倒的に楽になるのですが難しそうです。

App Service Authentication で Azure AD と Microsoft Account を使ったログインを実装する

App Service Authentication は非常に便利でかなり頻繁に使っていますが、基本はシングルテナントの利用が多く、Microsoft Account を使ったログイン方法に関してはこれまで試したことが無かったのと、NuGet のログイン画面を見て実現方法に興味を持ったので試してみました。

ちなみに NuGet のログイン画面は以下のようなものです。Microsoft Account と組織アカウントの両方でログイン出来ますが、常にアカウントの選択が必要かつ利用可能なアカウントがあれば表示されています。

この NuGet と同じログイン画面を App Service Authentication だけで実現していきます。

登録済みであれば確認無しでシームレスにサインイン出来て便利ですが、複数サインインしている場合は利用するアカウントを選ばせたいケースもあるかなと思います。

MSA も利用可能な Azure AD アプリケーションを作成

適当に作成した Web Appで App Service Authentication を有効化していくわけですが、Azure Portal の新しい Authentication を使って設定するようにします。新しい設定では v2.0 エンドポイントが使われて MSA と Azure AD の扱いが統合されているので簡単に登録できます。

Identity Provider として Microsoft を選んだ後に表示される Supported account types を Current tenant から Any Azure AD directory & personal Microsoft accounts に変更します。

これ以外の手順は通常と同じなのでドキュメントの方を参照してください。ボタンを押していくだけで Azure AD アプリケーションの作成とシークレットの登録まで行ってくれます。

ここまでの手順で Azure AD テナントに所属するアカウントでのログインは行えるようになっていますが、最後に MSA でログイン可能にするために Issuer URL を変更します。Azure Portal で作ると常にテナントに紐づく Issuer URL が設定されるので修正する必要があります。

具体的には https://sts.windows.net/TENANT_ID/v2.0 となっている Issuer URL を以下のように https://login.microsoftonline.com/common/v2.0 に変更します。

この Issuer URL の意味についてはドキュメントが良くまとまっています。要するに MSA と Azure AD の両方でログイン可能にするためには common を使う必要があります。

設定を保存した後に Web App にアクセスしてみると、見慣れた Azure AD アプリケーションのアクセス許可の確認画面が表示されます。MSA でログインはしているのですが、Azure AD テナントにも所属しているのでこちらが優先されているようです。

現在のテナントに所属していない MSA や、別テナントでアプリケーションを作成した場合には MSA が優先されて利用されるので、その場合は MSA 側のアクセス許可の確認画面が表示されます。

ログイン時にアカウントを選択可能にする

ここまでで MSA と Azure AD でログイン可能になりましたが、まだ NuGet のようなアカウント選択の画面は表示されないので、ログイン時のパラメータを追加して対応します。

NuGet の場合は prompt=select_account がログイン時に追加されています。ドキュメントにも説明と他に設定可能な値が載っているので参照してください。

ログイン時に渡す追加パラメータは Azure Portal から設定は出来ないので、Azure CLI や ARM Explorer などを使って設定する必要があります。手軽なので ARM Explorer を使うのが良いと思います。

ARM 上は authsettingsV2 として扱われているので、その中にある Azure AD セクションに以下のように loginParameters を追加します。

保存した後に Web App にアクセスすると、以下のように常にアカウントの選択画面と、利用可能なアカウントがある場合は表示されるようになります。これで NuGet と同じログイン画面を実現出来ました。

個人的には利用可能なアカウントが下に表示されるのが気に入っているので、MSA と組織アカウントの両方対応が必要なサービスの場合には使ってみたいと思っています。

MSA のみ利用可能にするときの注意点

今回は Azure AD と MSA の両方に対応した Azure AD アプリケーションと Web App の設定を行いましたが、MSA のみ利用可能なアプリケーションを作る際には同じような修正が必要となるのでメモしておきます。

Azure AD アプリケーションの作成自体は App Service Authentication の設定時に Personal microsoft account only を選ぶだけでよいです。

この状態で Web App にアクセスすると MSA でのログイン後に 401 エラーとなります。

例によって Issuer URL がテナントに紐づいている形式になっているのが原因のようなので、正しくログインを行うためには MSA 専用の Issuer URL に変更する必要があります。

古い App Service Authentication のドキュメントにはこの辺の手順が載っています。

MSA のみを使う場合には、以下のどちらかを Issuer URL に設定する必要があります。consumers は名前の通りですが、GUID で始まる方は MSA のテナント ID のようです。

https://login.microsoftonline.com/consumers/v2.0
https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0

Issuer URL を変更して設定を保存すれば、MSA のみでのログインも正常に動作するようになります。

Azure Portal から設定した場合のみ Issuer URL が自動設定されるので、ARM Template や Terraform を使う際には Issuer URL を間違えなければ問題ないはずです。v1 と v2 と同様に注意しておきたいです。