Azure Bicep
Bicep is a declarative language for deploying Azure resources through Azure Resource Manager. It provides concise syntax, type validation, modules, and direct access to Azure resource types without requiring a separate state file.
A production-safe workflow is: author the template, lint and build it, validate it at the intended scope, review a what-if result, deploy with an authorized identity, and verify the resources.
Who this guide is for
This guide explains:
- Bicep syntax and resource declarations.
- Parameters, variables, outputs, conditions, and loops.
- Modules and deployment scopes.
- Azure CLI and PowerShell deployment.
- Safe secret handling.
- CI/CD validation and
what-if. - Common compilation and deployment failures.
Why use Bicep
- It is designed specifically for Azure Resource Manager.
- It supports existing Azure resource types and API versions.
- It provides compile-time validation and editor tooling.
- It supports reusable modules.
- Azure stores deployment history without Bicep requiring a separate state backend.
- It can work with template specs, deployment stacks, and registries.
Bicep simplifies template authoring; it does not remove the need for RBAC, policy compliance, safe rollout planning, or resource validation.
Install and verify Bicep
Azure CLI can install and manage the Bicep CLI:
az bicep install
az bicep version
Upgrade when planned and tested:
az bicep upgrade
Use the official Bicep extension for Visual Studio Code for completion, type information, navigation, linting, and diagnostics.
Basic Bicep file
targetScope = 'resourceGroup'
@description('Azure region for resources.')
param location string = resourceGroup().location
@allowed([
'dev'
'test'
'prod'
])
param environment string
var commonTags = {
environment: environment
managedBy: 'bicep'
workload: 'cloudforge-example'
}
resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = {
name: '<globally-unique-storage-name>'
location: location
tags: commonTags
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
output storageAccountId string = storage.id
The API version is an example. Confirm the current supported version and property behavior for the target resource before publication or production deployment.
Resource symbolic names
In this declaration:
resource storage 'Microsoft.Storage/storageAccounts@2025-01-01' = {
// ...
}
storage is the symbolic name used inside the Bicep file. It is not the Azure resource name. References such as storage.id create an inferred dependency.
Avoid adding dependsOn when Bicep can infer the dependency from a resource reference.
Parameters and parameter files
Use parameters for values that vary by environment. Use .bicepparam files to supply non-secret environment configuration.
main.bicepparam:
using './main.bicep'
param environment = 'dev'
param location = 'westeurope'
Do not place real secrets in a parameter file committed to source control.
Use decorators to improve validation:
@description('Number of application instances.')
@minValue(1)
@maxValue(10)
param instanceCount int = 2
@description('Administrative password supplied securely during deployment.')
@secure()
param adminPassword string
@secure() prevents the value from being recorded or displayed in some normal deployment contexts, but the value must still be supplied and handled securely.
Existing resources
Reference a resource without redeploying it:
param existingKeyVaultName string
resource keyVault 'Microsoft.KeyVault/vaults@2025-05-01' existing = {
name: existingKeyVaultName
}
output keyVaultId string = keyVault.id
An existing declaration does not grant access and does not validate that every dependent operation is authorized.
Conditions
Deploy a resource only when a condition is true:
param deployDiagnostics bool = true
resource diagnosticStorage 'Microsoft.Storage/storageAccounts@2025-01-01' = if (deployDiagnostics) {
name: '<globally-unique-diagnostic-storage-name>'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: false
minimumTlsVersion: 'TLS1_2'
supportsHttpsTrafficOnly: true
}
}
Check references to conditional resources carefully. A conditional declaration does not automatically make all references to that resource conditional.
Loops
Create multiple resources from an array:
param subnetDefinitions array = [
{
name: 'snet-app'
prefix: '10.20.1.0/24'
}
{
name: 'snet-data'
prefix: '10.20.2.0/24'
}
]
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2024-05-01' = {
name: 'vnet-cloudforge'
location: resourceGroup().location
properties: {
addressSpace: {
addressPrefixes: [
'10.20.0.0/16'
]
}
subnets: [for subnet in subnetDefinitions: {
name: subnet.name
properties: {
addressPrefix: subnet.prefix
}
}]
}
}
For stable deployments, derive resource identity from stable keys rather than array positions that may change during reordering.
Modules
Suggested structure:
infra/
├── main.bicep
├── main.bicepparam
└── modules/
├── storage.bicep
├── key-vault.bicep
└── monitoring.bicep
Module example:
module storageModule './modules/storage.bicep' = {
name: 'storage-${environment}'
params: {
location: location
environment: environment
}
}
output storageAccountId string = storageModule.outputs.storageAccountId
Use modules to create meaningful platform components, not merely to wrap every single resource. Give each module a clear interface, description, version strategy, example, and expected scope.
For sharing across repositories, consider a private Bicep registry or template specs with controlled versioning.
Deployment scopes
Bicep supports resource group, subscription, management group, and tenant scopes.
Resource-group deployment:
targetScope = 'resourceGroup'
Subscription deployment:
targetScope = 'subscription'
param resourceGroupName string
param location string
resource appResourceGroup 'Microsoft.Resources/resourceGroups@2024-11-01' = {
name: resourceGroupName
location: location
}
Use the narrowest scope that can complete the job. Higher-scope deployments require broader permissions and deserve stronger review.
Build and lint
Compile Bicep to an ARM JSON template:
az bicep build --file main.bicep
Treat linter warnings intentionally. Configure rules centrally where appropriate, and document justified exceptions rather than disabling broad validation without review.
Validate and preview
Log in and select the intended subscription:
az login
az account set --subscription <subscription-id>
az account show --output table
Validate a resource-group deployment:
az deployment group validate \
--resource-group <resource-group> \
--template-file main.bicep \
--parameters main.bicepparam
Preview with what-if:
az deployment group what-if \
--resource-group <resource-group> \
--template-file main.bicep \
--parameters main.bicepparam
Review creates, changes, deletions, and replacements. what-if is an important preview but may contain noise or incomplete predictions for some resource behavior. Apply human review to high-impact changes.
Deploy with Azure CLI
az deployment group create \
--name cloudforge-$(date +%Y%m%d%H%M%S) \
--resource-group <resource-group> \
--template-file main.bicep \
--parameters main.bicepparam
Verify deployment status:
az deployment group show \
--resource-group <resource-group> \
--name <deployment-name> \
--query properties.provisioningState \
--output tsv
Deploy with Azure PowerShell
Connect-AzAccount
Set-AzContext -SubscriptionId '<subscription-id>'
Test-AzResourceGroupDeployment `
-ResourceGroupName '<resource-group>' `
-TemplateFile './main.bicep' `
-TemplateParameterFile './main.bicepparam'
New-AzResourceGroupDeployment `
-Name "cloudforge-$((Get-Date).ToString('yyyyMMddHHmmss'))" `
-ResourceGroupName '<resource-group>' `
-TemplateFile './main.bicep' `
-TemplateParameterFile './main.bicepparam'
Confirm the exact cmdlet support for .bicepparam with the Az version used by the deployment environment.
Secret handling
- Prefer managed identity and workload identity for runtime access.
- Retrieve deployment-time secrets from an approved secret store when a resource API truly requires a value.
- Mark secret parameters with
@secure(). - Do not output secrets.
- Do not embed secrets in source code, parameter files, pipeline variables, deployment names, or tags.
- Avoid calling
list*functions unless the returned secret value is truly necessary. - Restrict deployment-history access because parameters and outputs may reveal operational information.
CI/CD workflow
A pull-request validation job can run:
az bicep build --file infra/main.bicep
az deployment group validate \
--resource-group <validation-resource-group> \
--template-file infra/main.bicep \
--parameters infra/environments/dev.bicepparam
A protected deployment job should:
- Authenticate with workload identity federation or managed identity.
- Select and display the non-secret target context.
- Run validation.
- Generate and retain a readable
what-ifresult. - Require appropriate approval for production changes.
- Deploy the reviewed version.
- Verify resource and application health.
Do not grant broad Owner permission to a routine deployment identity. Assign only the actions required by the template, including separate identity-assignment permissions when necessary.
Deployment modes and deletion risk
Incremental deployments are commonly used. Complete-mode behavior can delete resources not present in the template and therefore requires careful scope analysis and governance.
Deployment stacks provide additional lifecycle management capabilities, but deletion behavior must still be reviewed and tested. Never introduce a deletion-capable deployment pattern directly into production without a controlled trial.
Bicep types and nullable values
Use specific types to catch invalid input early. User-defined object types can document a module interface:
type subnetConfiguration = {
name: string
addressPrefix: string
serviceEndpoints: string[]?
}
@description('Subnets created inside the virtual network.')
param subnets subnetConfiguration[]
Use optional or nullable values intentionally. Avoid accepting broad object or array parameters when the required structure is known.
Functions and expressions
Bicep includes functions for strings, arrays, objects, resources, scopes, dates, deployment information, and other transformations.
var normalizedEnvironment = toLower(environment)
var appName = take('app-${normalizedEnvironment}-${uniqueString(subscription().id, resourceGroup().id)}', 60)
uniqueString() is deterministic, not random and not secret. Use it for repeatable naming, not passwords or cryptographic material.
Be careful with values derived from the current date or deployment name. If they change on every deployment, they can create continuous drift or force avoidable updates.
Outputs and information exposure
Return only what downstream automation needs:
output webAppResourceId string = webApp.id
output webAppHostname string = webApp.properties.defaultHostName
Never output secret values. Even a secure input can become exposed if returned through an ordinary output, written into tags, or included in deployment scripts.
Child and extension resources
Child resources belong to a parent resource. Use the parent property where supported:
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-05-01' = {
parent: storage
name: 'default'
}
Extension resources add configuration such as diagnostic settings or role assignments to another resource. Confirm the deployment scope and target scope; many authorization errors come from assigning the extension at the wrong level.
Cross-scope modules
A resource-group deployment can invoke a module at another permitted scope:
module sharedMonitoring './modules/monitoring.bicep' = {
name: 'shared-monitoring-${environment}'
scope: resourceGroup(sharedSubscriptionId, sharedResourceGroupName)
params: {
environment: environment
}
}
The deployment identity must have permission at every target scope. Review cross-subscription and management-group deployments particularly carefully because their impact boundary is larger.
Module registries and versioning
An Azure Container Registry can host private Bicep modules. A module alias can keep source references readable.
module network 'br/contoso:network/vnet:2.1.0' = {
name: 'network-${environment}'
params: {
location: location
environment: environment
}
}
The registry and alias are examples. Use an approved registry, immutable semantic versions, access controls, retention, and a release process.
Do not republish a different module under an existing production version. Consumers must be able to reproduce what was deployed.
Template specs
Template specs store ARM templates as versioned Azure resources. They can be useful when organizations distribute approved templates through Azure RBAC and portal experiences.
Choose between a Bicep registry and template specs based on authoring workflow, consumer experience, versioning, permissions, and deployment requirements. Document which artifact is the source of truth.
Bicep configuration and linting
bicepconfig.json controls analyzers and module aliases for a directory tree.
{
"analyzers": {
"core": {
"enabled": true,
"rules": {
"no-hardcoded-env-urls": {
"level": "warning"
},
"no-unused-params": {
"level": "error"
},
"no-unused-vars": {
"level": "error"
}
}
}
}
}
Treat configuration as shared engineering policy. Review rule changes like code; do not downgrade a useful rule merely to make the pipeline green.
Deployment scripts
A deployment script can run Azure CLI or Azure PowerShell during an ARM deployment. Use one only when the operation cannot be represented as a normal resource.
Deployment scripts introduce identity, storage, networking, cleanup, logging, timeout, and idempotency concerns. Pin runtime versions where supported and never print secure parameters.
Prefer a first-class Azure resource declaration or a separate controlled pipeline step when either provides clearer lifecycle ownership.
Role assignments
Role-assignment names should be deterministic so repeated deployments refer to the same assignment:
param principalId string
param roleDefinitionResourceId string
resource assignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, roleDefinitionResourceId)
properties: {
principalId: principalId
roleDefinitionId: roleDefinitionResourceId
principalType: 'ServicePrincipal'
}
}
The deployment identity needs permission to create role assignments. Use the narrowest scope and verify the principal type. RBAC propagation may delay immediate access even after deployment succeeds.
Key Vault design
For a production Key Vault module, consider:
- RBAC authorization model and separation of duties.
- Soft delete and purge protection requirements.
- Public network access and private endpoints.
- Firewall and trusted service behavior.
- Diagnostic settings and retention.
- Workload identity and role assignments.
- Certificate, key, and secret rotation ownership.
- Recovery and break-glass access.
Infrastructure code should create the vault and access model, but application secret values should enter through an approved secret lifecycle—not ordinary source-controlled parameters.
Private endpoints and DNS
A private endpoint alone does not complete private connectivity. The client must resolve the service name to the private endpoint address and route traffic to it.
Validate:
- Private endpoint provisioning state.
- Private DNS zone and record.
- Virtual-network links.
- Custom DNS forwarding when used.
- Network path and security rules.
- Public network access setting.
- Application identity and service authorization.
Document DNS resources in the same or an explicitly dependent module so ownership is clear.
Deployment stacks
Deployment stacks can manage a collection of resources as a unit and define behavior when resources are removed from the stack. This makes lifecycle intent more explicit but also increases the need to understand detach and delete behavior.
Before using a stack in production:
- Test unmanaged-resource behavior.
- Review deny settings.
- Confirm resource support.
- Generate a
what-ifresult. - Protect data-bearing resources.
- Define recovery when a resource is accidentally removed from the template.
Testing strategy
Use layered validation:
- Editor diagnostics and linter.
az bicep buildcompilation.- ARM validation at the intended scope.
what-ifreview.- Policy and security scanning.
- Deployment into a disposable or isolated environment.
- Post-deployment checks against resource properties and application health.
Compilation validates syntax and types known to Bicep. It cannot prove that quota is available, policy allows the resource, the identity is authorized, or the application works.
Complete platform deployment layout
infra/
├── bicepconfig.json
├── main.bicep
├── environments/
│ ├── dev.bicepparam
│ ├── test.bicepparam
│ └── prod.bicepparam
├── modules/
│ ├── identity.bicep
│ ├── monitoring.bicep
│ ├── network.bicep
│ ├── key-vault.bicep
│ └── web-app.bicep
└── tests/
└── deployment-smoke-tests.ps1
The root template coordinates modules. Environment parameter files contain non-secret differences. Module outputs connect dependencies. The deployment pipeline validates, previews, deploys, and verifies.
Troubleshooting decision table
| Symptom | Evidence | Likely cause | Safe next step |
|---|---|---|---|
| Build diagnostic | File, line, error code | Syntax, type, module path | Fix the first compiler error |
| Validation denied | Inner Azure error | Policy, quota, invalid property | Inspect the exact policy or resource rule |
| Authorization failure | Principal, action, scope | Missing RBAC | Grant only the required action |
| Existing resource not found | Resource ID and scope | Wrong subscription/RG/name | Verify scope and existing declaration |
| Role assignment conflict | Deterministic name and scope | Different assignment under same name | Recalculate identity tuple and inspect existing role |
| Private service unreachable | DNS and endpoint state | Missing zone link or route | Test name resolution from the workload network |
what-if shows deletes | Scope and lifecycle mode | Stack/complete behavior or moved ownership | Stop and review resource ownership |
| Deployment succeeds but app fails | Health logs and settings | Runtime configuration or dependency | Diagnose application startup separately |
Troubleshooting
Bicep compilation fails
Read the first diagnostic and its file location. Common causes include invalid syntax, wrong property types, inaccessible module paths, unsupported API properties, and circular dependencies.
az bicep build --file main.bicep
InvalidTemplateDeployment
Inspect the inner error, not only the top-level message. Check Azure Policy, region availability, SKU restrictions, quota, naming rules, dependency failures, and property combinations.
AuthorizationFailed
Confirm the deployment identity, scope, operation, and role assignment. Role changes may take time to propagate. A deployment that creates role assignments requires permissions beyond ordinary resource creation.
ResourceNotFound
Check the subscription, resource group, resource name, scope, dependency order, and API version. For existing resources in another scope, declare the correct scope explicitly.
ParentResourceNotFound
The child resource name or parent relationship is wrong, or the parent exists in another scope. Use the parent property where supported and reference the parent symbolically.
DeploymentActiveAndUneditable
Another deployment with the same name may still be running. Use unique deployment names in pipelines and inspect the active deployment rather than forcing a conflicting update.
what-if shows unexpected deletion
Stop and confirm the target scope, deployment mode or stack behavior, resource ownership, and template version. Check whether resources were moved between modules or scopes.
API version or property error
Use the current Bicep resource reference for that resource type. Newest is not automatically safest; select an API version that is supported and tested for required features.
Secret appears in deployment output
Remove the output immediately, rotate the exposed value, and review deployment history and pipeline logs. @secure() should be applied to sensitive parameters, and secrets should never be emitted as outputs.
Command reference
az bicep version
az bicep build --file main.bicep
az bicep decompile --file template.json
az deployment group validate --resource-group <rg> --template-file main.bicep
az deployment group what-if --resource-group <rg> --template-file main.bicep
az deployment group create --resource-group <rg> --template-file main.bicep
az deployment group list --resource-group <rg> --output table
Decompiled templates require review and refactoring; decompilation is a starting point, not a guarantee of clean or production-ready Bicep.
Frequently asked questions
Does Bicep require a state file?
No separate Bicep state file is required. Azure Resource Manager processes deployments and Azure holds the resource state and deployment history.
Is what-if the same as a deployment approval?
No. It previews expected changes. High-impact production changes still require review, authorization, verification, and rollback planning.
Should every resource be placed in its own module?
No. Create modules around coherent, reusable capabilities with useful interfaces.
Can Bicep deploy at subscription level?
Yes. Set targetScope = 'subscription' and use subscription deployment commands. Higher scopes require suitable permissions.
Bicep or Terraform?
Bicep provides an Azure-native authoring experience. Terraform provides a multi-provider workflow and maintains its own state. Choose based on scope, governance, team experience, and integration requirements.