BCPNFR24 - Deterministic Deployment Names
ID: BCPNFR24 - Category: Naming/Composition - Deterministic Deployment Names
When a module references child, utility, or other modules, the deployment name MUST be deterministic. This means the deployment name must produce the same value for the same set of inputs across repeated deployments.
Why deterministic?
Azure Resource Manager has an 800-deployment limit per scope (resource group, subscription, management group, tenant). Non-deterministic names (e.g., those incorporating timestamps or utcNow()) create a new deployment object on every run, which can lead to this limit being reached over time.
While an automatic cleanup process exists for resource group and subscription scopes, it can take some time to take effect. Due to eventual consistency in the backend, the deployment count may not reflect the cleanup immediately, which can lead to failed deployments even when the actual number of deployments is below the 800 limit. Additionally, automatic cleanup does not apply to management group or tenant scopes.
We are actively working with the product team to enhance the cleanup process. In the meantime, deterministic deployment names provide a reliable way to keep deployment counts stable by overwriting previous deployment objects rather than creating new ones.
Deterministic deployment names cause Azure to overwrite the previous deployment object, keeping the deployment count stable regardless of how many times the module is deployed.
Requirement
Module owners MUST construct deployment names for referenced modules using uniqueString() seeded with the parent resource’s ID (<parentResource>.id) and location, rather than deployment().name, subscription().id, resourceGroup().id, utcNow(), or other non-deterministic or scope-level values.
The deployment name MUST follow the pattern:
'${uniqueString(<parentResource>.id, location)}-<ChildModuleDescriptor>-${index}'Where:
| Segment | Description |
|---|---|
uniqueString(<parentResource>.id, location) | A deterministic hash derived from the parent resource’s resource ID and deployment location. This is both unique per resource instance and stable across deployments. |
<ChildModuleDescriptor> | A short, human-readable label identifying the child module being deployed (e.g., DB, Subnet, FederatedIdentityCred). |
${index} | The loop index variable, included when deploying in a loop. Omit for single (non-looped) deployments. |
location parameter
If location is not available, for example when deploying a global resource that does not have a location property, it is acceptable to omit it. However, the <parentResource>.id MUST always be included as the primary seed for uniqueString.
Why parent resource ID?
Using the parent resource’s ID as the uniqueString seed provides two critical properties:
- Deterministic — the same parent resource always produces the same hash, so repeated deployments overwrite rather than accumulate.
- Collision-free — different parent resource instances produce different hashes, so deploying multiple instances of the same module type within the same scope does not cause naming collisions.
Why not subscription().id and resourceGroup().id separately?
The parent resource’s ID (e.g., /subscriptions/.../resourceGroups/.../providers/.../resourceName) already contains the subscription ID and resource group ID as segments. Using <parentResource>.id as a single input to uniqueString captures all of this context in one value, keeping the code concise and readable rather than passing multiple scope-level values separately.
Supporting multiple deployments of the same module at the same scope
A common scenario is deploying the same module type more than once within the same scope — for example, two different SQL servers each with their own set of databases, or two user-assigned identities each with their own federated credentials. Because the parent resource ID is unique per resource instance, the resulting deployment names will differ even when the child module type and index are identical. This ensures that parallel deployments of the same module at the same scope do not collide.
Other approaches fail on one or both of these properties:
| Approach | Deterministic? | Collision-free? | Issue |
|---|---|---|---|
deployment().name | ❌ | ✅ | Changes every deployment; hits 800-limit |
utcNow() / timestamps | ❌ | ✅ | Changes every deployment; hits 800-limit |
subscription().id + resourceGroup().id | ✅ | ❌ | Same hash for all resources in the same RG; collisions when deploying multiple instances |
<parentResource>.id, location | ✅ | ✅ | Recommended — stable and unique per instance |
Examples
Example 1: Single child module deployment
resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }
module server_database 'database/main.bicep' = {
name: '${uniqueString(server.id, location)}-Sql-DB'
params: {
serverName: server.name
(...)
}
}Example 2: Child module deployment in a loop
resource server 'Microsoft.Sql/servers@2023-05-01-preview' = { ... }
module server_databases 'database/main.bicep' = [for (database, index) in (databases ?? []): {
name: '${uniqueString(server.id, location)}-Sql-DB-${index}'
params: {
serverName: server.name
(...)
}
}]