Skip to main content

dev-bicep

Date: 2025-07-19

[[TOC]]

Tools

Bicep Playground { azure.github.io }

I hope it still works! Amazing online tool to learn bicep!

I've used it with Azure Bicep Cheat Sheet below.

2025-07-19

Official Reference from MSFT

2025-07-19 Microsoft.Automation/automationAccounts - Bicep, ARM template & Terraform AzAPI reference | Microsoft Learn { learn.microsoft.com }

Yay!

2025-07-20 azure-docs-bicep-samples/samples/loops/loopproperty.bicep at main · Azure/azure-docs-bicep-samples { github.com }

Maybe okayish, but a bit outdated. Check the recent resource version.

image-20250719231321178

Bicep Guide

nnellans/bicep-guide: Bicep Guide { github.com }

by Nathan Nellans

image-20250719230246949

Azure Bicep Cheat Sheet

Source: johnlokerse/azure-bicep-cheat-sheet: Quick-reference guide on Azure Bicep 💪🏻 { github.com }

img

This is a copy of the cheat sheet created by John Lokerse.


What is a cheat sheet?

A cheat sheet is a concise set of notes or a reference guide used for quick retrieval of essential information. It's often a single page that contains summaries, commands, formulas, or procedures that someone might need to reference frequently, especially when learning a new topic or skill.

What is Azure Bicep?

Azure Bicep is a domain-specific language (also known as DSL) designed by Microsoft for defining and deploying Azure resources in a declarative manner. It's the next generation of Azure Resource Manager (ARM) templates, offering a cleaner syntax, improved type safety, and better support for modularization. While ARM templates use JSON syntax, Bicep uses a more concise syntax that aims to make it easier for developers to author and maintain Azure deployments.

Topics

Topics

[!NOTE] Click the arrow next to a topic to expand its content.

Basics

Declarations of new and existing resources, variables, parameters and outputs, etcetera.

Create a resource

GitHub Copilot Prompt - Learn more on resource creation

how to define a resource in azure bicep

resource resourceName 'ResourceType@version' = {
name: 'exampleResourceName'
properties: {
// resource properties here
}
}

Create a child resource

GitHub Copilot Prompt - Learn more about creating child resources

Via name

resource resVnet 'Microsoft.Network/virtualNetworks@2022-01-01' = {
name: 'my-vnet'
}

resource resChildSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = {
name: '${resVnet}/my-subnet'
}

Via parent property

resource resVnet 'Microsoft.Network/virtualNetworks@2022-01-01' = {
name: 'my-vnet'
}

resource resChildSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = {
name: 'my-subnet'
parent: resVnet
}

Via parent resource

resource resVnet 'Microsoft.Network/virtualNetworks@2022-01-01' = {
name: 'my-vnet'

resource resChildSubnet 'subnets' = {
name: 'my-subnet'
}
}

Reference to an existing resource

resource resKeyVaultRef 'Microsoft.KeyVault/vaults@2019-09-01' = existing {
name: 'myExistingKeyVaultName'
}

Access a nested resource (::)

resource resVnet 'Microsoft.Network/virtualNetworks@2022-01-01' existing = {
name: 'my-vnet'
resource resChildSubnet 'subnets' existing = {
name: 'my-subnet'
}
}

// access child resource
output outChildSubnetId string = resVnet::resChildSubnet.id

Declare a variable

var varEnvironment = 'dev'

There is no need to declare a datatype for a variable, because the type is inferred from the value.

Declare a parameter

param parStorageAccountName string
param parLocation string = resourceGroup().location

Available datatypes are: string, bool, int, object, array and custom (user defined type).

Declare a secure parameter

@secure()
param parSecureParameter string

Declare an output

resource resPublicIp 'Microsoft.Network/publicIPAddresses@2023-02-01' ={
name: parPublicIpName
tags: parTags
location: parLocation
zones: parAvailabilityZones
sku: parPublicIpSku
properties: parPublicIpProperties
}

output outPublicIpId string = resPublicIp.id
output outMyString string = 'Hello!'

Available datatypes are: string, bool, int, object, array and custom (user defined type).

String interpolation

var varGreeting = 'Hello'
output outResult string = '${varGreeting} World'

Multi-line strings

var varMultiLineString = '''
This is a
Muli-line string
variable.
'''
Modules

Split your deployment into smaller, reusable components.

Create a module

GitHub Copilot Prompt

module modVirtualNetwork './network.bicep' = {
name: 'networkModule'
params: {
parLocation: 'westeurope'
parVnetName: 'my-vnet-name'
}
}

Reference to a module using a bicep registry

