Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure IoT Hub Cosmos DB Routing Endpoint Terraform Module

A single IoT Hub custom routing endpoint that writes device messages as documents into an existing Cosmos DB container — secure by default with managed-identity authentication, and key-based access only when a caller explicitly opts in. Targets hashicorp/azurerm ~> 4.0.


Terraform azurerm Module Version Type Resources


🧩 Overview

This module manages a single IoT Hub Cosmos DB account routing endpoint attached to an IoT Hub you already own.

  • 🔌 Creates one azurerm_iothub_endpoint_cosmosdb_account.this bound to an existing IoT Hub by iothub_id.
  • 🔐 Authenticates with a managed identity (identityBased) by default — no account key is required unless the caller opts into keyBased.
  • 🌐 Requires endpoint_uri (the Cosmos DB account URI) for every authentication mode, plus database_name and container_name naming the write target.
  • 🗝️ Supports keyBased auth via sensitive primary_key / secondary_key inputs — provisioned out of band, never committed.
  • 🧩 Exposes optional partition_key_name and partition_key_template to control how routed documents are partitioned in the container.
  • ⏱️ Exposes per-operation timeouts; this resource does not support Azure tags.
  • 📤 Emits the endpoint's resource id first, then its name, for downstream routing-rule wiring.

💡 Why it matters: IoT Hub message routing can persist device telemetry directly into a Cosmos DB container, giving you a queryable, low-latency document store without an intermediate consumer. Defaulting to managed-identity authentication keeps account keys out of your configuration and state entirely.


❤️ Support this project

If this module saves you time, a little support goes a long way:


🗺️ Where this fits in the family

flowchart LR
  hub["terraform-azurerm-iothub"]
  me["terraform-azurerm-iothub-endpoint-cosmosdb-account"]
  cosmos["terraform-azurerm-cosmosdb-account"]
  uai["terraform-azurerm-user-assigned-identity"]
  route["terraform-azurerm-iothub-route"]
  eh["terraform-azurerm-iothub-endpoint-eventhub"]
  sbq["terraform-azurerm-iothub-endpoint-servicebus-queue"]
  sbt["terraform-azurerm-iothub-endpoint-servicebus-topic"]
  sc["terraform-azurerm-iothub-endpoint-storage-container"]
  hub -->|"iothub_id"| me
  cosmos -->|"endpoint_uri / database_name / container_name"| me
  uai -->|"identity_id"| me
  me -->|"routes to endpoint"| route
  hub -.->|"sibling endpoint"| eh
  hub -.->|"sibling endpoint"| sbq
  hub -.->|"sibling endpoint"| sbt
  hub -.->|"sibling endpoint"| sc
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
  classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
  class me me;
  class hub target;
  class cosmos,uai,route,eh,sbq,sbt,sc ext;
Loading

The parent IoT Hub, the target Cosmos DB account, and the user-assigned identity are all owned by sibling modules. This module consumes their identifiers and creates the routing endpoint that a terraform-azurerm-iothub-route rule then directs traffic to. The Event Hub, Service Bus, and Storage Container endpoint modules are siblings on the same hub.


🧬 What this module builds

flowchart LR
  in_id["name / resource_group_name / iothub_id"]
  in_target["endpoint_uri / database_name / container_name"]
  in_auth["authentication_type / identity_id / primary_key / secondary_key"]
  in_part["partition_key_name / partition_key_template"]
  res["azurerm_iothub_endpoint_cosmosdb_account.this"]
  out["id / name"]
  in_id -->|"input"| res
  in_target -->|"input"| res
  in_auth -->|"input"| res
  in_part -->|"input"| res
  res -->|"output"| out
  classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
  class res me;
Loading

Resource inventory

Resource Cardinality Role
azurerm_iothub_endpoint_cosmosdb_account.this single (keystone) The Cosmos DB routing endpoint on the named IoT Hub, targeting a specific account URI, database, and container.

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures provider "azurerm", its authentication, and the mandatory features {} block.

Schema notes that bite

  • 🔒 name, resource_group_name, and iothub_id are force-new — changing any of them destroys and recreates the endpoint.
  • 🌐 endpoint_uri is required for every authentication mode (it is the Cosmos DB account URI), unlike the other IoT Hub endpoint types where the URI is identity-only. database_name and container_name are likewise always required.
  • 🗝️ primary_key / secondary_key are valid only with authentication_type = "keyBased". Supplying them with identityBased is a configuration error.
  • 🔐 identity_id is valid only with authentication_type = "identityBased" and must reference one of the IoT Hub's assigned identities. Omit it to use the hub's system-assigned identity.
  • 🚫 The endpoint name must be unique across the hub's endpoint types; events, operationsMonitoringEvents, fileNotifications, and $default are reserved and cannot be used.
  • 🧩 partition_key_name and partition_key_template shape how routed documents are partitioned; they are in-place updatable, not force-new.

