Bicep that redeploys cleanly (idempotent infrastructure)
The known-good shape for Bicep that can run on every commit without drift, outages or failures — deterministic names, explicit API versions with types, subnets inside the VNet, role assignments named with guid(), explicit Key Vault permission model, deletion through deployment stacks not complete mode, what-if in CI, no secrets in outputs — and the known-bad configurations with the exact errors or silent damage each one causes.
markdownpatternv1W6T3-4PCGstage: buildtrust: officialservice: bicepservice: arminterface: iacinterface: ciidempotent Bicepredeploy Bicep safelyincremental mode properties resetsubnets deleted on redeployInUseSubnetCannotBeDeletedrole assignment guid nameRoleAssignmentUpdateNotPermittedenableRbacAuthorization explicitcomplete mode deletesdeployment stacks instead of complete modewhat-if in CIoutputs secrets deployment historyuniqueString namingBCP081 no typesdeployment name collisions DeploymentActiveAndUneditable
Bicep that redeploys cleanly
A Bicep file is idempotent when deploying it twice changes nothing the second time. Incremental mode makes that possible but doesn't guarantee it: it reapplies every property of every resource in the file, so anything the file omits or computes differently each run is reset, renamed or refused.
Known-good
param location string = resourceGroup().location
param appPrincipalId string
var suffix = uniqueString(resourceGroup().id)
var secretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6'
resource vnet 'Microsoft.Network/virtualNetworks@2026-01-01' = {
name: 'vnet-${suffix}'
location: location
properties: {
addressSpace: { addressPrefixes: ['10.20.0.0/16'] }
subnets: [ // subnets inside the VNet, not as child resources
{ name: 'apps', properties: { addressPrefix: '10.20.1.0/24' } }
{ name: 'data', properties: { addressPrefix: '10.20.2.0/24' } }
]
}
}
resource kv 'Microsoft.KeyVault/vaults@2026-05-15' = {
name: take('kv-${suffix}', 24)
location: location
properties: {
tenantId: subscription().tenantId
sku: { family: 'A', name: 'standard' }
enableRbacAuthorization: true // explicit: the default depends on the API version
enablePurgeProtection: true
}
}
resource kvRead 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(kv.id, appPrincipalId, secretsUserRoleId)
scope: kv
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', secretsUserRoleId)
principalId: appPrincipalId
principalType: 'ServicePrincipal'
}
}
output keyVaultUri string = kv.properties.vaultUri // an address, not a secret
And in the pipeline:
bicep buildfails the job onBCP035/036/037/081warnings.what-ifruns and is reviewed beforecreate.- Each run uses a unique deployment name (with a timestamp), or one name per environment that never runs twice at once.
- Deletions go through a deployment stack (
reference/arm/deployment-stacks.md).
Known-bad
| Configuration | What happens |
|---|---|
Subnets declared as separate Microsoft.Network/virtualNetworks/subnets resources, VNet redeployed without them | The VNet's subnets is reapplied as empty: subnets are removed, or the deploy fails with InUseSubnetCannotBeDeleted if something uses them |
| A property set by hand in the portal, absent from the file | Reset to its default on the next deploy (incremental mode reapplies, it doesn't merge) |
Role assignment named from newGuid() (a parameter default) or a hard-coded GUID | newGuid(): each run asks for the same principal, role and scope under a new name → RoleAssignmentExists (409). Hard-coded GUID with a recreated identity: RoleAssignmentUpdateNotPermitted |
Key Vault without enableRbacAuthorization, API version moved from 2025-05-01 to 2026-05-15 | New vaults become RBAC vaults and the template's accessPolicies are ignored: 403 ForbiddenByRbac for every consumer. Existing vaults keep their model |
--mode Complete to delete removed resources | Deletes everything in the group that isn't in the file, including resources other teams deployed; DeploymentFailedCleanUp when it lacks rights. Microsoft is deprecating complete mode |
Resource names from newGuid() or utcNow() parameter defaults | A new resource every run; the old ones stay |
An API version Bicep has no types for (BCP081) | No property checking: typos deploy and fail in Azure, or silently do nothing |
Secrets in output (keys, connection strings) | Stored in plain text in deployment history, readable by anyone with read access to the group. The linter warns: outputs-should-not-contain-secrets |
| One fixed deployment name used by parallel pipelines | DeploymentActiveAndUneditable |
dependsOn lists copied everywhere | Slower deployments, circular dependencies, and job-size limits on large loops |
Which side is wrong
- The second deploy fails, the first worked: look for a name or property computed differently per run (
newGuid,utcNow, a changed principal) or a concurrent deployment. - The second deploy succeeds but something broke: a property or child resource the file doesn't declare was reset. Diff
what-ifoutput from the run before. - Works in one subscription, fails in another: providers, quotas, policy or region (
reference/arm/errors.md), not the file.