module modBicepRegistryReference 'br/<bicep registry name>:<file path>:<tag>' = {
name: 'deployment-name'
params: {}
}
Conditions

Resource definitions based on conditions.

GitHub Copilot - Learn more about conditions

If condition

param parDeployResource bool

resource resDnsZone 'Microsoft.Network/dnszones@2018-05-01' = if (parDeployResource) {
name: 'myZone'
location: 'global'
}

Ternary if/else condition

param parEnvironment string

var varSku = parEnvironment == 'prod' ? 'premium' : 'standard'
Loops

Loop constructions.

GitHub Copilot - Learn more about loops

foreach using an array

param parStorageAccountNames array = [
'storageaccount1'
'storageaccount2'
'storageaccount3'
]

resource resStorageAccounts 'Microsoft.Storage/storageAccounts@2021-04-01' = [for name in parStorageAccountNames: {
name: name
location: 'westeurope'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}]

foreach using an array of objects

param parStorageAccountNames array = [
{
name: 'storageaccount1'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
{
name: 'storageaccount2'
kind: 'StorageV2'
sku: {
name: 'Standard_LRS'
}
}
]

resource resStorageAccounts 'Microsoft.Storage/storageAccounts@2021-04-01' = [for storageAccount in parStorageAccountNames: {
name: storageAccount.name
location: 'westeurope'
kind: storageAccount.kind
sku: {
name: storageAccount.sku
}
}]
Data manipulation

Functions used to manipulate data.

GitHub Copilot - Learn more about lambda functions

Example data

var varGroceryStore = [
{
productName: 'Icecream'
productPrice: 2
productCharacteristics: [
'Vegan'
'Seasonal'
]
}
{
productName: 'Banana'
productPrice: 4
productCharacteristics: [
'Bio'
]
}
]

filter() function

  output outProducts array = filter(varGroceryStore, item => item.productPrice >= 4)

returns

[
{
"productName": "Banana",
"productPrice": 4,
"productCharacteristics": [
"Bio"
]
}
]

map() function

output outDiscount array = map(range(0, length(varGroceryStore)), item => {
productNumber: item
productName: varGroceryStore[item].productName
discountedPrice: 'The item ${varGroceryStore[item].productName} is on sale. Sale price: ${(varGroceryStore[item].productPrice / 2)}'
})

returns

[
{
"productNumber": 0,
"productName": "Icecream",
"discountedPrice": "The item Icecream is on sale. Sale price: 1"
},
{
"productNumber": 1,
"productName": "Banana",
"discountedPrice": "The item Banana is on sale. Sale price: 2"
}
]

sort() function

output outUsingSort array = sort(varGroceryStore, (a, b) => a.productPrice <= b.productPrice)

returns

[
{
"productName": "Icecream",
"productPrice": 2,
"productCharacteristics": [
"Vegan"
"Seasonal"
]
},
{
"productName": "Banana",
"productPrice": 4,
"productCharacteristics": [
"Bio"
]
}
]
User Defined Types

Define custom complex data structures.

GitHub Copilot - Learn more on User Defined Types

Primitive types

// a string type with two allowed strings ('Standard_LRS' or 'Standard_GRS')
type skuType = 'Standard_LRS' | 'Standard_GRS'

// an integer type with one allowed value (1337)
type integerType = 1337

// an boolean type with one allowed value (true)
type booleanType = true

// Reference the type
param parMyStringType skuType
param parMyIntType integerType
param parMyBoolType booleanType

A custom type that enforced an array with a specific object structure

type arrayWithObjectsType = {
name: string
age: int
}[]

param parCustomArray arrayWithObjectsType = [
{
name: 'John'
age: 30
}
]

Optional properties in objects (using ?)

type arrayWithObjectsType = {
name: string
age: int
hasChildren: bool?
hasPets: bool?
}[]

param parCustomArray arrayWithObjectsType = [
{
name: 'John'
age: 30
}
{
name: 'Jane'
age: 31
hasPets: true
}
{
name: 'Jack'
age: 45
hasChildren: true
hasPets: true
}
]
User Defined Functions

Define custom complex expressions.

GitHub Copilot - Learn more about User Defined Functions

User-defined function syntax

func <function-name> (<parameter-name> <data-type>) <return-type> => <expression>

Basic user-defined function

func funcSayHelloTo() string => 'Hello and welcome, John Doe'

User-defined function with parameters

func funcSayHelloTo(name string) string => 'Hello and welcome, ${name}'

With multiple parameters:

func funcPersonNameAndAge(name string, age int) string => 'My name is ${name} and my age is ${age}'

User-defined function return types

func funcReturnTypeArray() array => [1, 2, 3, 4, 5]
func funcReturnTypeObject() object => {name: 'John Doe', age: 31}
func funcReturnTypeInt() int => 1337
func funcReturnTypeBool(key string) bool => contains({}, key)
func funcReturnTypeUserDefinedType() customTypeUsedAsReturnType => {
hello: 'world'
}

type customTypeUsedAsReturnType = {
hello: string
}
Compile-time imports

Import and export() enable reuse of user-defined types variables, functions.
Supported in Bicep and Bicepparam files.

GitHub Copilot - learn more about compile-time imports

export() decorator (shared.bicep)

@export()
var region = 'we'

@export()
type tagsType = {
Environment: 'Prod' | 'Dev' | 'QA' | 'Stage' | 'Test'
CostCenter: string
Owner: string
BusinessUnit: string
*: string
}

import statement

import { region, tagsType } from 'shared.bicep'

output outRegion string = region
output outTags tagsType = {
Environment: 'Dev'
CostCenter: '12345'
BusinessUnit: 'IT'
Owner: 'John Lokerse'
}

import statement with alias

using 'keyVault.bicep'
import { region as importRegion } from 'shared.bicep'

param parKeyVaultName = 'kv-${importRegion}-${uniqueString(importRegion)}'

import statement using a wildcard

import * as shared from 'shared.bicep'

output outRegion string = shared.region
output outTags shared.tagsType = {
Environment: 'Dev'
CostCenter: '12345'
BusinessUnit: 'IT'
Owner: 'John Lokerse'
}
Networking

CIDR functions to make subnetting easier.

GitHub Copilot - learn more about the networking functions

parseCidr() function

output outParseCidrInformation object = parseCidr('192.168.1.0/24')

returns

"outParseCidrInformation": {
"type": "Object",
"value": {
"broadcast": "192.168.1.255",
"cidr": 24,
"firstUsable": "192.168.1.1",
"lastUsable": "192.168.1.254",
"netmask": "255.255.255.0",
"network": "192.168.1.0"
}
}

cidrSubnet() function

output outCidrSubnet string = cidrSubnet('192.168.1.0/24', 25, 0)

returns

"outCidrSubnet": {
"type": "String",
"value": "192.168.1.0/25"
}

cidrHost() function

output outCidrHost array = [for i in range(0, 10): cidrHost('192.168.1.0/24', i)]

returns

"outCidrHost": {
"type": "Array",
"value": [
"192.168.1.1",
"192.168.1.2",
"192.168.1.3",
"192.168.1.4",
"192.168.1.5",
"192.168.1.6",
"192.168.1.7",
"192.168.1.8",
"192.168.1.9",
"192.168.1.10"
]
}
Bicepconfig

Customize your Bicep development experience.

GitHub Copilot - Learn more about bicepconfig.json

Azure Container Registry configuration

{
"moduleAliases": {
"br": {
"<bicep registry name>": {
"registry": "<url to registry>",
"modulePath": "<module path of the alias>"
}
}
}
}
Dependencies

Implicit and explicit dependencies.

GitHub Copilot - learn more about dependencies

Implicit dependency using symbolic name

resource resNetworkSecurityGroup 'Microsoft.Network/networkSecurityGroups@2019-11-01' = {
name: 'my-networkSecurityGroup'
location: resourceGroup().location
}

resource nsgRule 'Microsoft.Network/networkSecurityGroups/securityRules@2019-11-01' = {
name: '${resNetworkSecurityGroup}/AllowAllRule'
properties: {
// resource properties here
}
}

Explicit dependency using dependsOn

resource resDnsZone 'Microsoft.Network/dnsZones@2018-05-01' = {
name: 'contoso.com'
location: 'global'
}

module modVirtualNetwork './network.bicep' = {
name: 'networkModule'
params: {
parLocation: 'westeurope'
parVnetName: 'my-vnet-name'
}
dependsOn: [
resDnsZone
]
}
Deployment

Orchestration commands to deploy Azure Bicep to your Azure Environment.

Azure CLI

ScopeCommand
resourceGroupaz deployment group create --resource-group ResourceGroupName --template-file template.bicep --parameters parameters.bicepparam
subscriptionaz deployment sub create --location location --template-file template.bicep --parameters parameters.bicepparam
managementGroupaz deployment mg create --management-group-id ManagementGroupId --template-file template.bicep --parameters parameters.bicepparam
tenantaz deployment tenant create --location location --template-file template.bicep --parameters parameters.bicepparam

Azure PowerShell

ScopeCommand
resourceGroupNew-AzResourceGroupDeployment -ResourceGroupName "ResourceGroupName" -TemplateFile "template.bicep" -TemplateParameterFile "parameters.bicepparam
subscriptionNew-AzDeployment -Location "Location" -TemplateFile "template.bicep" -TemplateParameterFile "parameters.bicepparam"
managementGroupNew-AzManagementGroupDeployment -ManagementGroupId "ManagementGroupId" -Location "location" -TemplateFile "template.bicep" -TemplateParameterFile "parameters.bicepparam"
tenantNew-AzTenantDeployment -Location "Location" -TemplateFile "template.bicep" -TemplateParameterFile "parameters.bicepparam"
Target Scopes

Deployment scope definitions.

GitHub Copilot - Learn more about target scopes

Target scopes

The targetScope directive in Azure Bicep determines the level at which the Bicep template will be deployed within Azure. The default is targetScope = 'resourceGroup'.

Azure Bicep supports multiple levels of targetScope:

ScopeDescription
resourceGroupThe Bicep file is intended to be deployed at the Resource Group level.
subscriptionThe Bicep file targets a Subscription, allowing you to manage resources or configurations across an entire subscription.
managementGroupFor managing resources or configurations across multiple subscriptions under a specific Management Group.
tenantThe highest scope, targeting the entire Azure tenant. This is useful for certain global resources or policies.
targetScope = 'resourceGroup'

resource resKeyVault 'Microsoft.KeyVault/vaults@2019-09-01' = {
// key vault properties here
}

Use the scope property on modules to deploy on a different scope than the target scope:

// Uses the targetScope
module modStorageModule1 'storage.bicep' = {
name: 'storageModule1'
}

// Uses the scope of the module
module modStorageModule2 'storage.bicep' = {
name: 'storageModule2'
scope: resourceGroup('other-subscription-id', 'other-resource-group-name')
// module properties here
}
Azure Verified Modules

Microsoft building blocks for Azure Bicep right at your fingertips.

GitHub Copilot - Learn more about Azure Verified Modules

Azure Verified Modules reference

When you're writing Bicep, you can reference Azure Verified Modules (AVM) directly in your Bicep files. To get access to the IntelliSense prompt, you need the Azure Bicep VSCode extension installed. Additionally, to restore the Bicep modules successfully, make sure you have access to the Microsoft Container Registry at mcr.microsoft.com.

As an example, here is how to reference to an Azure Key Vault from the Microsoft Container Registry:

More information on Azure Verified Modules can be found here.

Popular Azure Bicep Patterns

(GPT Research)

Below are many common Bicep coding patterns used in enterprise-grade deployments. Each pattern includes a code example (with sample output where appropriate) and an explanation, along with references to official documentation or community examples.

UniqueString-based Naming

Developers often build resource names by combining meaningful prefixes with the uniqueString() function to guarantee global uniqueness. For example:

param env string = 'prod'
param appName string = 'myApp'
param storageAccountName string = '${appName}-${env}-${uniqueString(resourceGroup().id)}'

resource stg 'Microsoft.Storage/storageAccounts@2023-04-01' = {
name: storageAccountName
location: resourceGroup().location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}

Output Example: If resourceGroup().id is "RG/123" this might produce a name like myApp-prod-zcztcwvu6iyg6. The 13-character suffix (zcztcwvu6iyg6 in this case) comes from uniqueString(). This pattern ensures deterministic yet unique names (the same RG ID yields the same suffix) and incorporates a human-readable prefix for context.

Why it Emerges: Azure resource names often must be globally unique. Embedding uniqueString(resourceGroup().id) (or similar seeds like subscription ID) with a descriptive prefix creates names that differ per environment or project yet remain repeatable across deployments.

Standard Naming Convention

Many teams follow a structured naming convention (environment, application code, etc.) via string interpolation. For example:

param shortAppName string = 'app'
param shortEnv string = 'prod'
param appServiceName string = '${shortAppName}-${shortEnv}-${uniqueString(resourceGroup().id)}'

resource webApp 'Microsoft.Web/sites@2022-09-01' = {
name: appServiceName
location: resourceGroup().location
kind: 'app'
// ...
}

Output Example: With shortAppName='app', shortEnv='prod', a sample name could be app-prod-0a1b2c3d4e5f. This combines meaningful parts (app-prod) with uniqueString() to avoid collisions.

Why it Emerges: Embedding environment tags or application codes in names makes them meaningful (e.g. indicating “prod” vs “dev”), while uniqueString() adds a unique suffix. Microsoft documentation explicitly recommends using string interpolation with uniqueString to build names that are unique, deterministic, and informative.

Default Parameters and Overrides

It’s common to define resource names (or other values) as parameters with sensible defaults that users can override. For instance:

param resourcePrefix string = 'contoso' 
param location string = resourceGroup().location
param saName string = '${resourcePrefix}sa${uniqueString(resourceGroup().id)}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = {
name: saName
location: location
// ...
}