🔑 Required Azure RBAC Roles / Permissions

Least-privilege at the parent IoT Hub scope:

  • Microsoft.Devices/IotHubs/write and Microsoft.Devices/IotHubs/read on the IoT Hub. Contributor scoped to the hub (there is no built-in "IoT Hub Contributor" role, and the four real IoT Hub roles are data-plane only, so none can create a routing endpoint) (or Contributor scoped to the hub) covers this.
  • For identityBased routing, the hub's identity needs a Cosmos DB data-plane role on the target account — for example the built-in Cosmos DB Built-in Data Contributor — so the endpoint can write documents.

Azure Prerequisites

  • An existing IoT Hub and a target Cosmos DB account, database, and container in a supported US Azure region — this module references all of them and creates none.
  • For identityBased auth, a managed identity assigned to the IoT Hub with data access to the Cosmos DB account.
  • The Microsoft.Devices resource provider registered on the subscription.

📁 Module Structure

terraform-azurerm-iothub-endpoint-cosmosdb-account/
├── providers.tf     # required_version + azurerm ~> 4.0 pin; no provider block
├── variables.tf     # typed inputs: name, resource_group_name, iothub_id, endpoint_uri, database_name, container_name, authentication_type, identity_id, primary_key, secondary_key, partition_key_name, partition_key_template, subscription_id, timeouts
├── main.tf          # keystone azurerm_iothub_endpoint_cosmosdb_account.this + dynamic timeouts
├── outputs.tf       # id (first), then name
├── README.md        # this document
├── SCOPE.md         # the cross-module contract
├── LICENSE          # MIT
└── .gitignore       # canonical library ignore set

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-iot-eastus/providers/Microsoft.Devices/IotHubs/iot-fleet-eastus"

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  # authentication_type defaults to "identityBased" — the IoT Hub's identity writes documents.
}

ℹ️ The caller owns the provider "azurerm" block, its authentication (Azure CLI, managed identity, or OIDC), and the mandatory features {} block. This module declares no provider configuration.

🔒 With no authentication_type set, the endpoint uses identityBased — no account key is present in configuration or state.


🔌 Cross-Module Contract

Consumes

Input Type Source module
iothub_id string terraform-azurerm-iothub
endpoint_uri / database_name / container_name string terraform-azurerm-cosmosdb-account
identity_id string terraform-azurerm-user-assigned-identity
primary_key / secondary_key string (sensitive) provisioned out of band (key auth only)

Emits

Output Description Consumed by
id Resource ID of the routing endpoint (emitted first). terraform-azurerm-iothub-route
name Endpoint name. routing rules referencing the endpoint by name

📚 Example Library

1 · Minimal, secure default (identityBased)

The empty-ish call: managed-identity authentication, no account key.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"
}

🔒 authentication_type defaults to identityBased; the IoT Hub's system-assigned identity writes documents, and no key touches state.

2 · Explicit identityBased with a user-assigned identity

Pin the endpoint to a specific user-assigned identity on the hub.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type = "identityBased"
  identity_id         = module.iot_identity.id
}

🔐 identity_id must be one of the IoT Hub's assigned identities; grant it a Cosmos DB data-plane role on the target account.

3 · Key-based auth with a primary key

Only when a managed identity is not an option.

variable "cosmos_primary_key" {
  type      = string
  sensitive = true
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type = "keyBased"
  primary_key         = var.cosmos_primary_key
}

🔒 primary_key is sensitive. Provision it out of band (Key Vault, a CI secret) and pass a reference — never commit it to source.

4 · Key-based auth with primary and secondary keys

Supply both keys to support rotation without downtime.

variable "cosmos_primary_key" {
  type      = string
  sensitive = true
}

variable "cosmos_secondary_key" {
  type      = string
  sensitive = true
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type = "keyBased"
  primary_key         = var.cosmos_primary_key
  secondary_key       = var.cosmos_secondary_key
}

⚠️ primary_key / secondary_key are valid only with keyBased. Prefer identityBased — key auth keeps standing secrets in play.

5 · Partitioning by a named container key

