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

Created 21 Oct 2026 · Modified 23 Oct 2026 · 1 version

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 build fails the job on BCP035/036/037/081 warnings.
  • what-if runs and is reviewed before create.
  • 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

ConfigurationWhat happens
Subnets declared as separate Microsoft.Network/virtualNetworks/subnets resources, VNet redeployed without themThe 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 fileReset 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 GUIDnewGuid(): 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-15New 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 resourcesDeletes 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 defaultsA 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 pipelinesDeploymentActiveAndUneditable
dependsOn lists copied everywhereSlower 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-if output from the run before.
  • Works in one subscription, fails in another: providers, quotas, policy or region (reference/arm/errors.md), not the file.

Read this document as markdown — the same bytes an agent receives.