Azure Bicep
Language Overview¶
Azure Bicep is a domain-specific language (DSL) for deploying Azure resources declaratively. It provides a transparent abstraction over Azure Resource Manager (ARM) templates with cleaner, more readable syntax and first-class support for modularity.
Key Characteristics¶
- Format: Bicep files (
.bicep) compiled to ARM JSON - Type: Declarative infrastructure as code
- Execution: Azure Resource Manager
- Primary Use Cases:
- Azure-native infrastructure deployment
- Modular infrastructure components
- Multi-environment Azure deployments
- Azure Landing Zones and governance
Quick Reference¶
| Category | Convention | Example | Notes |
|---|---|---|---|
| Naming | |||
| File Names | kebab-case.bicep |
storage-account.bicep |
Descriptive, lowercase |
| Resource Names | camelCase |
storageAccount |
Symbolic name in Bicep |
| Parameter Names | camelCase |
environmentName |
Descriptive purpose |
| Variable Names | camelCase |
storageAccountName |
Clear intent |
| Module Names | camelCase |
networkModule |
Match file name |
| Output Names | camelCase |
storageAccountId |
Match resource attribute |
| File Structure | |||
| Parameters | Top of file | After targetScope |
Input values |
| Variables | After parameters | Computed values | Local computations |
| Resources | After variables | Main content | Azure resources |
| Modules | With resources | Nested deployments | Reusable components |
| Outputs | End of file | Export values | Return values |
| Best Practices | |||
| Modules | Reusable components | Single responsibility | One resource type |
| Parameters | Validate input | @allowed, @minLength |
Constrain values |
| Deployment Scopes | Explicit scope | targetScope = 'subscription' |
Default is resourceGroup |
| Security | |||
| Secrets | Key Vault references | @secure() decorator |
Never hardcode |
| RBAC | Least privilege | Specific resource scope | No subscription-wide |
| Encryption | Enable by default | Customer-managed keys | All storage |
File Structure¶
Complete Template Example¶
// storage-account.bicep
// Deploys a secure storage account with blob containers
targetScope = 'resourceGroup'
// ============================================================================
// Parameters
// ============================================================================
@description('The Azure region for resources')
param location string = resourceGroup().location
@description('Environment name for resource naming and tagging')
@allowed([
'dev'
'staging'
'prod'
])
param environmentName string
@description('The name prefix for resources')
@minLength(3)
@maxLength(11)
param namePrefix string
@description('Storage account SKU')
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_RAGRS'
'Standard_ZRS'
'Premium_LRS'
])
param storageSku string = 'Standard_LRS'
@description('Enable blob versioning')
param enableVersioning bool = true
@description('Tags to apply to all resources')
param tags object = {}
// ============================================================================
// Variables
// ============================================================================
var storageAccountName = '${namePrefix}${environmentName}${uniqueString(resourceGroup().id)}'
var defaultTags = {
Environment: environmentName
ManagedBy: 'Bicep'
CreatedDate: utcNow('yyyy-MM-dd')
}
var allTags = union(defaultTags, tags)
// ============================================================================
// Resources
// ============================================================================
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: take(storageAccountName, 24)
location: location
tags: allTags
sku: {
name: storageSku
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
allowSharedKeyAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
encryption: {
services: {
blob: {
enabled: true
keyType: 'Account'
}
file: {
enabled: true
keyType: 'Account'
}
}
keySource: 'Microsoft.Storage'
}
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
virtualNetworkRules: []
ipRules: []
}
}
}
resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
properties: {
containerDeleteRetentionPolicy: {
enabled: true
days: 7
}
deleteRetentionPolicy: {
enabled: true
days: 7
}
isVersioningEnabled: enableVersioning
}
}
resource dataContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobServices
name: 'data'
properties: {
publicAccess: 'None'
}
}
resource logsContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobServices
name: 'logs'
properties: {
publicAccess: 'None'
}
}
// ============================================================================
// Outputs
// ============================================================================
@description('The resource ID of the storage account')
output storageAccountId string = storageAccount.id
@description('The name of the storage account')
output storageAccountName string = storageAccount.name
@description('The primary blob endpoint')
output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob
Naming Conventions¶
File Names¶
# Good - kebab-case, descriptive
storage-account.bicep
virtual-network.bicep
key-vault.bicep
app-service-plan.bicep
sql-database.bicep
container-registry.bicep
# Bad - inconsistent casing or unclear
StorageAccount.bicep
vnet.bicep
kv.bicep
plan.bicep
Resource Symbolic Names¶
// Good - camelCase, descriptive
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
}
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: vnetName
}
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
}
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
name: appServicePlanName
}
// Bad - unclear or wrong casing
resource sa 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
}
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: vnetName
}
resource KV 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
}
Parameter Names¶
// Good - camelCase with descriptive names and decorators
@description('The Azure region for resource deployment')
param location string = resourceGroup().location
@description('Environment name used for resource naming')
@allowed(['dev', 'staging', 'prod'])
param environmentName string
@description('The name prefix for all resources')
@minLength(3)
@maxLength(10)
param resourcePrefix string
@description('Enable diagnostic logging')
param enableDiagnostics bool = true
@description('Virtual network address space')
param vnetAddressPrefix string = '10.0.0.0/16'
// Bad - vague names or missing decorators
param env string
param prefix string
param diag bool
param addr string
Variable Names¶
// Good - camelCase, computed values with clear purpose
var storageAccountName = '${resourcePrefix}${environmentName}${uniqueString(resourceGroup().id)}'
var keyVaultName = 'kv-${resourcePrefix}-${environmentName}'
var appInsightsName = 'appi-${resourcePrefix}-${environmentName}'
var logAnalyticsName = 'log-${resourcePrefix}-${environmentName}'
var defaultTags = {
Environment: environmentName
ManagedBy: 'Bicep'
Project: projectName
}
var subnetConfigurations = [
{
name: 'web-subnet'
addressPrefix: '10.0.1.0/24'
}
{
name: 'app-subnet'
addressPrefix: '10.0.2.0/24'
}
{
name: 'data-subnet'
addressPrefix: '10.0.3.0/24'
}
]
// Bad - unclear purpose
var name1 = '${prefix}${env}${uniqueString(resourceGroup().id)}'
var x = 'kv-${prefix}-${env}'
var temp = {}
Azure Resource Naming¶
// Recommended naming convention: {resource-type}-{workload}-{environment}-{region}-{instance}
var namingPrefix = '${workloadName}-${environmentName}-${location}'
// Storage Account (3-24 chars, lowercase alphanumeric only)
var storageAccountName = take('st${workloadName}${environmentName}${uniqueString(resourceGroup().id)}', 24)
// Key Vault (3-24 chars, alphanumeric and hyphens)
var keyVaultName = take('kv-${namingPrefix}', 24)
// Virtual Network
var vnetName = 'vnet-${namingPrefix}'
// Subnet
var subnetName = 'snet-${purpose}-${namingPrefix}'
// Network Security Group
var nsgName = 'nsg-${purpose}-${namingPrefix}'
// Application Service Plan
var appServicePlanName = 'asp-${namingPrefix}'
// Web App
var webAppName = 'app-${namingPrefix}'
// Function App
var functionAppName = 'func-${namingPrefix}'
// SQL Server
var sqlServerName = 'sql-${namingPrefix}'
// SQL Database
var sqlDatabaseName = 'sqldb-${namingPrefix}'
// Container Registry (5-50 chars, alphanumeric only)
var acrName = take('acr${workloadName}${environmentName}${uniqueString(resourceGroup().id)}', 50)
// AKS Cluster
var aksName = 'aks-${namingPrefix}'
// Log Analytics Workspace
var logAnalyticsName = 'log-${namingPrefix}'
// Application Insights
var appInsightsName = 'appi-${namingPrefix}'
Parameters¶
Parameter Decorators¶
// Required parameter with description
@description('The name of the workload or application')
param workloadName string
// Optional parameter with default
@description('The Azure region for deployment')
param location string = resourceGroup().location
// Allowed values constraint
@description('The deployment environment')
@allowed([
'dev'
'staging'
'prod'
])
param environmentName string
// String length constraints
@description('Resource name prefix')
@minLength(3)
@maxLength(10)
param namePrefix string
// Numeric constraints
@description('Number of instances to deploy')
@minValue(1)
@maxValue(10)
param instanceCount int = 2
// Secure parameter (hidden in logs and portal)
@description('The administrator password')
@secure()
param adminPassword string
// Metadata for additional context
@description('Tags to apply to all resources')
@metadata({
example: {
Environment: 'prod'
CostCenter: '12345'
}
})
param tags object = {}
Parameter Types¶
// String parameter
param resourceName string
// Integer parameter
param instanceCount int = 1
// Boolean parameter
param enablePublicAccess bool = false
// Array parameter
@description('List of allowed IP addresses')
param allowedIpAddresses array = []
// Object parameter
@description('Network configuration settings')
param networkConfig object = {
vnetAddressPrefix: '10.0.0.0/16'
subnetAddressPrefix: '10.0.1.0/24'
}
// Union types (Bicep 0.21+)
@description('SKU tier for the resource')
param skuTier 'Basic' | 'Standard' | 'Premium' = 'Standard'
// User-defined types (Bicep 0.21+)
@description('Subnet configuration')
type subnetConfigType = {
name: string
addressPrefix: string
serviceEndpoints: string[]?
}
param subnets subnetConfigType[]
Parameter Files¶
// main.bicepparam (Bicep parameter file format)
using './main.bicep'
param environmentName = 'prod'
param location = 'eastus2'
param workloadName = 'myapp'
param instanceCount = 3
param tags = {
Environment: 'prod'
CostCenter: '12345'
Owner: 'platform-team'
}
// parameters.prod.json (JSON parameter file format)
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": {
"value": "prod"
},
"location": {
"value": "eastus2"
},
"workloadName": {
"value": "myapp"
},
"instanceCount": {
"value": 3
},
"tags": {
"value": {
"Environment": "prod",
"CostCenter": "12345",
"Owner": "platform-team"
}
}
}
}
Variables¶
Variable Patterns¶
// Simple computed value
var storageAccountName = '${namePrefix}${environmentName}${uniqueString(resourceGroup().id)}'
// Conditional value
var skuName = environmentName == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
// Complex object
var defaultTags = {
Environment: environmentName
ManagedBy: 'Bicep'
DeployedAt: utcNow('yyyy-MM-dd')
ResourceGroup: resourceGroup().name
}
// Merged objects
var allTags = union(defaultTags, customTags)
// Array construction
var subnets = [
{
name: 'web'
addressPrefix: cidrSubnet(vnetAddressPrefix, 24, 0)
serviceEndpoints: ['Microsoft.Storage', 'Microsoft.KeyVault']
}
{
name: 'app'
addressPrefix: cidrSubnet(vnetAddressPrefix, 24, 1)
serviceEndpoints: ['Microsoft.Sql', 'Microsoft.Storage']
}
{
name: 'data'
addressPrefix: cidrSubnet(vnetAddressPrefix, 24, 2)
serviceEndpoints: []
}
]
// Environment-specific configuration
var environmentConfig = {
dev: {
vmSize: 'Standard_B2s'
instanceCount: 1
enableHA: false
}
staging: {
vmSize: 'Standard_D2s_v3'
instanceCount: 2
enableHA: false
}
prod: {
vmSize: 'Standard_D4s_v3'
instanceCount: 3
enableHA: true
}
}
var currentConfig = environmentConfig[environmentName]
Built-in Functions¶
// String functions
var lowerName = toLower(resourceName)
var upperName = toUpper(resourceName)
var trimmedName = trim(resourceName)
var replacedName = replace(resourceName, '-', '_')
var substringName = substring(resourceName, 0, 10)
var formattedName = format('{0}-{1}-{2}', prefix, env, region)
// Unique string generation
var uniqueSuffix = uniqueString(resourceGroup().id)
var uniqueStorageName = 'st${uniqueString(subscription().subscriptionId, resourceGroup().id)}'
// GUID generation
var newGuid = guid(resourceGroup().id, deployment().name)
// Array functions
var firstItem = first(myArray)
var lastItem = last(myArray)
var arrayLength = length(myArray)
var containsItem = contains(myArray, 'item')
var flatArray = flatten(nestedArray)
var distinctArray = union(array1, array2)
var intersectArray = intersection(array1, array2)
// Object functions
var hasProperty = contains(myObject, 'propertyName')
var propertyValue = myObject.?propertyName ?? 'default'
var mergedObject = union(object1, object2)
var objectKeys = objectKeys(myObject)
// Resource group and subscription
var rgName = resourceGroup().name
var rgLocation = resourceGroup().location
var rgId = resourceGroup().id
var subId = subscription().subscriptionId
var tenantId = tenant().tenantId
// Date and time
var deploymentTime = utcNow()
var formattedDate = utcNow('yyyy-MM-dd')
var formattedDateTime = utcNow('yyyy-MM-ddTHH:mm:ssZ')
// CIDR functions (Bicep 0.20+)
var firstSubnet = cidrSubnet('10.0.0.0/16', 24, 0) // 10.0.0.0/24
var secondSubnet = cidrSubnet('10.0.0.0/16', 24, 1) // 10.0.1.0/24
var hostIp = cidrHost('10.0.0.0/24', 5) // 10.0.0.5
// JSON parsing
var parsedJson = json(loadTextContent('config.json'))
Resources¶
Basic Resource Declaration¶
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: tags
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
minimumTlsVersion: 'TLS1_2'
}
}
Child Resources¶
// Method 1: Parent property
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
resource blobServices 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
}
}
resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobServices
name: 'data'
properties: {
publicAccess: 'None'
}
}
// Method 2: Nested declaration
resource storageAccountNested 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: '${storageAccountName}nested'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
resource blobServicesNested 'blobServices' = {
name: 'default'
properties: {}
resource containerNested 'containers' = {
name: 'data'
properties: {
publicAccess: 'None'
}
}
}
}
Existing Resources¶
// Reference existing resource in same resource group
resource existingVnet 'Microsoft.Network/virtualNetworks@2023-05-01' existing = {
name: 'existing-vnet'
}
// Reference existing resource in different resource group
resource existingKeyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: 'existing-keyvault'
scope: resourceGroup('other-rg')
}
// Reference existing resource in different subscription
resource existingStorage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = {
name: 'existingstorage'
scope: resourceGroup('other-subscription-id', 'other-rg')
}
// Use existing resource properties
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
parent: existingVnet
name: 'new-subnet'
properties: {
addressPrefix: '10.0.10.0/24'
}
}
output vnetId string = existingVnet.id
output keyVaultUri string = existingKeyVault.properties.vaultUri
Resource Dependencies¶
// Implicit dependency (automatic from references)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: functionAppName
location: location
kind: 'functionapp'
properties: {
// Implicit dependency on storageAccount
siteConfig: {
appSettings: [
{
name: 'AzureWebJobsStorage'
value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}'
}
]
}
}
}
// Explicit dependency with dependsOn
resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = {
name: 'diag-${storageAccount.name}'
scope: storageAccount
dependsOn: [
logAnalyticsWorkspace
]
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
category: 'StorageRead'
enabled: true
}
]
}
}
Modules¶
Module Definition¶
// modules/storage-account.bicep
@description('Storage account name')
param storageAccountName string
@description('Location for resources')
param location string = resourceGroup().location
@description('Storage SKU')
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_RAGRS'
'Standard_ZRS'
])
param sku string = 'Standard_LRS'
@description('Tags for resources')
param tags object = {}
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: tags
sku: {
name: sku
}
kind: 'StorageV2'
properties: {
accessTier: 'Hot'
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
@description('The resource ID of the storage account')
output id string = storageAccount.id
@description('The name of the storage account')
output name string = storageAccount.name
@description('The primary blob endpoint')
output primaryBlobEndpoint string = storageAccount.properties.primaryEndpoints.blob
Module Consumption¶
// main.bicep
param environmentName string
param location string = resourceGroup().location
// Deploy storage account using module
module storageModule 'modules/storage-account.bicep' = {
name: 'storage-deployment'
params: {
storageAccountName: 'st${environmentName}${uniqueString(resourceGroup().id)}'
location: location
sku: environmentName == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
tags: {
Environment: environmentName
}
}
}
// Use module outputs
output storageAccountId string = storageModule.outputs.id
output storageAccountName string = storageModule.outputs.name
Module with Different Scope¶
// Deploy module to different resource group
module networkModule 'modules/virtual-network.bicep' = {
name: 'network-deployment'
scope: resourceGroup('network-rg')
params: {
vnetName: 'vnet-${environmentName}'
location: location
}
}
// Deploy module to subscription scope
module policyModule 'modules/policy-assignment.bicep' = {
name: 'policy-deployment'
scope: subscription()
params: {
policyDefinitionId: policyDefinitionId
}
}
// Deploy module to management group scope
module mgmtGroupModule 'modules/management-group-policy.bicep' = {
name: 'mgmt-policy-deployment'
scope: managementGroup('my-management-group')
params: {
policyName: 'require-tags'
}
}
Module Loops¶
// Deploy multiple storage accounts
param storageConfigs array = [
{
name: 'data'
sku: 'Standard_LRS'
}
{
name: 'logs'
sku: 'Standard_GRS'
}
{
name: 'backup'
sku: 'Standard_RAGRS'
}
]
module storageAccounts 'modules/storage-account.bicep' = [for config in storageConfigs: {
name: 'storage-${config.name}'
params: {
storageAccountName: 'st${config.name}${uniqueString(resourceGroup().id)}'
location: location
sku: config.sku
tags: tags
}
}]
// Access outputs from module array
output storageIds array = [for i in range(0, length(storageConfigs)): storageAccounts[i].outputs.id]
Bicep Registry Modules¶
// Using Azure Container Registry for Bicep modules
module acrStorageModule 'br:myregistry.azurecr.io/bicep/modules/storage:v1.0.0' = {
name: 'acr-storage-deployment'
params: {
storageAccountName: storageAccountName
location: location
}
}
// Using public Bicep registry
module publicModule 'br/public:avm/res/storage/storage-account:0.9.0' = {
name: 'public-storage-deployment'
params: {
name: storageAccountName
location: location
}
}
// Using template specs
module templateSpecModule 'ts:11111111-1111-1111-1111-111111111111/my-rg/storageSpec:1.0.0' = {
name: 'template-spec-deployment'
params: {
storageAccountName: storageAccountName
}
}
Deployment Scopes¶
Resource Group Scope (Default)¶
// resource-group-deployment.bicep
targetScope = 'resourceGroup'
param storageAccountName string
param location string = resourceGroup().location
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
Subscription Scope¶
// subscription-deployment.bicep
targetScope = 'subscription'
param resourceGroupName string
param location string
// Create resource group
resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: resourceGroupName
location: location
tags: {
Environment: 'prod'
ManagedBy: 'Bicep'
}
}
// Deploy resources to the new resource group
module storageModule 'modules/storage-account.bicep' = {
name: 'storage-deployment'
scope: rg
params: {
storageAccountName: 'st${uniqueString(rg.id)}'
location: location
}
}
// Create role assignment at subscription level
resource readerRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, 'reader-assignment')
properties: {
principalId: principalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')
principalType: 'ServicePrincipal'
}
}
// Create policy assignment
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
name: 'require-tag-policy'
properties: {
policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/96670d01-0a4d-4649-9c89-2d3abc0a5025'
displayName: 'Require Environment Tag'
parameters: {
tagName: {
value: 'Environment'
}
}
}
}
Management Group Scope¶
// management-group-deployment.bicep
targetScope = 'managementGroup'
param policyName string
param policyDisplayName string
// Create policy definition at management group level
resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2021-06-01' = {
name: policyName
properties: {
displayName: policyDisplayName
policyType: 'Custom'
mode: 'All'
parameters: {
allowedLocations: {
type: 'Array'
metadata: {
displayName: 'Allowed locations'
description: 'The list of allowed locations for resources.'
}
}
}
policyRule: {
if: {
allOf: [
{
field: 'location'
notIn: '[parameters(\'allowedLocations\')]'
}
{
field: 'location'
notEquals: 'global'
}
]
}
then: {
effect: 'deny'
}
}
}
}
// Assign policy to management group
resource policyAssignment 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
name: '${policyName}-assignment'
properties: {
policyDefinitionId: policyDefinition.id
displayName: '${policyDisplayName} Assignment'
parameters: {
allowedLocations: {
value: [
'eastus'
'eastus2'
'westus2'
]
}
}
}
}
Tenant Scope¶
// tenant-deployment.bicep
targetScope = 'tenant'
param managementGroupName string
param managementGroupDisplayName string
param parentManagementGroupId string = ''
// Create management group hierarchy
resource managementGroup 'Microsoft.Management/managementGroups@2021-04-01' = {
name: managementGroupName
properties: {
displayName: managementGroupDisplayName
details: !empty(parentManagementGroupId) ? {
parent: {
id: '/providers/Microsoft.Management/managementGroups/${parentManagementGroupId}'
}
} : null
}
}
output managementGroupId string = managementGroup.id
Loops and Conditionals¶
Resource Loops¶
// Loop with array
param containerNames array = ['data', 'logs', 'backup']
resource containers 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = [for name in containerNames: {
parent: blobServices
name: name
properties: {
publicAccess: 'None'
}
}]
// Loop with index
resource subnets 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = [for (subnet, i) in subnetConfigs: {
parent: vnet
name: subnet.name
properties: {
addressPrefix: cidrSubnet(vnetAddressPrefix, 24, i)
serviceEndpoints: subnet.serviceEndpoints
}
}]
// Loop with range
resource networkSecurityGroups 'Microsoft.Network/networkSecurityGroups@2023-05-01' = [for i in range(0, 3): {
name: 'nsg-${i}'
location: location
properties: {
securityRules: []
}
}]
// Nested loops
param environments array = ['dev', 'staging', 'prod']
param regions array = ['eastus', 'westus2']
resource resourceGroups 'Microsoft.Resources/resourceGroups@2023-07-01' = [for env in environments: [for region in regions: {
name: 'rg-${env}-${region}'
location: region
}]]
// Loop with object items
param storageConfigurations object = {
data: {
sku: 'Standard_LRS'
tier: 'Hot'
}
logs: {
sku: 'Standard_GRS'
tier: 'Cool'
}
}
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for config in items(storageConfigurations): {
name: 'st${config.key}${uniqueString(resourceGroup().id)}'
location: location
sku: {
name: config.value.sku
}
kind: 'StorageV2'
properties: {
accessTier: config.value.tier
}
}]
Conditional Resources¶
// Conditional resource deployment
param deployDiagnostics bool = true
resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (deployDiagnostics) {
name: 'diag-${storageAccount.name}'
scope: storageAccount
properties: {
workspaceId: logAnalyticsWorkspace.id
logs: [
{
category: 'StorageRead'
enabled: true
}
]
}
}
// Conditional based on environment
param environmentName string
resource premiumStorage 'Microsoft.Storage/storageAccounts@2023-01-01' = if (environmentName == 'prod') {
name: 'stpremium${uniqueString(resourceGroup().id)}'
location: location
sku: {
name: 'Premium_LRS'
}
kind: 'StorageV2'
properties: {}
}
// Conditional module deployment
module monitoringModule 'modules/monitoring.bicep' = if (enableMonitoring) {
name: 'monitoring-deployment'
params: {
workspaceName: logAnalyticsName
location: location
}
}
// Conditional with loop
resource conditionalContainers 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = [for container in containers: if (container.deploy) {
parent: blobServices
name: container.name
properties: {
publicAccess: 'None'
}
}]
Conditional Properties¶
// Ternary operator for property values
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: environmentName == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
accessTier: environmentName == 'prod' ? 'Hot' : 'Cool'
allowBlobPublicAccess: false
// Conditional network rules
networkAcls: enablePrivateEndpoint ? {
defaultAction: 'Deny'
bypass: 'AzureServices'
} : {
defaultAction: 'Allow'
}
}
}
// Conditional object spread
var baseProperties = {
accessTier: 'Hot'
supportsHttpsTrafficOnly: true
}
var prodProperties = {
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
networkAcls: {
defaultAction: 'Deny'
}
}
resource conditionalStorage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: environmentName == 'prod' ? union(baseProperties, prodProperties) : baseProperties
}
Outputs¶
Basic Outputs¶
// String output
@description('The resource ID of the storage account')
output storageAccountId string = storageAccount.id
// Object output
@description('Storage account properties')
output storageAccountProperties object = {
name: storageAccount.name
primaryEndpoint: storageAccount.properties.primaryEndpoints.blob
resourceGroup: resourceGroup().name
}
// Array output
@description('List of container names')
output containerNames array = [for container in containers: container.name]
// Secure output (hidden in deployment logs)
@description('Storage account connection string')
@secure()
output connectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}'
Outputs from Loops¶
// Output array from resource loop
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for name in storageNames: {
name: 'st${name}${uniqueString(resourceGroup().id)}'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}]
output storageAccountIds array = [for (name, i) in storageNames: storageAccounts[i].id]
output storageAccountEndpoints array = [for (name, i) in storageNames: storageAccounts[i].properties.primaryEndpoints.blob]
// Complex output structure
output storageDetails array = [for (name, i) in storageNames: {
name: storageAccounts[i].name
id: storageAccounts[i].id
endpoint: storageAccounts[i].properties.primaryEndpoints.blob
}]
Outputs from Modules¶
// Module outputs
module networkModule 'modules/virtual-network.bicep' = {
name: 'network-deployment'
params: {
vnetName: vnetName
location: location
}
}
output vnetId string = networkModule.outputs.vnetId
output subnetIds array = networkModule.outputs.subnetIds
// Outputs from module loops
module storageModules 'modules/storage-account.bicep' = [for config in storageConfigs: {
name: 'storage-${config.name}'
params: {
storageAccountName: 'st${config.name}${uniqueString(resourceGroup().id)}'
location: location
}
}]
output moduleOutputs array = [for (config, i) in storageConfigs: {
name: config.name
id: storageModules[i].outputs.id
endpoint: storageModules[i].outputs.primaryBlobEndpoint
}]
Secret Management¶
Key Vault References¶
// Reference existing Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: keyVaultName
scope: resourceGroup(keyVaultResourceGroup)
}
// Get secret from Key Vault
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: sqlServerName
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: keyVault.getSecret('sql-admin-password')
}
}
// Reference Key Vault secret in module
module appService 'modules/app-service.bicep' = {
name: 'app-service-deployment'
params: {
appName: appName
location: location
// Pass secret reference to module
connectionString: keyVault.getSecret('app-connection-string')
}
}
Creating Key Vault with Secrets¶
// Create Key Vault
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
sku: {
family: 'A'
name: 'standard'
}
tenantId: tenant().tenantId
enableRbacAuthorization: true
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
}
// Create secret with secure parameter
@secure()
param adminPassword string
resource adminPasswordSecret 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: 'admin-password'
properties: {
value: adminPassword
contentType: 'password'
attributes: {
enabled: true
exp: dateTimeToEpoch(dateTimeAdd(utcNow(), 'P90D'))
}
}
}
// Create secret from resource output
resource storageAccountKey 'Microsoft.KeyVault/vaults/secrets@2023-07-01' = {
parent: keyVault
name: 'storage-account-key'
properties: {
value: storageAccount.listKeys().keys[0].value
contentType: 'storage-key'
}
}
Secure Parameters¶
// Secure string parameter (never logged)
@description('The administrator password for the SQL server')
@secure()
param sqlAdminPassword string
// Secure object parameter
@description('Credentials for external services')
@secure()
param credentials object
// Using secure parameters
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: sqlServerName
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: sqlAdminPassword
}
}
// Secure output
@description('Generated connection string')
@secure()
output connectionString string = 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${databaseName};'
Security Best Practices¶
Network Security¶
// Virtual Network with NSG
resource nsg 'Microsoft.Network/networkSecurityGroups@2023-05-01' = {
name: 'nsg-${subnetName}'
location: location
properties: {
securityRules: [
{
name: 'AllowHTTPS'
properties: {
priority: 100
direction: 'Inbound'
access: 'Allow'
protocol: 'Tcp'
sourcePortRange: '*'
destinationPortRange: '443'
sourceAddressPrefix: 'Internet'
destinationAddressPrefix: 'VirtualNetwork'
}
}
{
name: 'DenyAllInbound'
properties: {
priority: 4096
direction: 'Inbound'
access: 'Deny'
protocol: '*'
sourcePortRange: '*'
destinationPortRange: '*'
sourceAddressPrefix: '*'
destinationAddressPrefix: '*'
}
}
]
}
}
// Private Endpoint for Storage
resource privateEndpoint 'Microsoft.Network/privateEndpoints@2023-05-01' = {
name: 'pe-${storageAccount.name}'
location: location
properties: {
privateLinkServiceConnections: [
{
name: 'storage-connection'
properties: {
privateLinkServiceId: storageAccount.id
groupIds: [
'blob'
]
}
}
]
subnet: {
id: subnet.id
}
}
}
// Private DNS Zone
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = {
name: 'privatelink.blob.${environment().suffixes.storage}'
location: 'global'
}
resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = {
parent: privateDnsZone
name: 'vnet-link'
location: 'global'
properties: {
registrationEnabled: false
virtualNetwork: {
id: vnet.id
}
}
}
IAM and RBAC¶
// Role assignment for managed identity
param principalId string
@description('Built-in role definition IDs')
var roleDefinitions = {
contributor: 'b24988ac-6180-42a0-ab88-20f7382dd24c'
reader: 'acdd72a7-3385-48ef-bd42-f606fba81ae7'
storageBlobDataContributor: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
storageBlobDataReader: '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'
keyVaultSecretsUser: '4633458b-17de-408a-b874-0445c86b69e6'
}
// Assign Storage Blob Data Contributor to managed identity
resource storageRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(storageAccount.id, principalId, roleDefinitions.storageBlobDataContributor)
scope: storageAccount
properties: {
principalId: principalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.storageBlobDataContributor)
principalType: 'ServicePrincipal'
}
}
// Assign Key Vault Secrets User
resource keyVaultRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(keyVault.id, principalId, roleDefinitions.keyVaultSecretsUser)
scope: keyVault
properties: {
principalId: principalId
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitions.keyVaultSecretsUser)
principalType: 'ServicePrincipal'
}
}
Managed Identity¶
// User-assigned managed identity
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'id-${workloadName}-${environmentName}'
location: location
tags: tags
}
// App Service with managed identity
resource appService 'Microsoft.Web/sites@2023-01-01' = {
name: appServiceName
location: location
identity: {
type: 'SystemAssigned, UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
serverFarmId: appServicePlan.id
siteConfig: {
appSettings: [
{
name: 'AZURE_CLIENT_ID'
value: managedIdentity.properties.clientId
}
]
}
}
}
// Function App with system-assigned identity
resource functionApp 'Microsoft.Web/sites@2023-01-01' = {
name: functionAppName
location: location
kind: 'functionapp'
identity: {
type: 'SystemAssigned'
}
properties: {
serverFarmId: appServicePlan.id
}
}
// Output principal IDs for role assignments
output systemAssignedPrincipalId string = functionApp.identity.principalId
output userAssignedPrincipalId string = managedIdentity.properties.principalId
output userAssignedClientId string = managedIdentity.properties.clientId
Encryption¶
// Customer-managed key for storage
resource storageAccountCMK 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'${managedIdentity.id}': {}
}
}
properties: {
encryption: {
services: {
blob: {
enabled: true
keyType: 'Account'
}
file: {
enabled: true
keyType: 'Account'
}
}
keySource: 'Microsoft.Keyvault'
keyvaultproperties: {
keyname: encryptionKey.name
keyvaulturi: keyVault.properties.vaultUri
}
identity: {
userAssignedIdentity: managedIdentity.id
}
}
}
}
// SQL Database with TDE
resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-05-01-preview' = {
parent: sqlServer
name: databaseName
location: location
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
}
}
resource tde 'Microsoft.Sql/servers/databases/transparentDataEncryption@2023-05-01-preview' = {
parent: sqlDatabase
name: 'current'
properties: {
state: 'Enabled'
}
}
Testing¶
What-If Deployments¶
# Preview changes before deployment (Azure CLI)
az deployment group what-if \
--resource-group myResourceGroup \
--template-file main.bicep \
--parameters @parameters.prod.json
# Subscription-level what-if
az deployment sub what-if \
--location eastus \
--template-file subscription.bicep \
--parameters environmentName=prod
# Output what-if results as JSON
az deployment group what-if \
--resource-group myResourceGroup \
--template-file main.bicep \
--parameters @parameters.prod.json \
--out json > whatif-results.json
Bicep Linting¶
# Run Bicep linter
az bicep lint --file main.bicep
# Build with warnings as errors
az bicep build --file main.bicep --stdout 2>&1 | grep -E "(Warning|Error)"
# Lint all files in directory
find . -name "*.bicep" -exec az bicep lint --file {} \;
Unit Testing with PSRule¶
# Install PSRule for Azure
Install-Module -Name PSRule.Rules.Azure -Scope CurrentUser
# Run PSRule analysis
Assert-PSRule -Module PSRule.Rules.Azure -InputPath . -Format File -OutputFormat NUnit3 -OutputPath results.xml
# Configuration file: ps-rule.yaml
# binding:
# targetType:
# - resourceType
# - type
# input:
# pathIgnore:
# - ".git/**"
# - "*.md"
# output:
# culture:
# - en-US
# rule:
# include:
# - Azure.Resource.*
# - Azure.Storage.*
# - Azure.KeyVault.*
# ps-rule.yaml
binding:
targetType:
- resourceType
- type
configuration:
AZURE_BICEP_FILE_EXPANSION: true
AZURE_BICEP_FILE_EXPANSION_TIMEOUT: 10
input:
pathIgnore:
- ".git/**"
- "*.md"
- "**/*.json"
output:
culture:
- en-US
rule:
include:
- Azure.Resource.*
- Azure.Storage.*
- Azure.KeyVault.*
- Azure.SQL.*
- Azure.AppService.*
Integration Testing¶
// test/storage-account.test.bicep
// Test module for storage account
param testName string = 'storage-test-${uniqueString(resourceGroup().id)}'
// Deploy module under test
module storageUnderTest '../modules/storage-account.bicep' = {
name: 'storage-test-deployment'
params: {
storageAccountName: testName
location: resourceGroup().location
sku: 'Standard_LRS'
}
}
// Assertions via outputs
output testResults object = {
passed: true
tests: [
{
name: 'Storage account created'
result: !empty(storageUnderTest.outputs.id)
}
{
name: 'Storage account name matches'
result: storageUnderTest.outputs.name == testName
}
{
name: 'Blob endpoint available'
result: contains(storageUnderTest.outputs.primaryBlobEndpoint, 'blob.core.windows.net')
}
]
}
# Run integration test
az deployment group create \
--resource-group test-rg \
--template-file test/storage-account.test.bicep \
--query "properties.outputs.testResults.value" \
--output json
# Clean up test resources
az group delete --name test-rg --yes --no-wait
CI/CD Integration¶
GitHub Actions Workflow¶
name: Bicep CI/CD
on:
push:
branches: [main]
paths:
- 'bicep/**'
pull_request:
branches: [main]
paths:
- 'bicep/**'
permissions:
id-token: write
contents: read
env:
AZURE_RESOURCEGROUP_NAME: myResourceGroup
AZURE_LOCATION: eastus
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Bicep
run: |
az bicep install
az bicep version
- name: Lint Bicep files
run: |
find bicep -name "*.bicep" -exec az bicep lint --file {} \;
- name: Build Bicep files
run: |
find bicep -name "*.bicep" -exec az bicep build --file {} \;
- name: Run PSRule analysis
uses: microsoft/ps-rule@v2
with:
modules: PSRule.Rules.Azure
inputPath: bicep/
outputFormat: NUnit3
outputPath: reports/ps-rule-results.xml
- name: Upload PSRule results
uses: actions/upload-artifact@v4
if: always()
with:
name: psrule-results
path: reports/
preview:
needs: validate
runs-on: ubuntu-latest
environment: development
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: What-If deployment
uses: azure/arm-deploy@v2
with:
resourceGroupName: ${{ env.AZURE_RESOURCEGROUP_NAME }}
template: bicep/main.bicep
parameters: bicep/parameters/dev.bicepparam
additionalArguments: --what-if
deploy-dev:
needs: preview
runs-on: ubuntu-latest
environment: development
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Dev
uses: azure/arm-deploy@v2
with:
resourceGroupName: ${{ env.AZURE_RESOURCEGROUP_NAME }}
template: bicep/main.bicep
parameters: bicep/parameters/dev.bicepparam
failOnStdErr: false
deploy-prod:
needs: deploy-dev
runs-on: ubuntu-latest
environment: production
if: github.ref == 'refs/heads/main'
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy to Production
uses: azure/arm-deploy@v2
with:
resourceGroupName: rg-prod
template: bicep/main.bicep
parameters: bicep/parameters/prod.bicepparam
failOnStdErr: false
Azure DevOps Pipeline¶
# azure-pipelines.yml
trigger:
branches:
include:
- main
paths:
include:
- bicep/**
pool:
vmImage: ubuntu-latest
variables:
- group: azure-credentials
- name: resourceGroupName
value: myResourceGroup
- name: location
value: eastus
stages:
- stage: Validate
displayName: Validate Bicep
jobs:
- job: ValidateBicep
displayName: Lint and Build
steps:
- task: AzureCLI@2
displayName: Install Bicep
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az bicep install
az bicep version
- task: AzureCLI@2
displayName: Lint Bicep files
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
find bicep -name "*.bicep" -exec az bicep lint --file {} \;
- task: AzureCLI@2
displayName: Build Bicep files
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
find bicep -name "*.bicep" -exec az bicep build --file {} \;
- task: ps-rule-assert@2
displayName: Run PSRule
inputs:
inputType: inputPath
inputPath: bicep/
modules: PSRule.Rules.Azure
outputFormat: NUnit3
outputPath: $(Build.ArtifactStagingDirectory)/ps-rule-results.xml
- task: PublishTestResults@2
displayName: Publish PSRule Results
inputs:
testResultsFormat: NUnit
testResultsFiles: '$(Build.ArtifactStagingDirectory)/ps-rule-results.xml'
failTaskOnFailedTests: true
- stage: DeployDev
displayName: Deploy to Dev
dependsOn: Validate
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: DeployDev
displayName: Deploy to Dev Environment
environment: development
strategy:
runOnce:
deploy:
steps:
- checkout: self
- task: AzureCLI@2
displayName: What-If Deployment
inputs:
azureSubscription: $(azureServiceConnection)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az deployment group what-if \
--resource-group $(resourceGroupName) \
--template-file bicep/main.bicep \
--parameters @bicep/parameters/dev.bicepparam
- task: AzureResourceManagerTemplateDeployment@3
displayName: Deploy Bicep
inputs:
azureResourceManagerConnection: $(azureServiceConnection)
subscriptionId: $(subscriptionId)
resourceGroupName: $(resourceGroupName)
location: $(location)
templateLocation: linkedArtifact
csmFile: bicep/main.bicep
csmParametersFile: bicep/parameters/dev.bicepparam
deploymentMode: Incremental
Deployment Script¶
#!/bin/bash
# deploy.sh - Bicep deployment script
set -euo pipefail
# Configuration
RESOURCE_GROUP="${RESOURCE_GROUP:-myResourceGroup}"
LOCATION="${LOCATION:-eastus}"
ENVIRONMENT="${ENVIRONMENT:-dev}"
TEMPLATE_FILE="${TEMPLATE_FILE:-main.bicep}"
PARAMETERS_FILE="${PARAMETERS_FILE:-parameters/${ENVIRONMENT}.bicepparam}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Validate Bicep files
validate() {
log_info "Validating Bicep files..."
az bicep build --file "$TEMPLATE_FILE" --stdout > /dev/null
log_info "Validation successful"
}
# Run what-if deployment
whatif() {
log_info "Running what-if deployment..."
az deployment group what-if \
--resource-group "$RESOURCE_GROUP" \
--template-file "$TEMPLATE_FILE" \
--parameters "@$PARAMETERS_FILE"
}
# Deploy resources
deploy() {
log_info "Deploying to resource group: $RESOURCE_GROUP"
# Create resource group if it doesn't exist
if ! az group show --name "$RESOURCE_GROUP" &>/dev/null; then
log_info "Creating resource group: $RESOURCE_GROUP"
az group create --name "$RESOURCE_GROUP" --location "$LOCATION"
fi
# Deploy
az deployment group create \
--resource-group "$RESOURCE_GROUP" \
--template-file "$TEMPLATE_FILE" \
--parameters "@$PARAMETERS_FILE" \
--name "deployment-$(date +%Y%m%d%H%M%S)"
log_info "Deployment completed successfully"
}
# Main
case "${1:-deploy}" in
validate) validate ;;
whatif) whatif ;;
deploy) validate && deploy ;;
*)
echo "Usage: $0 {validate|whatif|deploy}"
exit 1
;;
esac
IDE Integration¶
VS Code Configuration¶
// .vscode/settings.json
{
"bicep.decompileOnPaste": true,
"bicep.enableOutputTimestamps": true,
"[bicep]": {
"editor.defaultFormatter": "ms-azuretools.vscode-bicep",
"editor.formatOnSave": true,
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.rulers": [120]
},
"files.associations": {
"*.bicep": "bicep",
"*.bicepparam": "bicep-params"
},
"editor.quickSuggestions": {
"strings": true
}
}
// .vscode/extensions.json
{
"recommendations": [
"ms-azuretools.vscode-bicep",
"ms-vscode.azure-account",
"ms-azuretools.vscode-azureresourcegroups",
"bewhite.psrule-vscode"
]
}
bicepconfig.json¶
{
"analyzers": {
"core": {
"enabled": true,
"rules": {
"no-hardcoded-env-urls": {
"level": "error"
},
"no-unused-params": {
"level": "warning"
},
"no-unused-vars": {
"level": "warning"
},
"prefer-interpolation": {
"level": "warning"
},
"secure-parameter-default": {
"level": "error"
},
"simplify-interpolation": {
"level": "warning"
},
"use-recent-api-versions": {
"level": "warning",
"maxAgeInDays": 730
},
"use-secure-value-for-secure-inputs": {
"level": "error"
},
"adminusername-should-not-be-literal": {
"level": "error"
},
"outputs-should-not-contain-secrets": {
"level": "error"
},
"max-outputs": {
"level": "warning"
},
"max-params": {
"level": "warning"
},
"max-resources": {
"level": "warning"
},
"max-variables": {
"level": "warning"
},
"explicit-values-for-loc-params": {
"level": "warning"
}
}
}
},
"moduleAliases": {
"br": {
"public": {
"registry": "mcr.microsoft.com",
"modulePath": "bicep"
},
"myregistry": {
"registry": "myregistry.azurecr.io",
"modulePath": "bicep/modules"
}
},
"ts": {
"myspecs": {
"subscription": "00000000-0000-0000-0000-000000000000",
"resourceGroup": "template-specs-rg"
}
}
},
"experimentalFeaturesEnabled": {
"assertions": true,
"extensibility": true,
"resourceTypedParamsAndOutputs": true,
"sourceMapping": true,
"symbolicNameCodegen": true
}
}
Project Structure¶
Single Module¶
project/
├── main.bicep # Main deployment file
├── bicepconfig.json # Bicep configuration
├── parameters/
│ ├── dev.bicepparam # Development parameters
│ ├── staging.bicepparam # Staging parameters
│ └── prod.bicepparam # Production parameters
└── README.md # Documentation
Multi-Module Project¶
project/
├── bicepconfig.json # Bicep configuration
├── main.bicep # Main orchestration file
├── modules/
│ ├── networking/
│ │ ├── virtual-network.bicep
│ │ ├── network-security-group.bicep
│ │ └── private-endpoint.bicep
│ ├── compute/
│ │ ├── app-service.bicep
│ │ ├── function-app.bicep
│ │ └── container-apps.bicep
│ ├── storage/
│ │ ├── storage-account.bicep
│ │ └── cosmos-db.bicep
│ ├── security/
│ │ ├── key-vault.bicep
│ │ └── managed-identity.bicep
│ └── monitoring/
│ ├── log-analytics.bicep
│ └── app-insights.bicep
├── parameters/
│ ├── dev.bicepparam
│ ├── staging.bicepparam
│ └── prod.bicepparam
├── test/
│ ├── storage-account.test.bicep
│ └── networking.test.bicep
├── .github/
│ └── workflows/
│ └── bicep-ci.yml
└── README.md
Enterprise Landing Zone¶
landing-zone/
├── bicepconfig.json
├── platform/
│ ├── management-groups/
│ │ └── main.bicep # Management group hierarchy
│ ├── connectivity/
│ │ ├── hub-network.bicep # Hub virtual network
│ │ ├── firewall.bicep # Azure Firewall
│ │ └── dns.bicep # Private DNS zones
│ ├── identity/
│ │ └── aad-config.bicep # Azure AD configuration
│ └── management/
│ ├── log-analytics.bicep # Central logging
│ └── automation.bicep # Automation account
├── policies/
│ ├── definitions/
│ │ ├── require-tags.bicep
│ │ └── allowed-locations.bicep
│ └── assignments/
│ └── baseline.bicep
├── landing-zones/
│ ├── corp/
│ │ └── main.bicep # Corporate landing zone
│ └── online/
│ └── main.bicep # Online landing zone
├── workloads/
│ ├── app-template/
│ │ └── main.bicep # Application template
│ └── data-template/
│ └── main.bicep # Data platform template
└── parameters/
├── platform/
│ ├── dev.bicepparam
│ └── prod.bicepparam
└── workloads/
├── app1-dev.bicepparam
└── app1-prod.bicepparam
Anti-Patterns¶
Hardcoded Values¶
// Bad - Hardcoded values
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'mystorageaccount123' // Hardcoded name
location: 'eastus' // Hardcoded location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
// Good - Parameterized values
@description('Storage account name')
param storageAccountName string
@description('Location for resources')
param location string = resourceGroup().location
@description('Storage SKU')
@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_RAGRS'])
param storageSku string = 'Standard_LRS'
resource storageAccountGood 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: storageSku
}
kind: 'StorageV2'
properties: {}
}
Missing Validation¶
// Bad - No parameter validation
param environmentName string
param instanceCount int
param prefix string
// Good - Proper validation
@description('Environment name')
@allowed(['dev', 'staging', 'prod'])
param environmentNameValidated string
@description('Number of instances')
@minValue(1)
@maxValue(10)
param instanceCountValidated int = 2
@description('Resource prefix')
@minLength(3)
@maxLength(10)
param prefixValidated string
Secrets in Plain Text¶
// Bad - Password as plain parameter
param adminPassword string = 'P@ssw0rd123!' // NEVER DO THIS
resource sqlServer 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: sqlServerName
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: adminPassword // Exposed in deployment logs
}
}
// Good - Secure parameter with Key Vault
@description('Admin password')
@secure()
param adminPasswordSecure string
// Or use Key Vault reference
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: keyVaultName
scope: resourceGroup(keyVaultRg)
}
resource sqlServerSecure 'Microsoft.Sql/servers@2023-05-01-preview' = {
name: sqlServerName
location: location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: keyVault.getSecret('sql-admin-password')
}
}
Missing Tags¶
// Bad - No tags
resource storageAccountNoTags 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
// Good - Comprehensive tagging
var defaultTags = {
Environment: environmentName
Application: applicationName
Owner: ownerEmail
CostCenter: costCenter
ManagedBy: 'Bicep'
DeployedAt: utcNow('yyyy-MM-dd')
}
resource storageAccountWithTags 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
tags: union(defaultTags, customTags)
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
Overly Permissive Network Rules¶
// Bad - Allow all traffic
resource storageAccountOpen 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
networkAcls: {
defaultAction: 'Allow' // Anyone can access
}
}
}
// Good - Deny by default with specific rules
resource storageAccountSecure 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: false
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
virtualNetworkRules: [
{
id: subnet.id
action: 'Allow'
}
]
ipRules: []
}
}
}
Monolithic Templates¶
// Bad - Everything in one file (500+ lines)
// main.bicep with all resources mixed together
// Good - Modular approach
// main.bicep - Orchestration only
module networkModule 'modules/networking/virtual-network.bicep' = {
name: 'network-deployment'
params: {
vnetName: vnetName
location: location
}
}
module storageModule 'modules/storage/storage-account.bicep' = {
name: 'storage-deployment'
params: {
storageAccountName: storageAccountName
location: location
subnetId: networkModule.outputs.subnetId
}
}
module appModule 'modules/compute/app-service.bicep' = {
name: 'app-deployment'
params: {
appName: appName
location: location
storageConnectionString: storageModule.outputs.connectionString
}
}
Tool Configuration¶
Pre-commit Configuration¶
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: bicep-lint
name: Bicep Lint
entry: bash -c 'find . -name "*.bicep" -exec az bicep lint --file {} \;'
language: system
files: \.bicep$
- id: bicep-build
name: Bicep Build
entry: bash -c 'find . -name "*.bicep" -exec az bicep build --file {} \;'
language: system
files: \.bicep$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-json
- repo: https://github.com/adrienverge/yamllint
rev: v1.33.0
hooks:
- id: yamllint
args: [--config-file, .yamllint.yaml]
EditorConfig¶
# .editorconfig
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.bicep]
indent_size = 2
[*.{json,bicepparam}]
indent_size = 2
[*.md]
trim_trailing_whitespace = false
References¶
Official Documentation¶
- Bicep Documentation
- Bicep Language Reference
- Azure Resource Manager Templates
- Azure Verified Modules
Tools¶
Related Guides¶
Status: Active