Bridging the Gap: Using Bicep Deployment Scripts to Create Entra Security Groups and Assign Entra Roles

Use Bicep deploymentScripts plus Microsoft Graph to automate Entra security group creation and Entra role assignments when the resource module does not support the scenario yet.

If you’ve been building out identity automation with Bicep and Azure Verified Modules (AVM), you’ve probably hit this at least once:

You can deploy most Azure resources cleanly with modules, but some Entra ID scenarios still require a workaround.

In my case, I needed to:

  1. Create a Microsoft Entra security group
  2. Assign an Entra role to that group

At the time of writing, the module path I wanted to use did not fully support this workflow end-to-end. So I used a pattern that has become one of my go-to options for edge cases: Bicep deploymentScripts with Microsoft Graph.


Why deploymentScripts works well here

deploymentScripts gives you a controlled way to run script logic inside your IaC deployment.

That means you can keep your deployment flow centralized in Bicep while still handling operations that currently need imperative logic.

For Entra identity operations, this is especially useful because you often need to:

  • Query if an object already exists
  • Create it only when needed
  • Fetch IDs for downstream assignments
  • Call Microsoft Graph endpoints that don’t map 1:1 to current module coverage

In short: Bicep stays the orchestrator, script handles the gap.


The design pattern

Here’s the high-level flow I used (matching my microsoft-graph/groups/AssignableToEntraRole module):

  1. Bicep deployment starts at subscription scope (or resource-group scope if preferred)
  2. A user-assigned managed identity is attached to the deployment script
  3. Script authenticates to Microsoft Graph using that managed identity
  4. Script checks for existing group by mailNickname (idempotency key)
  5. Script creates the security group if missing
  6. Script assigns the Entra role to the group if not already assigned
  7. Script returns outputs (resourceId, displayName, mailNickname) back to Bicep

This keeps repeated deployments safe and predictable.


Prerequisites

Before you run this pattern, ensure the identity used by deploymentScripts has sufficient Graph permissions and directory privileges.

At minimum, validate access for:

  • Group creation and updates in Entra ID
  • Group member management
  • Reading application metadata when needed
  • Entra directory role assignment operations

In my baseline module, the managed identity is granted Graph app roles such as:

  • RoleManagement.ReadWrite.Directory
  • Group.ReadWrite.All
  • GroupMember.ReadWrite.All
  • Application.Read.All

Depending on your tenant setup and governance model, you may need admin consent and privileged setup to grant these.

Info

Use a dedicated managed identity for deployment scripts and scope permissions as tightly as possible. Least privilege is your friend here.


Baseline example: role-assignable group module call

This is the same shape as my baseline example (AssignableToEntraRole/examples/basic/main.bicep):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
param userManagedIdentityName string

module roleAssignableGroup '../../main.bicep' = {
	name: 'create-role-assignable-group-example'
	params: {
		name: 'create-role-assignable-group-example'
		displayName: 'Contoso SQL Server Admins'
		mailNickname: 'contoso-sql-server-admins'
		memberObjectIds: [
			'11111111-1111-1111-1111-111111111111'
		]
		entraRoleDefinitionIds: [
			'88d8e3e3-8f55-4a1e-953a-9b9898b8876b'
		]
		entraRoleDirectoryScopeId: '/'
		userManagedIdentityName: userManagedIdentityName
	}
}

output resourceId string = roleAssignableGroup.outputs.resourceId
output displayName string = roleAssignableGroup.outputs.displayName
output mailNickname string = roleAssignableGroup.outputs.mailNickname

The module wraps a Microsoft.Resources/deploymentScripts resource under the hood.


Baseline script behavior (inside the module)

The module uses kind: 'AzureCLI' and drives Microsoft Graph with az rest.

The key implementation points are:

  1. Resolve existing group by mailNickname
  2. Create group with isAssignableToRole: true if missing
  3. Add members idempotently (with retries for Entra replication delay)
  4. Assign requested directory roles when missing
  5. Write outputs to $AZ_SCRIPTS_OUTPUT_PATH
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
existing_group_id=$(az rest \
	--method GET \
	--url "https://graph.microsoft.com/v1.0/groups?\$filter=mailNickname eq '${mail_nickname}'&\$select=id,displayName,mailNickname" \
	--query "value[0].id" \
	--output tsv || true)

if [[ -z "${existing_group_id}" ]]; then
	create_body=$(jq -n \
		--arg displayName "${display_name}" \
		--arg mailNickname "${mail_nickname}" \
		'{
			displayName: $displayName,
			mailEnabled: false,
			mailNickname: $mailNickname,
			securityEnabled: true,
			isAssignableToRole: true,
			groupTypes: []
		}')

	group_json=$(az rest --method POST --url "https://graph.microsoft.com/v1.0/groups" --headers "Content-Type=application/json" --body "${create_body}")
	group_id=$(echo "${group_json}" | jq -r '.id')
else
	group_id="${existing_group_id}"
fi

This is the same workaround pattern documented in the module README and is ideal when native Graph Bicep support for role-assignable groups is still limited.


Idempotency and safety notes

This pattern is powerful, but identity changes are sensitive. A few practical guardrails:

  • Prefer mailNickname (or another immutable key) for idempotent lookup
  • Add a stable marker (for example, description prefix or custom naming convention)
  • Validate role assignment state before writing
  • Keep script timeout realistic for directory propagation delays
  • Capture script logs to troubleshoot tenant permission issues quickly

Also remember that Entra replication can be eventually consistent, so small delays between create/read operations are normal.


When to use this approach (and when not to)

Use this when:

  • You need Entra identity operations not fully covered by current module support
  • You want to keep deployments inside a Bicep-first pipeline
  • You need deterministic, repeatable setup logic in CI/CD

Avoid this when:

  • Native module support exists and is stable for your exact scenario
  • You can decouple identity bootstrap from infrastructure deployment
  • Your security team requires a dedicated identity provisioning workflow/toolchain

Wrap up

Until full module support lands for every Entra scenario, deploymentScripts is a practical bridge.

You get to keep the benefits of IaC while still handling real-world identity provisioning requirements—like creating Entra security groups and assigning Entra roles—without manual portal steps.

If you’re already using Bicep in production, this pattern is worth adding to your toolbox.

In a follow-up post, I’ll share a complete reusable module pattern (parameters, script file layout, and CI validation) so this can be dropped into any landing-zone deployment.

Share with your network!

Built with Hugo - Theme Stack designed by Jimmy