Output Example: With defaults above, the storage account might be contososa-zcztcwvu6iyg6. If the user needs a different naming scheme, they can override resourcePrefix or pass a completely different saName.

Why it Emerges: Bicep best practices advise using parameters (with defaults) for values that may change between deployments. This makes the template reusable: defaults (like low-cost SKUs or test names) apply out-of-the-box, but operators can override them.

Parameter Constraints (@allowed)

Templates often include parameters for things like environments or SKUs, with an @allowed decorator to restrict values. For example:

@allowed([
'dev'
'test'
'prod'
])
param environment string = 'dev'

@allowed([
'Standard_LRS'
'Standard_GRS'
])
param storageSku string = 'Standard_LRS'

This ensures only permitted values are supplied. In a different context, the new vs existing resource example uses @allowed for "new"/"existing" choices.

Why it Emerges: Enforcing allowed values prevents misconfiguration (e.g. disallowing unsupported SKUs). It makes the template self-documenting about valid inputs.

Secure Parameters (@secure)

Sensitive inputs (passwords, secrets) should be marked with @secure. Example:

@secure()
param adminPassword string

resource sqlServer 'Microsoft.Sql/servers@2021-02-01' = {
name: 'sql-${uniqueString(resourceGroup().id)}'
properties: {
administratorLogin: 'adminUser'
administratorLoginPassword: adminPassword
}
}