Route documents into an existing partition key path.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  partition_key_name = "deviceId"
}

ℹ️ partition_key_name should match the container's configured partition key path so documents land in the right logical partition.

6 · Synthetic partition key via a template

Generate a synthetic partition key from message metadata.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  partition_key_name     = "synthetic-pk"
  partition_key_template = "{iothub}-{DD}-{MM}-{YYYY}"
}

💡 partition_key_template builds a value from tokens such as {iothub} and date components, spreading writes evenly across partitions.

7 · Custom timeouts

Override the provider's per-operation timeouts.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  timeouts = {
    create = "30m"
    delete = "30m"
  }
}

ℹ️ Unset timeout fields fall back to provider defaults; leave timeouts as null to accept them entirely.

8 · Explicit subscription for a cross-subscription target

Point the endpoint at a Cosmos DB account in a different subscription than the hub.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-shared-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  subscription_id = "11111111-1111-1111-1111-111111111111"
}

ℹ️ Omit subscription_id to default to the subscription of the parent IoT Hub; set it only for a cross-subscription Cosmos DB target.

9 · Wiring the endpoint URI from the Cosmos DB module

Source the account URI from the Cosmos DB module. The database and container names stay literal on purpose: that module emits sql_database_ids and sql_container_ids — Resource IDs, not names — so the names a caller already supplied to its sql_databases / sql_containers maps are the honest source for these two fields.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = module.resource_group.name
  iothub_id           = module.iothub.id

  endpoint_uri   = module.cosmosdb_account.endpoint
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type = "identityBased"
  identity_id         = module.iot_identity.id
}

💡 Sourcing the URI and names from module outputs keeps a rename in one place and lets Terraform order creation correctly.

10 · Multiple endpoints with for_each

Create a keyed set of endpoints, one per container, on the same hub.

locals {
  endpoints = {
    events  = { name = "route-events", container = "device-events" }
    alerts  = { name = "route-alerts", container = "device-alerts" }
    audit   = { name = "route-audit", container = "device-audit" }
  }
}

module "cosmos_endpoint" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"
  for_each = local.endpoints

  name                = each.value.name
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = each.value.container
}

💡 A stable map key (events, alerts, audit) means adding or removing one endpoint never re-indexes the others.

11 · Naming by environment

Derive the endpoint name and target from an environment variable.

variable "environment" {
  type    = string
  default = "prod"
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos-${var.environment}"
  resource_group_name = "rg-iot-${var.environment}-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-${var.environment}-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"
}

🔒 name is force-new — changing the environment value replaces the endpoint, which is the intended behavior across distinct environments.

12 · Consuming the emitted id downstream

Feed the endpoint id into a routing rule or another module.

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"
}

output "cosmos_endpoint_id" {
  value = module.cosmos_endpoint.id
}

💡 The id is emitted first by convention and is the canonical cross-resource reference in Azure.

13 · Key-based endpoint with a template partition key

Combine key-based auth with a synthetic partition key in one call.

variable "cosmos_primary_key" {
  type      = string
  sensitive = true
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = "rg-iot-eastus"
  iothub_id           = module.iothub.id

  endpoint_uri   = "https://cosmos-telemetry-eastus.documents.azure.com:443/"
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type    = "keyBased"
  primary_key            = var.cosmos_primary_key
  partition_key_name     = "synthetic-pk"
  partition_key_template = "{deviceId}-{YYYY}"
}

⚠️ Prefer identityBased wherever the account supports it; reach for keyBased only when a data-plane role assignment is genuinely unavailable.

14 · 🏗️ End-to-end composition

Wire an IoT Hub, a Cosmos DB account, and a user-assigned identity from sibling modules into this endpoint, then route messages to it.

provider "azurerm" {
  features {}
}

module "resource_group" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"

  name     = "rg-iot-eastus"
  location = "eastus"
}

module "iot_identity" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"

  name                = "id-iot-fleet"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

module "cosmosdb_account" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-cosmosdb-account.git?ref=v1.0.0"

  name                = "cosmos-telemetry-eastus"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
}

module "iothub" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub.git?ref=v1.0.0"

  name                = "iot-fleet-eastus"
  resource_group_name = module.resource_group.name
  location            = module.resource_group.location
  sku                 = { name = "S1", capacity = 1 }
}

