Use system-assigned managed identities to access Azure Cosmos DB data

APPLIES TO: SQL API

In this article, you'll set up a robust, key rotation agnostic solution to access Azure Cosmos DB keys by using managed identities and data plane role-based access control. The example in this article uses Azure Functions, but you can use any service that supports managed identities.

You'll learn how to create a function app that can access Azure Cosmos DB data without needing to copy any Azure Cosmos DB keys. The function app will trigger when an HTTP request is made and then list all of the existing databases.

Prerequisites

Prerequisite check

  1. In a terminal or command window, store the names of your Azure Functions function app, Azure Cosmos DB account and resource group as shell variables named functionName, cosmosName, and resourceGroupName.

    # Variable for function app name
    functionName="msdocs-function-app"
    
    # Variable for Cosmos DB account name
    cosmosName="msdocs-cosmos-app"
    
    # Variable for resource group name
    resourceGroupName="msdocs-cosmos-functions-dotnet-identity"
    

    Note

    These variables will be re-used in later steps. This example assumes your Azure Cosmos DB account name is msdocs-cosmos-app, your function app name is msdocs-function-app and your resource group name is msdocs-cosmos-functions-dotnet-identity.

  2. View the function app's properties using the az functionapp show command.

    az functionapp show \
        --resource-group $resourceGroupName \
        --name $functionName
    
  3. View the properties of the system-assigned managed identity for your function app using az webapp identity show.

    az webapp identity show \
        --resource-group $resourceGroupName \
        --name $functionName
    
  4. View the Cosmos DB account's properties using az cosmosdb show.

    az cosmosdb show \
        --resource-group $resourceGroupName \
        --name $cosmosName
    

Create Cosmos DB SQL API databases

In this step, you'll create two databases.

  1. In a terminal or command window, create a new products database using az cosmosdb sql database create.

    az cosmosdb sql database create \
        --resource-group $resourceGroupName \
        --name products \
        --account-name $cosmosName
    
  2. Create a new customers database.

    az cosmosdb sql database create \
        --resource-group $resourceGroupName \
        --name customers \
        --account-name $cosmosName
    

Get Cosmos DB SQL API endpoint

In this step, you'll query the document endpoint for the SQL API account.

  1. Use az cosmosdb show with the query parameter set to documentEndpoint. Record the result. You'll use this value in a later step.

    az cosmosdb show \
        --resource-group $resourceGroupName \
        --name $cosmosName \
        --query documentEndpoint
    
    cosmosEndpoint=$(
        az cosmosdb show \
            --resource-group $resourceGroupName \
            --name $cosmosName \
            --query documentEndpoint \
            --output tsv
    )
    
    echo $cosmosEndpoint
    

    Note

    This variable will be re-used in a later step.

Grant access to your Azure Cosmos account

In this step, you'll assign a role to the function app's system-assigned managed identity. Azure Cosmos DB has multiple built-in roles that you can assign to the managed identity. For this solution, you'll use the Cosmos DB Built-in Data Reader role.

Tip

When you assign roles, assign only the needed access. If your service requires only reading data, then assign the Cosmos DB Built-in Data Reader role to the managed identity. For more information about the importance of least privilege access, see the Lower exposure of privileged accounts article.

  1. Use az cosmosdb show with the query parameter set to id. Store the result in a shell variable named scope.

    scope=$(
        az cosmosdb show \
            --resource-group $resourceGroupName \
            --name $cosmosName \
            --query id \
            --output tsv
    )
    
    echo $scope
    

    Note

    This variable will be re-used in a later step.

  2. Use az webapp identity show with the query parameter set to principalId. Store the result in a shell variable named principal.

    principal=$(
        az webapp identity show \
            --resource-group $resourceGroupName \
            --name $functionName \
            --query principalId \
            --output tsv
    )
    
    echo $principal
    
  3. Create a new JSON object with the configuration of the new custom role.

    {
        "RoleName": "Read Cosmos Metadata",
        "Type": "CustomRole",
        "AssignableScopes": ["/"],
        "Permissions": [{
            "DataActions": [
                "Microsoft.DocumentDB/databaseAccounts/readMetadata"
            ]
        }]
    }
    
  4. Use az cosmosdb sql role definition create to create a new role definition named Read Cosmos Metadata using the custom JSON object.

    az cosmosdb sql role definition create \
        --resource-group $resourceGroupName \
        --account-name $cosmosName \
        --body @definition.json
    

    Note

    In this example, the role definition is defined in a file named definition.json.

  5. Use az role assignment create to assign the Read Cosmos Metadata role to the system-assigned managed identity.

    az cosmosdb sql role assignment create \
        --resource-group $resourceGroupName \
        --account-name $cosmosName \
        --role-definition-name "Read Cosmos Metadata" \
        --principal-id $principal \
        --scope $scope
    

