Infrastructure as code24 min read

Azure Bicep

Learn Bicep syntax, modules, deployment scopes, what-if validation, security controls, CI/CD, and Azure deployment troubleshooting.

Production-aware guide

Review scope, permissions, impact, and rollback before applying changes.

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:

  1. Authenticate with workload identity federation or managed identity.
  2. Select and display the non-secret target context.
  3. Run validation.
  4. Generate and retain a readable what-if result.
  5. Require appropriate approval for production changes.
  6. Deploy the reviewed version.
  7. 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:

  1. Private endpoint provisioning state.
  2. Private DNS zone and record.
  3. Virtual-network links.
  4. Custom DNS forwarding when used.
  5. Network path and security rules.
  6. Public network access setting.
  7. 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-if result.
  • Protect data-bearing resources.
  • Define recovery when a resource is accidentally removed from the template.

Testing strategy

Use layered validation:

  1. Editor diagnostics and linter.
  2. az bicep build compilation.
  3. ARM validation at the intended scope.
  4. what-if review.
  5. Policy and security scanning.
  6. Deployment into a disposable or isolated environment.
  7. 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

SymptomEvidenceLikely causeSafe next step
Build diagnosticFile, line, error codeSyntax, type, module pathFix the first compiler error
Validation deniedInner Azure errorPolicy, quota, invalid propertyInspect the exact policy or resource rule
Authorization failurePrincipal, action, scopeMissing RBACGrant only the required action
Existing resource not foundResource ID and scopeWrong subscription/RG/nameVerify scope and existing declaration
Role assignment conflictDeterministic name and scopeDifferent assignment under same nameRecalculate identity tuple and inspect existing role
Private service unreachableDNS and endpoint stateMissing zone link or routeTest name resolution from the workload network
what-if shows deletesScope and lifecycle modeStack/complete behavior or moved ownershipStop and review resource ownership
Deployment succeeds but app failsHealth logs and settingsRuntime configuration or dependencyDiagnose 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.

Official references