Bicep treats these values securely (they aren’t logged or stored in deployment history).

Why it Emerges: Enterprise deployments often include secrets. Marking them secure prevents accidental exposure. ARM/Bicep will mask secure parameters or values in logs, as recommended in official guidance.

Conditional Resource Deployment (if on resources)

Bicep’s if expression can toggle resource creation. For example, to deploy a DNS zone only when requested:

param deployDns bool = true

resource dnsZone 'Microsoft.Network/dnsZones@2023-07-01' = if (deployDns) {
name: 'myZone'
location: 'global'
}

If deployDns is false, the resource is skipped.

Why it Emerges: It allows one template to handle multiple scenarios (e.g. dev vs prod). Users can flip features on/off via parameters. This pattern is explicitly documented: “Use an if expression in the resource declaration. When the condition is false, the resource isn’t created”.

Conditional Modules (if on modules)

Similarly, you can conditionally deploy a module. For example:

param createWebApp bool = false

module webAppModule 'webApp.bicep' = if (createWebApp) {
name: 'deployWebApp'
params: { appName: 'myWebApp'; location: location }
}

Here webApp.bicep only runs if createWebApp == true.

Why it Emerges: Large deployments often have optional components. For instance, only deploy an App Service in certain environments. Wrapping modules in if keeps templates concise and flexible.

