Setting up fine-grained access control in Cosmos DB isn’t as straightforward as you might think. That’s because Role Definitions in Cosmos DB don’t function the same way as traditional Azure Role-Based Access Control (RBAC) managed through Azure Resource Manager (ARM). If you’ve ever tried to apply ARM Role Definitions to Cosmos DB data plane resources — like databases, containers, and items — you’ve probably realized it doesn’t work as expected. That’s because Cosmos DB requires its own Role Definitions and Role Assignments.

Rather than relying on ARM RBAC alone, Cosmos DB has its own access model, which means that in addition to ARM Role Assignments, you need to create Cosmos DB-specific Role Definitions and Role Assignments. Yes, it’s like Coraline, except the parents from the other world don’t have buttons for eyes — they have Cosmos DB logos instead. I suppose this is better than just using “local authentication” or a God-mode Access Key. It didn’t let you walk through walls or anything but it came close.

This article walks through a structured approach to setting up Cosmos DB access control using Terraform, with a focus on separating concerns between a central root module and workload-specific modules. We’ll explore how to define Cosmos DB-specific Role Definitions and how workloads can self-provision their own access while keeping the overall infrastructure maintainable.

ARM vs. Cosmos DB Role Definitions

Azure Resource Manager (ARM) provides Role-Based Access Control (RBAC) through Role Assignments that link Built-In or Custom Role Definitions to identities managed in Entra ID. These identities could be users, groups, service principals, or managed identities.

You can use ARM RBAC to control access to Cosmos DB, but only at the control plane level — things like managing the Cosmos DB account itself, configuring network settings, or setting up Private Endpoints. However, if you need to manage access to the data plane — databases, containers, and items — you must use Cosmos DB Role Definitions and Role Assignments instead. Rather than relying on local authentication or a “God-mode” access key, using Cosmos DB Role Definitions provides better security and allows you to enforce the principle of least privilege.

“Central” Root Module

When provisioning Cosmos DB, it’s common to manage a Cosmos DB account separately from workloads that depend on it. This separation allows changes to be made to individual workloads without affecting the overall Cosmos DB configuration.

For this reason, I deploy the Cosmos DB account using its own Terraform root module. This central module is responsible for:

  • Provisioning the Cosmos DB account
  • Managing ARM Role Assignments (e.g., Cosmos DB Operator)
  • Configuring network settings, such as Private Endpoints
  • Defining Cosmos DB-specific Role Definitions

Cosmos DB Role Definitions

I typically define three custom Cosmos DB Role Definitions:

  • Admin: Full access to databases, containers, and items
  • Writer: Can write and execute queries but not manage configurations
  • Reader: Can read data but cannot modify it

Here’s an example of how these Role Definitions are implemented:

Admin Role Definition

resource "azurerm_cosmosdb_sql_role_definition" "admin" {
  name                = "admin"
  resource_group_name = azurerm_resource_group.main.name
  account_name        = azurerm_cosmosdb_account.main.name
  type                = "CustomRole"
  assignable_scopes = [
    azurerm_cosmosdb_account.main.id
  ]

  permissions {
    data_actions = [
      "Microsoft.DocumentDB/databaseAccounts/readMetadata",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/*",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*"
    ]
  }
}

Writer Role Definition

resource "azurerm_cosmosdb_sql_role_definition" "writer" {
  name                = "writer"
  resource_group_name = azurerm_resource_group.main.name
  account_name        = azurerm_cosmosdb_account.main.name
  type                = "CustomRole"
  assignable_scopes = [
    azurerm_cosmosdb_account.main.id
  ]

  permissions {
    data_actions = [
      "Microsoft.DocumentDB/databaseAccounts/readMetadata",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeStoredProcedure",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/*"
    ]
  }
}

Reader Role Definition

resource "azurerm_cosmosdb_sql_role_definition" "reader" {
  name                = "reader"
  resource_group_name = azurerm_resource_group.main.name
  account_name        = azurerm_cosmosdb_account.main.name
  type                = "CustomRole"
  assignable_scopes = [
    azurerm_cosmosdb_account.main.id
  ]

  permissions {
    data_actions = [
      "Microsoft.DocumentDB/databaseAccounts/readMetadata",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeQuery",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/executeStoredProcedure",
      "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/items/read"
    ]
  }
}

If you don’t like mine, that’s okay. You can create your own just by tweaking which data_actions you want to grant to each Role Definition. Luckily there is a great resource with the complete list of all the Data Actions.

Now with the Role Definitions defined in the central Root Module. I can go forth and create additional workloads that grant themselves the access they need to the corresponding User Assigned Managed Identities that they use.

Workload Root Module

With the central Cosmos DB Role Definitions in place, workloads can now self-provision access by referencing the Role Definitions they need.

Each workload module should:

  • Retrieve the Cosmos DB Account
  • Retrieve the required Cosmos DB Role Definitions
  • Create a User Assigned Managed Identity
  • Assign the identity to a Cosmos DB Role

You should start out by referencing the Cosmos DB account using a data source.

data "azurerm_cosmosdb_account" "main" {
  name                = var.cosmosdb_account.name
  resource_group_name = var.cosmosdb_account.resource_group_name
}

Then you’ll also need a data source to the Cosmos DB Role Definitions you want to create Role Assignments for:

data "azurerm_cosmosdb_sql_role_definition" "writer" {
  resource_group_name = var.cosmosdb_account.resource_group_name
  account_name        = var.cosmosdb_account.name
  role_definition_id  = var.cosmosdb_account.writer_role_id
}

Then creating your own User Assigned Managed Identity and adding a Cosmos DB Role Assignment is easy!

resource "azurerm_user_assigned_identity" "function" {

  resource_group_name = azurerm_resource_group.main.name
  location            = azurerm_resource_group.main.location
  name                = "mi-${var.application_name}-${var.environment_name}"

}

Finally here is the role assignment:

resource "azurerm_cosmosdb_sql_role_assignment" "function_writer" {

  resource_group_name = data.azurerm_cosmosdb_sql_role_definition.writer.resource_group_name
  account_name        = data.azurerm_cosmosdb_sql_role_definition.writer.account_name
  role_definition_id  = data.azurerm_cosmosdb_sql_role_definition.writer.id
  scope               = data.azurerm_cosmosdb_account.main.id
  principal_id        = azurerm_user_assigned_identity.function.principal_id

}

That’s it!

Conclusion

Managing Cosmos DB access requires understanding the difference between ARM and Cosmos DB Role Definitions and Role Assignments — you might need to manage both. Terraform provides two separate resource types for handling these, and structuring your Cosmos environment correctly allows you to maximize the reuse of shared infrastructure while giving workloads the flexibility to extend and customize their access. Workloads can leverage predefined Role Definitions for common access patterns, whether for managed identities or even human users.