module "cosmos_endpoint" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-endpoint-cosmosdb-account.git?ref=v1.0.0"

  name                = "telemetry-to-cosmos"
  resource_group_name = module.resource_group.name
  iothub_id           = module.iothub.id

  endpoint_uri   = module.cosmosdb_account.endpoint
  database_name  = "telemetry"
  container_name = "device-events"

  authentication_type = "identityBased"
  identity_id         = module.iot_identity.id
  partition_key_name  = "deviceId"
}

module "iothub_route" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-iothub-route.git?ref=v1.0.0"

  name                = "route-to-cosmos"
  resource_group_name = module.resource_group.name
  iothub_name         = module.iothub.name
  routing_source      = "DeviceMessages"
  condition           = "true"
  endpoint_names      = [module.cosmos_endpoint.name]
  enabled             = true
}

output "cosmos_endpoint_id" {
  value = module.cosmos_endpoint.id
}

💡 Attribute references let Terraform order creation automatically: resource group, identity, Cosmos DB account, and hub first, then the endpoint, then the route that names it — no depends_on required. Grant iot_identity a Cosmos DB data-plane role on the account so identityBased writes succeed.


📥 Inputs

Identity & target (required)

Name Type Description
name string Endpoint name, unique across the hub's endpoint types. Force-new.
resource_group_name string Resource group of the Cosmos DB account this endpoint targets (not the hub's). Force-new.
iothub_id string Resource ID of the parent IoT Hub. Force-new.
endpoint_uri string Cosmos DB account URI. Required for every auth mode.
database_name string Cosmos DB database documents are written to.
container_name string Cosmos DB container within the database.

Authentication & configuration (optional)

Name Type Default Description
authentication_type string "identityBased" identityBased or keyBased. Secure default; enum-validated.
identity_id string null User-assigned identity ID. identityBased only.
primary_key string (sensitive) null Cosmos DB primary key. keyBased only.
secondary_key string (sensitive) null Cosmos DB secondary key. keyBased only.
partition_key_name string null Container partition key name.
partition_key_template string null Synthetic partition key template.
subscription_id string null Subscription of the endpoint; defaults to the hub's.
timeouts object(...) null Per-operation timeouts.
Full input schemas
variable "name" {
  type        = string
  description = <<-DESC
    The name of the routing endpoint. Must be unique across the IoT Hub's endpoint types.
    The names `events`, `operationsMonitoringEvents`, `fileNotifications`, and `$default` are
    reserved and cannot be used. Changing this forces a new resource to be created.
  DESC
}

variable "resource_group_name" {
  type        = string
  description = "The name of the resource group containing the **Cosmos DB account** this endpoint targets -- NOT the IoT Hub's resource group. The hub is located entirely from `iothub_id`; this value is sent as part of the endpoint payload and never appears in the endpoint's Resource ID. Changing this forces a new resource to be created."
}

variable "iothub_id" {
  type        = string
  description = "The resource ID of the parent IoT Hub this endpoint attaches to. Changing this forces a new resource to be created."
}

variable "endpoint_uri" {
  type        = string
  description = "The URI of the Cosmos DB account endpoint. Required for every authentication mode."
}

variable "database_name" {
  type        = string
  description = "The name of the Cosmos DB database that documents are written to."
}

variable "container_name" {
  type        = string
  description = "The name of the Cosmos DB container within the database that documents are written to."
}

variable "authentication_type" {
  type        = string
  default     = "identityBased"
  description = <<-DESC
    How the endpoint authenticates against the Cosmos DB account. One of `keyBased` (uses
    `primary_key` / `secondary_key`) or `identityBased` (uses a managed identity). Defaults to
    `identityBased` so no account key is required; set to `keyBased` only when a key is unavoidable.
  DESC

  validation {
    condition     = contains(["keyBased", "identityBased"], var.authentication_type)
    error_message = "authentication_type must be one of: keyBased, identityBased."
  }
}

variable "identity_id" {
  type        = string
  default     = null
  description = <<-DESC
    Resource ID of the user-assigned managed identity used to authenticate. Only valid when
    `authentication_type` is `identityBased`, and must be one of the IoT Hub's assigned identities.
    When omitted with `identityBased`, the IoT Hub's system-assigned identity is used.
  DESC
}

variable "primary_key" {
  type        = string
  default     = null
  sensitive   = true
  description = <<-DESC
    The primary key of the Cosmos DB account. Only valid when `authentication_type` is `keyBased`.
    Provision this out of band and pass a reference — never commit it to source.
  DESC
}

variable "secondary_key" {
  type        = string
  default     = null
  sensitive   = true
  description = <<-DESC
    The secondary key of the Cosmos DB account. Only valid when `authentication_type` is `keyBased`.
    Provision this out of band and pass a reference — never commit it to source.
  DESC
}

variable "partition_key_name" {
  type        = string
  default     = null
  description = "The name of the partition key associated with the Cosmos DB container."
}

variable "partition_key_template" {
  type        = string
  default     = null
  description = "The template for generating a synthetic partition key value for documents written to the container."
}

variable "subscription_id" {
  type        = string
  default     = null
  description = "The subscription ID for the endpoint. When omitted, it defaults to the subscription of the parent IoT Hub."
}

variable "timeouts" {
  type = object({
    create = optional(string)
    read   = optional(string)
    update = optional(string)
    delete = optional(string)
  })
  default     = null
  description = "Optional per-operation timeouts (create / read / update / delete)."
}

🧾 Outputs

Output Description Kind
id Azure Resource ID of the routing endpoint Passthrough
name Name of the routing endpoint, as created Passthrough
iothub_id Resource ID of the parent IoT Hub this endpoint is attached to Passthrough
iothub_name Name of the parent IoT Hub, taken from iothub_id Derived
iothub_resource_group_name Resource group holding the parent IoT Hub, taken from iothub_id Derived
iothub_subscription_id Subscription containing the parent IoT Hub, taken from iothub_id Derived
target_resource_group_name Resource group of the TARGET Cosmos DB account, read back from the endpoint record Passthrough
subscription_id Subscription recorded on the endpoint for the target account Passthrough
authentication_type The authentication mode in force, read back from Azure: keyBased or identityBased Passthrough
endpoint_uri URI of the target Cosmos DB account, read back from Azure Passthrough
target_account_host Host of the target Cosmos DB account, taken from endpoint_uri with the scheme, port and path removed Derived
database_name Cosmos DB SQL database routed documents are written into, read back from Azure Passthrough
container_name Cosmos DB SQL container routed documents are written into, read back from Azure Passthrough
identity_id User-assigned identity authenticating the endpoint, read back from Azure Passthrough
partition_key_name Partition key name recorded on the endpoint, read back from Azure Passthrough
partition_key_template Template IoT Hub uses to synthesise a partition key value for each routed document, read back from Azure Passthrough
uses_managed_identity True when delivery authenticates with a managed identity and no account key exists in this configuration or in its state Passthrough
uses_user_assigned_identity True when a specific user-assigned identity was named Derived
uses_system_assigned_identity True when the endpoint authenticates as the parent hub itself Derived
has_account_keys Whether the Cosmos DB account keys were supplied Passthrough
uses_synthetic_partition_key True when both a partition key name and a template were supplied, which is what makes IoT Hub generate a partition key value per document Derived
account_keys_are_never_emitted Constant true Constant
azure_never_returns_the_account_keys Constant true, and the single most important difference from the Event Hub and Service Bus endpoints Constant
account_keys_are_persisted_in_terraform_state True whenever the keyBased path is in use Derived
a_key_rotated_outside_terraform_is_undetectable Constant true Constant
importing_this_endpoint_does_not_recover_the_keys Constant true Constant
a_key_changed_in_configuration_does_reach_azure Constant true, and the opposite of the Event Hub and Service Bus behaviour, where editing only the key inside a connection string produces no diff at all Constant
an_account_key_is_not_scoped_to_this_container Constant true Constant
provider_default_authentication_type_is_key_based Constant true, recorded because this module deviates from it Constant
partition_key_pairing_is_stricter_than_the_platform Constant true Constant
routes_reference_this_endpoint_by_name Constant true Constant
destroy_removes_only_the_endpoint_never_its_routes Constant true Constant
endpoint_name_is_unique_across_all_endpoint_types Constant true Constant
inline_iothub_endpoint_blocks_cannot_be_mixed_with_this_module Constant true Constant
every_change_rewrites_the_whole_parent_hub Constant true Constant
changes_to_sibling_endpoints_serialise_on_a_hub_lock Constant true Constant
replacement_deletes_before_it_recreates Constant true Constant
force_new_fields The inputs that force replacement rather than an in-place update Derived
write_permission_on_the_target_is_not_verifiable_here Constant true Constant

🔒 No plaintext secret is emitted. primary_key and secondary_key are sensitive = true inputs and are never surfaced as outputs.


🧠 Architecture Notes

  • Single keystone, no children. This module owns exactly one resource, azurerm_iothub_endpoint_cosmosdb_account.this. Routing rules that direct traffic to the endpoint are sibling resources, referenced by name rather than created here.
  • Force-new fields. name, resource_group_name, and iothub_id all force replacement when changed. Plan carefully before altering any of them on a live endpoint — Terraform will destroy and recreate it, briefly interrupting message delivery.
  • endpoint_uri is always required. Unlike the other IoT Hub endpoint types, where the URI is only supplied for identity-based auth, the Cosmos DB endpoint needs the account URI (plus database_name and container_name) for both keyBased and identityBased modes.
  • keyBased vs identityBased. identityBased (the default) authenticates with a managed identity — either the hub's system-assigned identity or a user-assigned identity named by identity_id. keyBased authenticates with primary_key / secondary_key. The key inputs and identity_id are mutually exclusive: supply keys only with keyBased, and identity_id only with identityBased.
  • Sensitive keys, no state leakage of outputs. primary_key and secondary_key are marked sensitive = true, so they are redacted in plan output; the module emits no secret as an output.
  • Partitioning is in-place. partition_key_name and partition_key_template are updatable without replacement and control how routed documents are partitioned in the container.
  • features {} dependence. The provider "azurerm" block — including its mandatory features {} block — belongs to the caller's root module. If a plan fails to initialize in isolation, a missing caller-side features {} block is the usual cause.
  • No tags. The Cosmos DB routing endpoint resource does not support Azure tags, so the universal tail is timeouts only.

🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Authentication authentication_type = "identityBased" (managed identity) set to "keyBased" and supply primary_key / secondary_key
Account keys none accepted; nothing in state pass primary_key / secondary_key (sensitive) only under keyBased
Secret output never emitted —
Tags not supported by this resource; none accepted —
  • The enum that selects authentication defaults to the locked-down member (identityBased), enforced with a validation {} block listing the legal values.
  • The module accepts secrets only where unavoidable (keyBased), marks them sensitive, and instructs callers to provision them out of band.
  • The module emits no secret; only the endpoint id and name leave the module.

🚀 Runbook

# Initialize without a backend (plan-only, static analysis).
terraform init -backend=false

# Validate types and references.
terraform validate

# Confirm formatting.
terraform fmt -check
  • Pin the module with ?ref=v1.0.0 — never a branch.
  • No cloud apply is performed by this workflow; a human runs terraform apply from CI.

🧪 Testing

The offline proof gate:

  • terraform init -backend=false — resolves the provider without contacting a backend.
  • terraform validate — proves every input type, the authentication_type enum, and every attribute reference is well-formed.
  • terraform fmt -check — confirms canonical formatting.

What only terraform plan (against a configured provider) exercises: whether the named IoT Hub, Cosmos DB account, database, and container actually exist, whether the endpoint name collides with a reserved or existing name, and — for identityBased — whether the hub's identity holds a Cosmos DB data-plane role on the account. Validation cannot see live tenant state.


💬 Example Output

$ terraform output
id   = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-iot-eastus/providers/Microsoft.Devices/IotHubs/iot-fleet-eastus/routingEndpoints/telemetry-to-cosmos"
name = "telemetry-to-cosmos"

🔍 Troubleshooting

Symptom Cause Fix
IotHub not found / 404 on apply iothub_id does not match an existing hub. (resource_group_name plays no part in locating the hub -- it identifies the target resource's group.) Confirm the hub exists in the target resource group and subscription; source iothub_id from the IoT Hub module's output.
endpoint_uri rejected / documents never written The Cosmos DB account URI, database, or container is wrong, or the URI was omitted. Supply the account URI for every auth mode, and confirm database_name / container_name exist on the account.
Authorization failed writing to Cosmos DB (identityBased) The IoT Hub identity lacks a Cosmos DB data-plane role on the account. Assign a data-plane role such as Cosmos DB Built-in Data Contributor to the hub's identity on the target account.
Endpoint name rejected as reserved name is one of events, operationsMonitoringEvents, fileNotifications, or $default. Choose a name outside the reserved set that is unique across the hub's endpoint types.
primary_key ignored or an argument error Keys were supplied with identityBased, or identity_id was supplied with keyBased. Match the auth mode to its inputs: keys with keyBased, identity_id with identityBased.
Plan shows the endpoint being replaced after a minor edit A force-new field (name, resource_group_name, or iothub_id) changed. Confirm the replacement is intended; if not, revert the field. Use partition-key or timeout edits for in-place changes.
provider not initialized / features error The caller's root module is missing the provider "azurerm" { features {} } block. Add the provider block with features {} to the root module; this module intentionally omits it.

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."