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.
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.thisbound to an existing IoT Hub byiothub_id. - 🔐 Authenticates with a managed identity (
identityBased) by default — no account key is required unless the caller opts intokeyBased. - 🌐 Requires
endpoint_uri(the Cosmos DB account URI) for every authentication mode, plusdatabase_nameandcontainer_namenaming the write target. - 🗝️ Supports
keyBasedauth via sensitiveprimary_key/secondary_keyinputs — provisioned out of band, never committed. - 🧩 Exposes optional
partition_key_nameandpartition_key_templateto control how routed documents are partitioned in the container. - ⏱️ Exposes per-operation
timeouts; this resource does not support Azuretags. - 📤 Emits the endpoint's resource
idfirst, then itsname, 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.
If this module saves you time, a little support goes a long way:
- ⭐ Star the repository on GitHub.
- 🔗 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
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;
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.
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;
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. |
| 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, andiothub_idare force-new — changing any of them destroys and recreates the endpoint. - 🌐
endpoint_uriis 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_nameandcontainer_nameare likewise always required. - 🗝️
primary_key/secondary_keyare valid only withauthentication_type = "keyBased". Supplying them withidentityBasedis a configuration error. - 🔐
identity_idis valid only withauthentication_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
namemust be unique across the hub's endpoint types;events,operationsMonitoringEvents,fileNotifications, and$defaultare reserved and cannot be used. - 🧩
partition_key_nameandpartition_key_templateshape how routed documents are partitioned; they are in-place updatable, not force-new.
Least-privilege at the parent IoT Hub scope:
Microsoft.Devices/IotHubs/writeandMicrosoft.Devices/IotHubs/readon the IoT Hub.Contributorscoped 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
identityBasedrouting, 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.
- 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
identityBasedauth, a managed identity assigned to the IoT Hub with data access to the Cosmos DB account. - The
Microsoft.Devicesresource provider registered on the subscription.
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
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 mandatoryfeatures {}block. This module declares no provider configuration.
🔒 With no
authentication_typeset, the endpoint usesidentityBased— no account key is present in configuration or state.
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 |
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_typedefaults toidentityBased; 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_idmust 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_keyissensitive. 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_keyare valid only withkeyBased. PreferidentityBased— 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_nameshould 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_templatebuilds 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
timeoutsasnullto 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_idto 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"
}🔒
nameis 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
idis 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}"
}
⚠️ PreferidentityBasedwherever the account supports it; reach forkeyBasedonly 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_onrequired. Grantiot_identitya Cosmos DB data-plane role on the account soidentityBasedwrites succeed.
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)."
}| 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_keyandsecondary_keyaresensitive = trueinputs and are never surfaced as outputs.
- 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 bynamerather than created here. - Force-new fields.
name,resource_group_name, andiothub_idall 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_uriis 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 (plusdatabase_nameandcontainer_name) for bothkeyBasedandidentityBasedmodes.- keyBased vs identityBased.
identityBased(the default) authenticates with a managed identity — either the hub's system-assigned identity or a user-assigned identity named byidentity_id.keyBasedauthenticates withprimary_key/secondary_key. The key inputs andidentity_idare mutually exclusive: supply keys only withkeyBased, andidentity_idonly withidentityBased. - Sensitive keys, no state leakage of outputs.
primary_keyandsecondary_keyare markedsensitive = true, so they are redacted in plan output; the module emits no secret as an output. - Partitioning is in-place.
partition_key_nameandpartition_key_templateare updatable without replacement and control how routed documents are partitioned in the container. features {}dependence. Theprovider "azurerm"block — including its mandatoryfeatures {}block — belongs to the caller's root module. If a plan fails to initialize in isolation, a missing caller-sidefeatures {}block is the usual cause.- No tags. The Cosmos DB routing endpoint resource does not support Azure
tags, so the universal tail istimeoutsonly.
| 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 avalidation {}block listing the legal values. - The module accepts secrets only where unavoidable (
keyBased), marks themsensitive, and instructs callers to provision them out of band. - The module emits no secret; only the endpoint
idandnameleave the module.
# 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 applyfrom CI.
The offline proof gate:
terraform init -backend=false— resolves the provider without contacting a backend.terraform validate— proves every input type, theauthentication_typeenum, 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.
$ 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"| 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. |
azurerm_iothub_endpoint_cosmosdb_accountresourceazurerm_iothubresource- IoT Hub message routing — Microsoft Learn
- Sibling modules:
terraform-azurerm-iothub-endpoint-eventhub,terraform-azurerm-iothub-endpoint-servicebus-queue,terraform-azurerm-iothub-endpoint-servicebus-topic,terraform-azurerm-iothub-endpoint-storage-container - This module's
SCOPE.md— the cross-module contract.
💙 "Infrastructure as Code should be standardized, consistent, and secure."