Summary
Role assignments are not idempotent to declare, and neither Bicep nor azd can express "create this grant only if an equivalent one doesn't already exist". We need upsert-by-tuple ensure semantics.
What happens
azd provision fails with RoleAssignmentExists when a matching (principalId, roleDefinitionId, scope) tuple already exists under a different assignment name (a GUID) — e.g. an out-of-band grant applied by another process/team in a locked resource group.
Why a Bicep if condition does not solve it
A Bicep if (...) only gates whether a resource is declared; to gate it correctly you'd need to answer "does a role assignment for this (principal, role, scope) already exist, under any name?" — which ARM/Bicep can't:
- No query/enumeration primitive — there's no
listRoleAssignments() to test whether a matching tuple exists, so the if condition has no truthful input.
existing requires the name, which you don't have — existing by a computed deterministic GUID only matches assignments created with that name; the out-of-band grant has a different, arbitrary name, so it finds nothing.
- Deterministic naming (
guid(scope, principalId, roleId)) only makes your own assignments idempotent — it does nothing about a conflicting assignment created by someone else under a different name; ARM still returns RoleAssignmentExists on the tuple collision.
- No native upsert/ensure — ARM/Bicep has no "ensure a grant for this tuple exists; treat an existing one under any name as success."
In short, an if condition needs a boolean derived from live RBAC state, and Bicep has no way to obtain it — the gate has no input.
Current workaround
hooks/lib/ensure-role-assignment.ts — a thin wrapper over az role assignment create that treats RoleAssignmentExists as success. We removed the offending roleAssignments from Bicep entirely.
Ask
First-class idempotent "ensure" semantics for role assignments — an upsert keyed on the (principal, role, scope) tuple that succeeds if the grant already exists under any name, expressible declaratively rather than via an imperative az hook.
Note: this may be more of a Bicep/ARM platform gap than a pure azd gap; filing here for visibility and because azd is where the failure surfaces.
Context: encountered while deploying a multi-component AI/chat service (App Service + Function App + Logic App + Foundry hosted agent) end-to-end with azd provision/deploy in Azure/azure-sdk-tools tools/sdk-ai-bots/deployment. We currently work around this with a TypeScript hook.
Summary
Role assignments are not idempotent to declare, and neither Bicep nor
azdcan express "create this grant only if an equivalent one doesn't already exist". We need upsert-by-tuple ensure semantics.What happens
azd provisionfails withRoleAssignmentExistswhen a matching (principalId, roleDefinitionId, scope) tuple already exists under a different assignment name (a GUID) — e.g. an out-of-band grant applied by another process/team in a locked resource group.Why a Bicep
ifcondition does not solve itA Bicep
if (...)only gates whether a resource is declared; to gate it correctly you'd need to answer "does a role assignment for this (principal, role, scope) already exist, under any name?" — which ARM/Bicep can't:listRoleAssignments()to test whether a matching tuple exists, so theifcondition has no truthful input.existingrequires the name, which you don't have —existingby a computed deterministic GUID only matches assignments created with that name; the out-of-band grant has a different, arbitrary name, so it finds nothing.guid(scope, principalId, roleId)) only makes your own assignments idempotent — it does nothing about a conflicting assignment created by someone else under a different name; ARM still returnsRoleAssignmentExistson the tuple collision.In short, an
ifcondition needs a boolean derived from live RBAC state, and Bicep has no way to obtain it — the gate has no input.Current workaround
hooks/lib/ensure-role-assignment.ts— a thin wrapper overaz role assignment createthat treatsRoleAssignmentExistsas success. We removed the offendingroleAssignmentsfrom Bicep entirely.Ask
First-class idempotent "ensure" semantics for role assignments — an upsert keyed on the (principal, role, scope) tuple that succeeds if the grant already exists under any name, expressible declaratively rather than via an imperative
azhook.Note: this may be more of a Bicep/ARM platform gap than a pure
azdgap; filing here for visibility and becauseazdis where the failure surfaces.Context: encountered while deploying a multi-component AI/chat service (App Service + Function App + Logic App + Foundry hosted agent) end-to-end with
azdprovision/deploy in Azure/azure-sdk-toolstools/sdk-ai-bots/deployment. We currently work around this with a TypeScript hook.