しばやん雑記

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

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 を待ちましょう。