Programmatically access the Azure Cosmos DB keys

We now have a function app that has a system-assigned managed identity with the Cosmos DB Built-in Data Reader role. The following function app will query the Azure Cosmos DB account for a list of databases.

  1. Create a local function project with the --dotnet parameter in a folder named csmsfunc. Change your shell's directory

    func init csmsfunc --dotnet
    
    cd csmsfunc
    
  2. Create a new function with the template parameter set to httptrigger and the name set to readdatabases.

    func new --template httptrigger --name readdatabases
    
  3. Add the Azure.Identity and Microsoft.Azure.Cosmos NuGet package to the .NET project. Build the project using dotnet build.

    dotnet add package Azure.Identity
    
    dotnet add package Microsoft.Azure.Cosmos
    
    dotnet build
    
  4. Open the function code in an integrated developer environment (IDE).

    Tip

    If you are using the Azure CLI locally or in the Azure Cloud Shell, you can open Visual Studio Code.

    code .
    
  5. Replace the code in the readdatabases.cs file with this sample function implementation. Save the updated file.

    using System;
    using System.Collections.Generic;
    using System.Threading.Tasks;
    using Azure.Identity;
    using Microsoft.AspNetCore.Mvc;
    using Microsoft.Azure.Cosmos;
    using Microsoft.Azure.WebJobs;
    using Microsoft.Azure.WebJobs.Extensions.Http;
    using Microsoft.AspNetCore.Http;
    using Microsoft.Extensions.Logging;
    
    namespace csmsfunc
    {
        public static class readdatabases
        {
            [FunctionName("readdatabases")]
            public static async Task<IActionResult> Run(
                [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req,
                ILogger log)
            {
                log.LogTrace("Start function");
    
                CosmosClient client = new CosmosClient(
                    accountEndpoint: Environment.GetEnvironmentVariable("COSMOS_ENDPOINT", EnvironmentVariableTarget.Process),
                    new DefaultAzureCredential()
                );
    
                using FeedIterator<DatabaseProperties> iterator = client.GetDatabaseQueryIterator<DatabaseProperties>();
    
                List<(string name, string uri)> databases = new();
                while(iterator.HasMoreResults)
                {
                    foreach(DatabaseProperties database in await iterator.ReadNextAsync())
                    {
                        log.LogTrace($"[Database Found]\t{database.Id}");
                        databases.Add((database.Id, database.SelfLink));
                    }
                }
    
                return new OkObjectResult(databases);
            }
        }
    }
    

(Optional) Run the function locally

In a local environment, the DefaultAzureCredential class will use various local credentials to determine the current identity. While running locally isn't required for the how-to, you can develop locally using your own identity or a service principal.

  1. In the local.settings.json file, add a new setting named COSMOS_ENDPOINT in the Values object. The value of the setting should be the document endpoint you recorded earlier in this how-to guide.

    ...
    "Values": {
        ...
        "COSMOS_ENDPOINT": "https://msdocs-cosmos-app.documents.azure.com:443/",
        ...
    }
    ...
    

    Note

    This JSON object has been shortened for brevity. This JSON object also includes a sample value that assumes your account name is msdocs-cosmos-app.

  2. Run the function app

    func start
    

Deploy to Azure

Once published, the DefaultAzureCredential class will use credentials from the environment or a managed identity. For this guide, the system-assigned managed identity will be used as a credential for the CosmosClient constructor.

  1. Set the COSMOS_ENDPOINT setting on the function app already deployed in Azure.

    az functionapp config appsettings set \
        --resource-group $resourceGroupName \
        --name $functionName \
        --settings "COSMOS_ENDPOINT=$cosmosEndpoint"
    
  2. Deploy your function app to Azure by reusing the functionName shell variable:

    func azure functionapp publish $functionName
    
  3. Test your function in the Azure portal.

Next steps