しばやん雑記

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

Azure Cosmos DB における接続文字列の管理が RBAC サポートでついに不要に

Ignite は PaaS / Serverless 周りの話が少なかったですが、Cosmos DB に関してはいくつかアップデートがありました。その中でも RBAC サポートはアクセスキーや接続文字列を管理したくない勢としては待望の機能なので、使い勝手を確認しておきました。

Cosmos DB チームは相変わらずドキュメントがしっかり書かれているので、ドキュメント通りに作業を進めるだけでサクッと動きました。なので、あまり細かい使い方は書きません。

今のところは Azure Resource Manager の RBAC のように組み込みのロールは用意されていません。先に必要なカスタムロールを作成してから割り当てる必要がありますが、細かく権限管理が行えるので安心です。

アプリケーションから RBAC を使ってアクセスするためには最新のプレビュー版 SDK が必要になるので、間違えて正式版をインストールしないようにします。バージョンを明示的に指定しないと入りません。

Visual Studio の Azure サービス認証や Managed Identity を使う場合には、Cosmos DB SDK と同時に Azure.Identity のインストールが必要です。

早速サンプルコードを出していきますが、CosmosClientTokenCredential を受け取るコンストラクタが追加されているので、新しい Azure SDK を使ったことがある方なら余裕だと思います。

class Program
{
    static async Task Main(string[] args)
    {
        var tokenCredential = new DefaultAzureCredential();

        var cosmosClient = new CosmosClient("https://***.documents.azure.com:443/", tokenCredential, new CosmosClientOptions
        {
            SerializerOptions = new CosmosSerializationOptions
            {
                PropertyNamingPolicy = CosmosPropertyNamingPolicy.CamelCase
            }
        });

        var container = cosmosClient.GetContainer("HackAzure", "TodoItem");

        var iterator = container.GetItemLinqQueryable<TodoItem>()
                                .ToFeedIterator();

        while (iterator.HasMoreResults)
        {
            var items = await iterator.ReadNextAsync();

            foreach (var todoItem in items)
            {
                Console.WriteLine($"{todoItem.Id},{todoItem.Title},{todoItem.Body}");
            }
        }
    }
}

まずは Visual Studio でのデバッグ実行を行いたいので、ログインしているユーザーに対して Azure CLI で Read Write が行えるカスタムロールを割り当てました。

少し迷ったのが --role-definition-id に指定する値ですが、カスタムロール作成時に返ってきた id プロパティではなく name プロパティの値を指定するのが正解です。

az cosmosdb sql role assignment create -a *** -g *** --scope "/" --principal-id "ユーザーの Object ID" --role-definition-id "作成したカスタムロールの ID (GUID)"

カスタムロールを割り当てると F5 でのデバッグ実行だけで問題なく動作します。ロールを割り当てていない場合は 403 が返ってきてエラーとなります。

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

接続文字列のために Key Vault Reference などを使う必要がなくなり、Cosmos DB のエンドポイントだけ設定に持っておけば良いので扱いが楽です。

同じように Managed Identity を有効にした App Service / Azure Functions でも利用することができます。使い方はコンソールアプリケーションの時と全く同じですが、最近の Azure Functions っぽい書き方をします。

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
        var context = builder.GetContext();

        builder.Services.AddSingleton(provider =>
        {
            var tokenCredential = new DefaultAzureCredential();

            return new CosmosClient(context.Configuration["CosmosEndpoint"], tokenCredential);
        });
    }
}
public class Function1
{
    public Function1(CosmosClient cosmosClient)
    {
        _cosmosClient = cosmosClient;
    }

    private readonly CosmosClient _cosmosClient;

    [FunctionName("Function1")]
    public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req, ILogger log)
    {
        var container = _cosmosClient.GetContainer("HackAzure", "TodoItem");

        var iterator = container.GetItemLinqQueryable<TodoItem>()
                                .Take(3)
                                .ToFeedIterator();

        var items = await iterator.ReadNextAsync();

        return new OkObjectResult(items);
    }
}

トークンを取得するのに DefaultAzureCredential を使っているので、同じコードのまま Visual Studio でのデバッグ実行と Azure Function 上の両方で動作します。Azure CLI が入っている環境でも動作します。

実際に Azure Functions にデプロイしてテスト実行をしてみると、接続文字列無しで問題なく動作しました。

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

基本的な RBAC 周りの動作は確認したので、アプリケーションから操作可能な Container と処理を制限するカスタムロールを作成してみます。

個人的には Change Feed をよく使うので、Change Feed 経由での読み取りのみ可能なカスタムロールを以下のように作りました。readMetadata は Cosmos DB を使う上で必要なものなので入れておきます。

{
    "RoleName": "ChangeFeedOnly",
    "Type": "CustomRole",
    "AssignableScopes": ["/"],
    "Permissions": [{
        "DataActions": [
            "Microsoft.DocumentDB/databaseAccounts/readMetadata",
            "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/readChangeFeed"
        ]
    }]
}

このカスタムロールを割り当てればサクッと Change Feed のみ可能になるかと思い、一般的な Change Feed Processor のコードを書いて試してみましたが実際にはエラーになりました。

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

エラーになった理由としては単純で、Change Feed Processor は実際には Lease Container を必要としていて、Lease に対しては読み書きの権限が必要だからという話でした。

なので Change Feed Processor を利用するために、2 つのカスタムロールを 2 つの Container をスコープとして設定して割り当てました。Lease に対しては読み書き可能なロールを、Change Feed で読み取る Container に対しては作成した Change Feed のみ可能なロールを割り当てています。

accountName='Cosmos DB アカウント名'
resourceGroupName='リソースグループ名'
principalId='User / Managed Identity / Service Principal の ObjectId'
changeFeedRole='Change Feed のみ許可する Role Definition Id'
readWriteRole='Read Write を許可する Role Definition Id'
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName --scope "/dbs/HackAzure/colls/TodoItem" --principal-id $principalId --role-definition-id $changeFeedRole
az cosmosdb sql role assignment create -a $accountName -g $resourceGroupName --scope "/dbs/HackAzure/colls/Lease" --principal-id $principalId --role-definition-id $readWriteRole

このロールの割り当てで問題なく Change Feed Processor が動作するようになりました。実際にここまで細かく RBAC で制限するかは要件次第だと思いますが、ガチガチに制限することが出来るのは良いことです。

ちなみに Change Feed Processor ではなく Change Feed の Pull model を使う場合には、Change Feed のみ可能なロールの割り当てだけで動作します。Pull model は Lease を必要としないのが理由ですが、結局はどこかに継続トークンを保存する必要があるので、何かしらの読み書き権限は必要になります。