“New or Existing” Resources Pattern

A common enterprise pattern is letting the user choose “new” vs “existing” for a resource. For example, to either create a storage account or use an existing one:

param saName string
param location string = resourceGroup().location

@allowed(['new','existing'])
param mode string = 'new'

resource saNew 'Microsoft.Storage/storageAccounts@2023-04-01' = if (mode == 'new') {
name: saName
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}

resource saExisting 'Microsoft.Storage/storageAccounts@2023-04-01' existing = if (mode == 'existing') {
name: saName
}

output storageAccountId string =
(mode == 'new') ? saNew.id : saExisting.id

Output Example: If mode is "new", the template creates a new account and outputs its ID. If "existing", it skips creation and instead looks up the existing account’s ID.

Why it Emerges: Enterprises often integrate new deployments with existing resources. This pattern (shown in docs) uses conditional blocks and the existing keyword to flexibly handle both cases in one template.

Loops – Multiple Resource Instances

Bicep supports for loops to create multiple copies of a resource. For example, to make N storage accounts:

param location string = resourceGroup().location
param storageCount int = 3

resource storageAccts 'Microsoft.Storage/storageAccounts@2023-05-01' = [for i in range(0, storageCount): {
name: 'stg${i}${uniqueString(resourceGroup().id)}'
location: location
sku: { name: 'Standard_LRS' }
kind: 'StorageV2'
}]

Output Example: If storageCount=3, this creates 3 storage accounts named like stg0abc123..., stg1def456..., stg2ghi789.... You can then reference them as storageAccts[0], storageAccts[1], etc.

Why it Emerges: Loops eliminate repetitive code and allow dynamic scaling. This pattern is documented as “create multiple resource instances”. Each loop index (i) can be used in names or properties.

Loops – Multiple Module Deployments

