Theoretical Foundation: Modify an existing Bicep file
1. Initial Intuitionβ
In the previous two modules, we learned to interpret templates (read and understand) and modify JSON ARM Templates (edit the traditional format). This module focuses specifically on the techniques and particularities of modifying Bicep files.
Although Bicep and ARM Template describe the same infrastructure, they have very different syntaxes, and modification techniques have specific nuances for each language. Modifying a Bicep file is much more like programming than editing a configuration document. Bicep has types, expressions, loops, conditionals, and modules. This means you can apply software engineering principles directly to infrastructure.
The most accurate analogy: modifying an ARM JSON is like editing a blueprint in specification table format. Modifying a Bicep file is like editing source code of a program. The tools are better, the code is more readable, and errors are detected before deployment.
2. Contextβ
Bicep has been Microsoft's preferred language for IaC on Azure since 2021. Unlike ARM JSON which is generated by tools and rarely written by hand, Bicep is designed to be written, read, and modified by humans.
Bicep compiles to ARM Template before deployment. You can call az deployment group create directly with a .bicep file and the CLI compiles automatically. This means modifying Bicep is the modern way to modify Azure infrastructure.
3. Building the Conceptsβ
3.1 Anatomy of a Bicep File: What Can Be Modifiedβ
A Bicep file is composed of declarative elements that you can add, change, or remove:
3.2 Parameter Modifications: Decoratorsβ
Bicep's decorator system is much richer than equivalent properties in ARM JSON. Modifying parameters in Bicep means adding, removing, or changing decorators:
// Simple parameter (before modification)
param storageAccountName string
// Parameter with complete validation (after modification)
@description('Storage account name. Must be globally unique, lowercase letters and numbers only.')
@minLength(3)
@maxLength(24)
param storageAccountName string
// Add allowed value restrictions
@description('Storage account SKU')
@allowed([
'Standard_LRS'
'Standard_GRS'
'Standard_ZRS'
'Premium_LRS'
])
param storageSku string = 'Standard_LRS'
// Make parameter secure (doesn't appear in logs)
@secure()
param adminPassword string
The @secure() decorator is especially important: it makes Bicep mark the parameter as securestring in the compiled ARM Template. This prevents the value from appearing in deployment history or Azure Monitor logs.
3.3 Variable Modifications: Expressions and Interpolationβ
Variables in Bicep support complete expressions, including the ternary operator ?: (inline if/else):
// Simple variable
var storageSkuName = 'Standard_LRS'
// Conditional variable (modification to support multiple environments)
param environment string = 'dev'
var storageSkuName = environment == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
String interpolation: unlike ARM Template which uses concat(), Bicep uses interpolation with ${}:
// Before: using concat (ARM style in decompilation)
var storageFullName = concat(storageAccountName, uniqueString(resourceGroup().id))
// After: Bicep interpolation (more readable)
var storageFullName = '${storageAccountName}${uniqueString(resourceGroup().id)}'
3.4 Resource Modifications: Properties and Hierarchyβ
Add properties to an existing resourceβ
// Before: basic storage account
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: storageSku
}
kind: 'StorageV2'
properties: {}
}
// After: with added security properties
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: storageAccountName
location: location
sku: {
name: storageSku
}
kind: 'StorageV2'
properties: {
minimumTlsVersion: 'TLS1_2'
allowSharedKeyAccess: false
supportsHttpsTrafficOnly: true
networkAcls: {
defaultAction: 'Deny'
bypass: 'AzureServices'
}
}
tags: tags // adding tags to resource
}
Add child resource with parentβ
The parent is the preferred way in Bicep to express hierarchy. It's safer than manually constructing the name because Bicep checks that the parent resource exists:
// Add blob service to existing storage account in template
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount // symbolic reference to parent resource
name: 'default'
properties: {
deleteRetentionPolicy: {
enabled: true
days: 7
}
containerDeleteRetentionPolicy: {
enabled: true
days: 7
}
}
}
// Add container within blob service
param containerName string = 'data'
resource blobContainer 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: blobService // chains with blob service above
name: containerName
properties: {
publicAccess: 'None'
}
}
Add resource lock to existing resourceβ
// Add lock to critical resource
resource storageLock 'Microsoft.Authorization/locks@2020-05-01' = {
name: '${storageAccount.name}-lock'
scope: storageAccount // scope points to resource to be protected
properties: {
level: 'CanNotDelete'
notes: 'Production storage protected against accidental deletion'
}
}
3.5 Conditional Modifications with ifβ
Add a resource that should only exist in certain scenarios:
// Add control parameter
param enableDiagnostics bool = false
param logAnalyticsWorkspaceId string = ''
// Conditional resource: only created if enableDiagnostics is true
resource diagnosticSettings 'Microsoft.Insights/diagnosticSettings@2021-05-01-preview' = if (enableDiagnostics) {
name: 'diag-${storageAccountName}'
scope: storageAccount
properties: {
workspaceId: logAnalyticsWorkspaceId
logs: []
metrics: [
{
category: 'Transaction'
enabled: true
}
]
}
}
3.6 Loops: for for Multiple Instancesβ
Replace multiple identical resources with a loop:
// Before: three subnets declared individually
resource subnet1 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
parent: vnet
name: 'subnet-frontend'
properties: { addressPrefix: '10.0.1.0/24' }
}
resource subnet2 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
parent: vnet
name: 'subnet-backend'
properties: { addressPrefix: '10.0.2.0/24' }
}
// After: using for loop
param subnets array = [
{ name: 'subnet-frontend', prefix: '10.0.1.0/24' }
{ name: 'subnet-backend', prefix: '10.0.2.0/24' }
{ name: 'subnet-database', prefix: '10.0.3.0/24' }
]
resource subnetResources 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = [for subnet in subnets: {
parent: vnet
name: subnet.name
properties: {
addressPrefix: subnet.prefix
}
}]
To output all resources created by the loop:
output subnetIds array = [for (subnet, i) in subnets: subnetResources[i].id]
3.7 Update the apiVersionβ
When adding new properties that require a more recent API version:
// Before
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
// After (newer version with support for new properties)
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
To discover available versions:
az provider show \
--namespace Microsoft.Storage \
--query "resourceTypes[?resourceType=='storageAccounts'].apiVersions" \
--output table
3.8 Add and Use Modulesβ
Extract part of the file into a module for reusability:
// Before: everything in main.bicep
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: 'vnet-production'
location: location
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
}
}
// After: extracted to module
// modules/vnet.bicep (separate file)
// main.bicep uses the module:
module vnetModule './modules/vnet.bicep' = {
name: 'vnet-deploy'
params: {
vnetName: 'vnet-production'
addressPrefix: '10.0.0.0/16'
location: location
}
}
// Use module output in another resource
resource subnet 'Microsoft.Network/virtualNetworks/subnets@2023-05-01' = {
name: '${vnetModule.outputs.vnetName}/subnet-app'
...
}
4. Structural Viewβ
5. Practical Operationβ
Compile and Verify Modified Fileβ
After each significant modification, compile to check for errors before attempting to deploy:
# Compile Bicep to ARM JSON (checks syntax errors)
az bicep build --file main.bicep
# With output to specific file (for inspection of generated ARM)
az bicep build --file main.bicep --outfile main-compiled.json
# Check Bicep compiler version
az bicep version
# Update Bicep compiler
az bicep upgrade
Linting with Quality Rulesβ
The Bicep compiler has built-in linting rules that warn about best practices:
# Linting is done automatically on build
# To see all available rules:
az bicep build --file main.bicep --stdout 2>&1 | grep "Warning"
VS Code with the Bicep extension shows linting warnings in real-time with yellow underlines. Common warnings:
use-stable-vm-image: VM image may not be available in all regionsno-unused-params: parameter declared but never used in templateno-unused-vars: variable declared but never referencedprefer-interpolation: use'${var}'instead ofconcat(var, '')
Important and Non-Obvious Behaviorsβ
Symbolic names vs. resource names: in Bicep, the symbolic name (like storageAccount or vnet) is used for references within the file. The Azure resource name (like myaccount) is the value of the name property. These are different things.
// 'storageAccount' is the symbolic name (used in Bicep)
// 'myaccount' is the actual Azure resource name
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'myaccount' // Azure name
...
}
// Reference by symbolic name (not by Azure name)
output storageId string = storageAccount.id // correct
// output storageId string = 'myaccount'.id // error: this doesn't work
dependsOn in Bicep is rarely necessary: when you reference a resource by symbolic name, Bicep automatically infers the dependency. Use dependsOn only when the dependency exists but cannot be expressed via direct reference.
// Implicit dependency: blobService depends on storageAccount
// because it uses 'parent: storageAccount'
resource blobService 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: storageAccount // creates dependency automatically
name: 'default'
properties: {}
}
// explicit dependsOn (rarely necessary)
resource somethingThatDepends 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'my-identity'
location: location
dependsOn: [
storageAccount // necessary only if there's no direct reference
]
}
6. Implementation Methodsβ
6.1 VS Code with Bicep Extensionβ
When to use: all development and modification of Bicep files.
The Bicep extension for VS Code offers:
- IntelliSense: autocomplete for resource types, properties, and values
- Real-time validation: syntax errors and linting warnings without needing to compile
- Hover documentation: hovering over a property shows documentation
- Code actions: automatic fix suggestions for detected problems
- Visualizer:
Ctrl+Shift+P> "Bicep: Open Bicep Visualizer" shows a graph of resources and dependencies
To install:
# Via VS Code extensions
code --install-extension ms-azuretools.vscode-bicep
6.2 Azure CLI for Validation and Deploymentβ
# After modifying the Bicep file:
# 1. Compile to check syntax errors
az bicep build --file main.bicep
# 2. Validate against Azure (checks if deployment would be accepted)
az deployment group validate \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.prod.json
# 3. What-If to see impact
az deployment group what-if \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.prod.json
# 4. Deploy with interactive confirmation
az deployment group create \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.prod.json \
--confirm-with-what-if
6.3 Bicep Playground (Online)β
For exploratory modifications or learning: https://aka.ms/bicepdemo
The Bicep Playground allows editing Bicep and viewing the compiled ARM JSON side by side in real-time, without needing to install anything locally.
7. Control and Securityβ
Check Generated ARM JSONβ
After modifying a Bicep, it's good practice to check the compiled ARM JSON to confirm the modification produced exactly what you expected:
az bicep build --file main.bicep --outfile main-check.json
Compare the JSON before and after modification. Differences in the compiled JSON reflect exactly what will change in deployment.
Secure Parameters: Never in Plain Textβ
When modifying a Bicep file to accept passwords or keys:
// INCORRECT: password parameter as common string
param dbPassword string // value will appear in logs!
// CORRECT: use @secure()
@secure()
param dbPassword string // value doesn't appear in logs
// BETTER: Key Vault reference in parameters file
// (without needing to pass password as string anywhere)
The corresponding parameters file with Key Vault reference:
{
"parameters": {
"dbPassword": {
"reference": {
"keyVault": {
"id": "/subscriptions/<sub>/resourceGroups/rg-secrets/providers/Microsoft.KeyVault/vaults/my-kv"
},
```bicep
"secretName": "db-admin-password"
}
}
}
8. Decision Makingβ
When to use parent vs. manually building the name?β
| Situation | Approach | Reason |
|---|---|---|
| Child resource of a resource created in the same file | parent: | Automatic dependency inference, safer |
| Child resource of an existing resource outside the template | Constructed name with 'parent-resource/child' | Parent resource doesn't exist in the template's symbolic scope |
| Multiple child resources in loop | parent: + loop | More concise and with inferred dependency |
When to extract a module?β
| Situation | Decision | Reason |
|---|---|---|
| Pattern repeated in 3+ files | Extract to module | DRY: don't repeat yourself |
| Bicep file with > 200 lines | Consider modules | Maintainability |
| Component with specific version requirement | Module with version | Independent lifecycle control |
| Team sharing components | Module in Bicep registry | Centralized distribution |
When to use for vs. multiple resources?β
| Situation | Approach | Reason |
|---|---|---|
| Resources with identical structure, variable quantity | for loop | Parameterizable, no repetition |
| Two resources with very different configurations | Separate resources | Clarity about each one |
| Resources that need individual reference later | for + index [i] in outputs | Allows referencing each item |
| Fixed quantity known at design time | Separate resources or for | Depends on readability preference |
9. Best Practicesβ
Organize the Bicep file in logical order: parameters first, then variables, then resources in dependency order (parent before child), then outputs. This facilitates top-to-bottom reading.
Use descriptive symbolic names: the symbolic name storageAccount is much better than sa1 or resource1. Anyone reading the file immediately understands what that symbol represents.
Always specify @description(): parameters without description are difficult to understand outside of context. Even if it seems obvious to whoever created it, adding description is an investment in future readability.
Use ternary operator for simple logic, if for conditional resources: to vary a value based on condition, use condition ? value1 : value2. To create or not create a resource, use resource ... = if (condition) { ... }.
Prefer uniqueString(resourceGroup().id) to random suffixes: uniqueString() is deterministic for the same resource group. The same suffix generated each deploy allows ARM to update the existing resource instead of creating a new one.
10. Common Errorsβ
Modifying symbolic name and breaking all references
The resource was called storageAcct and was renamed to storageAccount for better readability. But there are other references in the file like storageAcct.id, parent: storageAcct, outputs using storageAcct. All of these need to be updated. The compiler detects these errors, but it's important to do the rename consistently. VS Code with the Bicep extension has rename symbol (F2) that automatically updates all references.
Adding property incompatible with current apiVersion
A new security property is added to the resource: allowBlobPublicAccess: false. The deploy fails with error "Unknown property 'allowBlobPublicAccess'". The cause is that the current apiVersion of the resource is 2019-04-01, which doesn't support this property. The solution is to update the apiVersion to a more recent version that supports the property.
Using concat() instead of interpolation
Bicep has the linter configured to warn about this: prefer-interpolation. Although concat(storageAccountName, uniqueString(resourceGroup().id)) works, '${storageAccountName}${uniqueString(resourceGroup().id)}' is the idiomatic Bicep way and should be preferred.
Referencing module output before the module is "created"
In Bicep, a module is only available for reference after its declaration. If you try to use moduleA.outputs.id before declaring module moduleA, Bicep returns an invalid reference error. The order of declarations in the Bicep file matters for readability, even though ARM processes in parallel.
Forgetting to update parameter files when adding new required parameter
A new parameter containerName is added to the Bicep file without defaultValue. The parameters.prod.json file is not updated. The deploy fails with missing required parameter error. Always check if all parameter files for all environments need to be updated when adding parameters without defaultValue.
11. Operation and Maintenanceβ
Verify Template Consistency with Real Stateβ
# What-If in Complete mode shows differences between template and current state
az deployment group what-if \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.prod.json \
--mode Complete
If what-if shows resources to be created that should already exist, or resources to be deleted that aren't in the template, there's drift: the real state has diverged from the template.
Check Linting Warnings in Pipelineβ
Configure the pipeline to fail if there are linting warnings:
# In a pipeline, treat warnings as errors
az bicep build --file main.bicep 2>&1 | \
grep -i "warning" && \
echo "Linting warnings found" && exit 1 || \
echo "No linting warnings"
Bicep Limitsβ
| Item | Limit |
|---|---|
| Maximum Bicep file size | 4 MB |
| Nested modules (depth) | 5 levels |
| Parameters per template | 256 |
| Variables per template | 256 |
| Outputs per template | 64 |
| Resources per template | 800 |
12. Integration and Automationβ
Bicep Registry: Shared Modulesβ
For organizations with multiple teams using Bicep, the Bicep Registry (based on Azure Container Registry) allows publishing versioned modules:
// Reference module from a private registry
module vnet 'br:myacr.azurecr.io/bicep/vnet:v1.2' = {
name: 'vnet-deploy'
params: {
vnetName: 'vnet-production'
location: location
}
}
Publish a module to the registry:
az bicep publish \
--file modules/vnet.bicep \
--target br:myacr.azurecr.io/bicep/vnet:v1.2
CI Pipeline with Bicep Validationβ
# .github/workflows/bicep-validate.yml
name: Validate Bicep Changes
on:
pull_request:
paths: ['**/*.bicep', '**/parameters*.json']
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Login
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Install Bicep
run: az bicep install
- name: Build Bicep (Lint + Compile)
run: |
for f in $(find . -name "*.bicep"); do
echo "Building $f"
az bicep build --file "$f"
done
- name: Validate against test environment
run: |
az deployment group validate \
--resource-group rg-bicep-validation \
--template-file main.bicep \
--parameters @parameters.dev.json
- name: What-If Report
run: |
az deployment group what-if \
--resource-group rg-production \
--template-file main.bicep \
--parameters @parameters.prod.json \
--output table
13. Final Summaryβ
Essential points:
- Modifying a Bicep file is closer to programming than editing configuration. The compiler detects errors before deploy, and VS Code with the Bicep extension provides real-time feedback.
- Main types of modification: add/change parameters with decorators, add/modify variables with expressions, add/change resources and their properties, add conditional and looped resources, add outputs and modules.
parent:is the preferred way to express resource hierarchy in Bicep, automatically creating implicit dependencies.
Critical differences:
- Symbolic name (e.g.,
storageAccount) vs. Azure resource name (e.g.,'myaccount'): the symbolic is used within Bicep for references; the Azure name is what appears in the portal. Changing one doesn't change the other. @secure()on parameter vs. parameter without decorator: with@secure(), the value doesn't appear in ARM logs; without it, the value is exposed. Any password or API key must have@secure().- Implicit
parent:vs. explicitdependsOn::parent:creates the dependency automatically;dependsOn:is only necessary when the dependency exists but can't be expressed via direct reference. if (condition)on resource vs. ternary? :: theifdecides whether the resource is created or not; the ternary selects between two values for a property.
What needs to be remembered:
az bicep build --file main.bicepdetects syntax errors and linting warnings before any deploy.az deployment group validatechecks if the template would be accepted by Azure, in addition to checking syntax.az deployment group what-ifshows the real impact on existing resources before applying.- When adding new parameter without
defaultValue, all parameter files for all environments need to be updated. - VS Code with the Bicep extension offers
F2(rename symbol) to rename symbolic names and automatically update all references. - Use
uniqueString(resourceGroup().id)for unique but deterministic name suffixes, not timestamps or random GUIDs.