In the same way, you can deploy multiple instances of a module. For example:

param location string = resourceGroup().location
param vmCount int = 2

module vmModule 'vmTemplate.bicep' = [for i in range(0, vmCount): {
name: 'deployVm${i}'
params: {
vmName: 'vm${i}'
location: location
}
}]

output vmIds array = [for i in range(0, vmCount): vmModule[i].outputs.vmId]

This deploys vmTemplate.bicep twice with different parameters, and collects their output IDs.

Why it Emerges: Scaling out resources (VMs, subnets, etc.) is common. Using loops to instantiate modules keeps code DRY and lets you parametrize the count.

Loops in Child Properties (Nested Loops)

You can loop inside a resource’s properties. For example, defining multiple subnets in a VNet:

param location string = resourceGroup().location

var subnets = [
{ name: 'api'; subnetPrefix: '10.0.0.0/24' }
{ name: 'worker'; subnetPrefix: '10.0.1.0/24' }
]

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: 'vnet1'
location: location
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
subnets: [for sn in subnets: {
name: sn.name
properties: { addressPrefix: sn.subnetPrefix }
}]
}
}

This creates a single VNet with two subnets (api and worker) using one loop.

Why it Emerges: Often a resource has a collection child (like subnets). Loops make it easy to define them from data. The official docs highlight looping in nested objects for exactly this purpose.

Loops with Conditions

You can combine loops with conditions. For example, deploy up to N resources but only if a flag is true:

param createResources bool = true
param resourceCount int = 3
param location string = resourceGroup().location

var baseName = 'stor${uniqueString(resourceGroup().id)}'

module storageMod 'storageAccount.bicep' = [for i in range(0, resourceCount): if (createResources) {
name: 'deploy${i}${baseName}'
params: {
storageName: '${i}${baseName}'
location: location
}
}]

If createResources is false, the loop yields no instances. If true, it deploys resourceCount modules. Loop-with-if is demonstrated in docs.

Why it Emerges: This pattern allows conditional scale-out. For example, you might allow an admin to choose whether multiple instances should be created at all. It combines flexibility of loops with if-based gating.

Module Reuse (Local Modules)

Large deployments are broken into reusable modules. Example:

param location string = resourceGroup().location

resource plan 'Microsoft.Web/serverfarms@2021-02-01' = {
name: 'myAppPlan'
location: location
sku: { name: 'B1'; tier: 'Basic' }
}

module webApp 'modules/webApp.bicep' = {
name: 'webAppModule'
params: {
appName: 'myWebApp'
location: location
appServicePlanId: plan.id
}
}

module sqlDb 'modules/sqlDatabase.bicep' = {
name: 'sqlModule'
params: {
serverName: 'mySqlServer'
databaseName: 'myDatabase'
location: location
}
}

This calls separate Bicep files for each component. The iaMachs guide shows the same idea: splitting App Service, SQL, storage into modules.

Why it Emerges: Modularization is key for maintainability. It lets teams develop and version components (e.g. a standard webApp module) and keeps main files concise.

Registry Modules (Versioned Reuse)

Beyond local files, modules can be pulled from an Azure Container Registry with a version tag. For example:

module vnet 'br:myregistry.azurecr.io/shared-modules/networking/vnet:1.0.0' = {
name: 'vnetDeployment'
params: { vnetName: 'project-vnet'; addressPrefixes: ['10.1.0.0/16'] }
}

This references a published VNet module in ACR. The iaMachs example shows using the br: syntax to fetch a shared “vnet” module from a registry.

Why it Emerges: Enterprises often maintain a library of standardized modules. Referencing them by URL and version number ensures consistency and enables version control of infrastructure components.

Outputs from Modules

Modules can return outputs to the parent template. For example, if webApp.bicep defines:

resource webApp 'Microsoft.Web/sites@2022-09-01' = { name: appName; ... }
output defaultHostName string = webApp.properties.defaultHostName

then the parent can capture it:

module webApp 'webApp.bicep' = {
name: 'webAppModule'; params: { appName: 'myApp'; location: location; ... }
}
output webAppUrl string = webApp.outputs.defaultHostName

This pattern (seen in the iaMachs example) allows you to pass values (like hostnames, IDs) back up.

Why it Emerges: Modules encapsulate resources but sometimes you need their results (e.g. an endpoint URL) in the main template. Exporting outputs from modules and then using them avoids hardcoding and re-querying resources. It’s a best practice to pass values this way.

Conditional Expressions (?:)

Bicep supports the ternary ? : operator for concise logic in variables or outputs. For example, continuing the “new vs existing” pattern:

output storageAccountId string = 
(newOrExisting == 'new') ? saNew.id : saExisting.id

This chooses between saNew.id or saExisting.id based on the mode. This exact snippet appears in the docs.

Why it Emerges: It’s a compact way to select values. Instead of writing an if block around outputs, you can inline conditional logic. This often appears in outputs or variables when a decision depends on a parameter.

Referencing Existing Resources (Same Scope)

Use the existing keyword to refer to an Azure resource already created. For example, to get an existing storage account in the current RG:

resource stg 'Microsoft.Storage/storageAccounts@2023-04-01' existing = {
name: 'examplestorage'
}

output blobEndpoint string = stg.properties.primaryEndpoints.blob

This does not create a new storage account. It simply lets you access properties (here primaryEndpoints.blob) of that named account.

Why it Emerges: Often, a deployment needs to reference an existing resource (like a Key Vault or VNet) to retrieve information. existing is the official Bicep construct for this (instead of, say, using resourceId()/reference()). It ensures the resource is only looked up and not redeployed.

Referencing Existing Resources (Cross-Scope)

You can reference resources in another resource group (or subscription/management group) by setting the scope property. Example:

resource otherStg 'Microsoft.Storage/storageAccounts@2023-04-01' existing = {
name: 'examplestorage'
scope: resourceGroup('OtherResourceGroup')
}

output blobEndpoint string = otherStg.properties.primaryEndpoints.blob

This grabs examplestorage from OtherResourceGroup. The docs give a similar example (see the “Different scope” section).

Why it Emerges: Large solutions span multiple RGs or subscriptions. Setting scope lets you reference a resource by name in the right RG. This avoids hardcoding IDs and keeps templates flexible.

Tags Pattern

Tagging resources is critical in enterprise for governance/billing. Two common patterns:

  • Inline tags: Define tags per resource (static or via expressions).

    resource stg 'Microsoft.Storage/storageAccounts@2021-04-01' = {
    name: 'stg${uniqueString(resourceGroup().id)}'
    location: location
    tags: {
    Dept: 'Finance'
    Environment: 'Production'
    LastDeployed: utcNow('yyyy-MM-dd')
    }
    }

    This example (from Microsoft docs) shows literal tag values and one from a parameter (utcNow date).

  • Object or union-based tags: Define a common tags object and merge it. For instance, a parameter or variable commonTags = { Dept: 'Finance'; Owner: 'Ops' }, then use tags: union(commonTags, { specificTag: 'X'}). The StackOverflow solution illustrates this using union(commonTags, { additionalTag: 'value' }).

Why it Emerges: Tagging consistently is a best practice. Developers either hard-code tags inline or maintain a central tags object (possibly parameterized) and merge in resource-specific tags. This pattern is documented (apply tags in Bicep).

Default Location Parameter

A very common shortcut is to default the location parameter to the resource group’s location:

param location string = resourceGroup().location

This way, deployments automatically use the RG’s location unless overridden.

Why it Emerges: Nearly all resources require a location. Defaulting it to resourceGroup().location saves users from having to specify it each time, and ensures resources deploy to the same region as the RG.

Base Name Variable Pattern

Often templates define a “base” name or prefix as a var to reuse in multiple places. Example:

var baseName = 'store${uniqueString(resourceGroup().id)}'

resource storage1 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: '${baseName}01'
// ...
}
resource storage2 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: '${baseName}02'
// ...
}

This avoids repeating the uniqueString(...) call. In the loops example above, baseName was used as well.

Why it Emerges: Reusing a computed base string (like uniqueString) keeps names consistent and code DRY. It also guarantees related resources share the same suffix.

Object/Array Parameter Configurations

Complex settings can be passed as objects or arrays rather than many parameters. For instance:

param vNetSettings object = {
name: 'VNet1'
location: 'eastus'
addressPrefixes: [
{ name: 'firstPrefix'; addressPrefix: '10.0.0.0/22' }
]
subnets: [
{ name: 'apiSubnet'; addressPrefix: '10.0.0.0/24' }
{ name: 'workerSubnet'; addressPrefix: '10.0.1.0/24' }
]
}

resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: vNetSettings.name
location: vNetSettings.location
properties: {
addressSpace: { addressPrefixes: [vNetSettings.addressPrefixes[0].addressPrefix] }
subnets: [for sn in vNetSettings.subnets: {
name: sn.name
properties: { addressPrefix: sn.addressPrefix }
}]
}
}

This pattern (shown in Microsoft docs) packs related values into one object parameter.

Why it Emerges: Passing grouped values as a single object/array makes templates cleaner and more extensible. It’s easier to manage one parameter than many, especially for structured configuration like VNet/subnet definitions.

Secure Outputs (@secure())

When a template outputs sensitive information, mark the output secure:

@secure()
output dbConnString string = sqlServer.properties.fullyQualifiedDomainName

This hides the output from logs and history.

Why it Emerges: Connection strings or keys should not appear in plain text after deployment. Using @secure() on outputs is a documented best practice to protect sensitive data.

Subscription-Level Deployment (targetScope)

Large-scale templates may deploy at the subscription scope (for RGs, policies, etc.). Use targetScope = 'subscription'. For example, to create a new resource group:

targetScope = 'subscription'

param rgName string
param rgLocation string

resource newRG 'Microsoft.Resources/resourceGroups@2024-11-01' = {
name: rgName
location: rgLocation
}

This example (from Microsoft docs) runs at subscription scope to create RGs. You can also define modules with scope: newRG as shown in that article.

Why it Emerges: Enterprise scripts often need to manage RGs, budgets, or policies at the subscription or management-group level. Setting targetScope='subscription' is the pattern for these deployments.

(Additional Notes)

  • Parameter Files: A common practice is to use separate .bicepparam files per environment (dev/test/prod) that supply parameter values. This isn’t shown above but is widely used for deployment promotion.
  • API Versions: While not a “pattern” per se, always use recent API versions for resources (as best practice recommends).
  • Resource Symbolic Names: Don’t include “Name” in the symbolic resource name (use e.g. resource storageAccount not resource storageAccountName).

Each of the above patterns is widely used in production-grade Bicep files. The examples demonstrate how they work in code; the cited docs explain their rationale. By studying these patterns, you can learn industry-standard practices for naming, conditional logic, modularization, looping, and securing Bicep deployments.

Other resources

2025-07-19 45 days of Azure Bicep Language { gist.github.com }

Collection of articles on medium

Inspiration and use-cases

2025-07-19 Deploying an Automation Account with a Runbook and Schedule Using Bicep - Arinco { arinco.com.au }

resource automationAccountModules 'Microsoft.Automation/automationAccounts/modules@2020-01-13-preview' = [for module in modules: {
parent: automationAccount
name: module.name
properties: {
contentLink: {
uri: module.version == 'latest' ? '${module.uri}/${module.name}' : '${module.uri}/${module.name}/${module.version}'
version: module.version == 'latest' ? null : module.version
}
}
}]

2025-07-19 AKS-Construction/bicep/automationrunbook/automation.bicep at d1a98d0bd12e8d1625f382e93c6478b95f186d31 · Azure/AKS-Construction { github.com }

This is what I needed

type runbookJob = {
scheduleName: string
parameters: object?
}

@description('The Runbook-Schedule Jobs to create with workflow specific parameters')
param runbookJobSchedule runbookJob[]

@description('The name of the runbook to create')
param runbookName string

@allowed([
'GraphPowerShell'
'Script'
])
@description('The type of runbook that is being imported')
param runbookType string = 'Script'

@description('The URI to import the runbook code from')
param runbookUri string = ''

@description('A description of what the runbook does')
param runbookDescription string = ''

var runbookVersion = '1.0.0.0'
var tomorrow = dateTimeAdd(today, 'P1D','yyyy-MM-dd')
var timebase = '1900-01-01'
var scheduleNoExpiry = '9999-12-31T23:59:00+00:00'
var workWeek = {weekDays: [
'Monday'
'Tuesday'
'Wednesday'
'Thursday'
'Friday'
]
}

resource automationAccount 'Microsoft.Automation/automationAccounts@2022-08-08' = {
name: automationAccountName
location: location
identity: {
type: 'SystemAssigned'
}
properties: {
sku: {
name: accountSku
}
}
}

resource automationAccountDiagLogging 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if(!empty(loganalyticsWorkspaceId)) {
name: 'diags'
scope: automationAccount
properties: {
workspaceId: loganalyticsWorkspaceId
logs: [for diagCategory in diagnosticCategories: {
category: diagCategory
enabled: true
}]
}
}

2025-07-19 azure-bicep/Maester/WebApp/modules/aa-advanced.bicep at 40b46281ee427fd43fd089c1ce81e52481cb6f5f · brianveldman/azure-bicep { github.com }

Another good example, but simpler to follow

@description('Runbook Deployment')
resource automationAccountRunbook 'Microsoft.Automation/automationAccounts/runbooks@2023-11-01' = {
name: 'runBookMaester'
location: __location__
parent: automationAccount
properties: {
runbookType: 'PowerShell72'
logProgress: true
logVerbose: true
description: 'Runbook to execute Maester report'
publishContentLink: {
uri: __ouMaesterScriptBlobUri__
}
}
}

Structure