Bicep Best Practices

Best practices in Bicep are patterns, conventions, and habits that make templates easier to read, safer to deploy, simpler to maintain, and consistent across teams. A template that works is not necessarily a template that is well-written. Best practices bridge that gap — they represent lessons learned from real deployments, team collaboration, and infrastructure at scale.

Think of best practices like building codes for a house. A house can stand without following them, but following them ensures safety, consistency, and that the next person who works on the structure understands what was built and why.

1. File and Project Organization

A well-organized project structure makes it easy to find files, understand scope, and onboard new team members. Use a consistent layout across all Bicep projects.

Recommended Project Structure

project/
│
├── main.bicep                  ← Entry point; orchestrates all modules
├── main.bicepparam             ← Default parameter file
│
├── modules/                    ← Reusable, single-purpose modules
│   ├── storage.bicep
│   ├── appservice.bicep
│   ├── keyvault.bicep
│   └── networking.bicep
│
├── environments/               ← Environment-specific parameter files
│   ├── dev.bicepparam
│   ├── staging.bicepparam
│   └── prod.bicepparam
│
└── .github/
    └── workflows/
        └── deploy.yml          ← CI/CD pipeline definition

File Organization Rules

  • One resource type per module – A storage module deploys only storage accounts. A networking module deploys only virtual networks and subnets.
  • main.bicep orchestrates, modules do work – The main file calls modules and wires outputs to inputs. It does not define individual resources directly.
  • Parameter files per environment – Never hard-code environment-specific values inside templates. Use .bicepparam files for dev, staging, and prod differences.
  • Keep module files short – If a module is longer than 150 lines, consider splitting it further.

2. Naming Conventions

Consistent naming makes templates predictable. Someone reading the template for the first time should immediately understand what each parameter, variable, and resource does.

Parameter Naming

  • Use camelCase for all parameter names.
  • Name parameters after what they represent, not their type.
  • Prefix boolean parameters with enable or is.
// Good – clear, camelCase, descriptive
param storageAccountName string
param appServicePlanSku string
param enableDiagnostics bool
param isProduction bool

// Bad – unclear, inconsistent casing
param SA_Name string
param sku string
param diag bool

Variable Naming

  • Use camelCase for variables.
  • Prefix computed names with the resource type abbreviation when constructing resource names dynamically.
// Good
var storageAccountName = 'st${appName}${environment}'
var appServicePlanName = 'asp-${appName}-${environment}'
var keyVaultName = 'kv-${appName}-${environment}'

// Bad – no context about what the variable represents
var name1 = 'storage${appName}'
var thing = 'asp-${appName}'

Resource Symbolic Names

  • Use camelCase for symbolic names.
  • Name the symbol after the resource it represents.
  • Avoid generic names like res or r1.
// Good
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { ... }
resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = { ... }
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = { ... }

// Bad
resource r1 'Microsoft.Storage/storageAccounts@2023-01-01' = { ... }
resource myresource 'Microsoft.Web/serverfarms@2023-01-01' = { ... }

Azure Resource Name Conventions

Resource TypeRecommended PrefixExample
Resource Grouprg-rg-myapp-prod
Storage Accountststmyappprod
App Service Planasp-asp-myapp-prod
App Service / Web Appapp-app-myapp-prod
Key Vaultkv-kv-myapp-prod
Virtual Networkvnet-vnet-myapp-prod
Subnetsnet-snet-frontend-prod
Network Security Groupnsg-nsg-frontend-prod
SQL Serversql-sql-myapp-prod
SQL Databasesqldb-sqldb-myapp-prod

3. Parameters — Best Practices

Parameters are the interface between the template and the outside world. Poorly designed parameters lead to invalid deployments, confusion, and security issues.

Always Add Descriptions

Every parameter should have a @description decorator explaining its purpose. This documentation appears in the Azure Portal and in IDE tooltips.

// Good – documented parameters
@description('The name of the storage account. Must be globally unique, 3–24 lowercase alphanumeric characters.')
param storageAccountName string

@description('The Azure region where all resources will be deployed.')
param location string = resourceGroup().location

@description('The pricing tier for the App Service Plan.')
@allowed(['F1', 'B1', 'B2', 'S1', 'P1v3'])
param appServicePlanSku string = 'B1'

// Bad – no documentation
param storageAccountName string
param location string = resourceGroup().location
param appServicePlanSku string = 'B1'

Constrain Parameters with Decorators

Use decorators to enforce valid values at validation time, before deployment begins.

@description('Storage account name must be 3–24 lowercase alphanumeric characters.')
@minLength(3)
@maxLength(24)
param storageAccountName string

@description('Number of App Service instances.')
@minValue(1)
@maxValue(10)
param instanceCount int = 1

@description('Allowed environments for this deployment.')
@allowed(['dev', 'staging', 'prod'])
param environment string

Use Default Values for Optional Parameters

Provide sensible defaults so callers only need to supply values that differ from the norm.

param location string = resourceGroup().location
param skuName string = 'Standard_LRS'
param enableHttpsOnly bool = true

Never Pass Secrets as Plain Parameters

Secrets passed as string parameters appear in deployment history in plain text. Always use Key Vault references for sensitive values.

// Bad – secret in plain text
param dbPassword string

// Good – reference Key Vault at deployment time
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: keyVaultName
  scope: resourceGroup(keyVaultResourceGroup)
}

// Use the secret securely
resource sqlServer 'Microsoft.Sql/servers@2023-02-01-preview' = {
  name: sqlServerName
  properties: {
    administratorLoginPassword: keyVault.getSecret('dbPassword')
  }
}

4. Use Variables to Eliminate Repetition

If a value is used more than once, store it in a variable. Variables make the template easier to update and reduce the chance of inconsistency.

// Good – single definition, used everywhere
var resourcePrefix = '${appName}-${environment}'
var storageAccountName = 'st${appName}${environment}'
var appServicePlanName = 'asp-${resourcePrefix}'
var appServiceName = 'app-${resourcePrefix}'
var keyVaultName = 'kv-${resourcePrefix}'
var tags = {
  application: appName
  environment: environment
  managedBy: 'bicep'
}

// Bad – repeated inline expressions
resource storageAccount '...' = {
  name: 'st${appName}${environment}'
  tags: {
    application: appName
    environment: environment
    managedBy: 'bicep'
  }
}

resource appServicePlan '...' = {
  name: 'asp-${appName}-${environment}'
  tags: {
    application: appName
    environment: environment
    managedBy: 'bicep'
  }
}

5. Tagging Strategy

Tags enable cost management, resource filtering, governance policies, and automation. Apply tags consistently across all resources using a shared variable.

// Define once as a variable
var commonTags = {
  application: appName
  environment: environment
  owner: ownerEmail
  costCenter: costCenterCode
  managedBy: 'bicep'
  deployedAt: utcNow()
}

// Apply to every resource
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  tags: commonTags
  sku: { name: skuName }
  kind: 'StorageV2'
  properties: { ... }
}

resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: appServicePlanName
  location: location
  tags: commonTags
  sku: { name: appServicePlanSku }
}

Recommended Tags

Tag KeyExample ValuePurpose
applicationmyappIdentifies which app owns the resource
environmentprodSeparates dev / staging / prod costs
ownerteam@company.comAccountability and contact
costCenterCC-1023Finance allocation
managedBybicepShows resource is IaC-managed

6. Use Modules for Reusability

Modules encapsulate a set of resources behind a clean interface of parameters and outputs. They enable reuse across projects and prevent copy-paste drift between environments.

What Makes a Good Module

  • Single responsibility – One module deploys one logical unit (e.g., a storage account or a virtual network).
  • Well-defined parameters – All inputs are documented with @description.
  • Useful outputs – Modules export resource IDs, names, and connection strings needed by other modules.
  • No hard-coded values – Every environment-specific or project-specific value is a parameter.
// modules/storage.bicep

@description('Name of the storage account.')
@minLength(3)
@maxLength(24)
param storageAccountName string

@description('Azure region for the storage account.')
param location string

@description('Storage SKU.')
@allowed(['Standard_LRS', 'Standard_GRS', 'Premium_LRS'])
param skuName string = 'Standard_LRS'

@description('Tags to apply to the storage account.')
param tags object = {}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  tags: tags
  sku: { name: skuName }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
    allowBlobPublicAccess: false
  }
}

output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name
output primaryEndpoints object = storageAccount.properties.primaryEndpoints
// main.bicep – consuming the module

module storageModule './modules/storage.bicep' = {
  name: 'storage-deployment'
  params: {
    storageAccountName: storageAccountName
    location: location
    skuName: storageSku
    tags: commonTags
  }
}

// Use the output from the storage module
output storageEndpoint string = storageModule.outputs.primaryEndpoints.blob

7. Always Pin API Versions

The API version controls the schema of the resource. Unpinned or automatically updated API versions can cause unexpected changes in resource behavior when the API evolves.

// Good – explicit, pinned API version
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = { ... }
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' = { ... }

// Bad – never use 'latest' or omit the version
// Bicep requires an API version, but never rely on automatically-updated references

Rule: When updating an API version, review the changelog for the resource type to understand what properties changed. Update one resource type at a time and test in a lower environment first.

8. Prefer Resource References Over Hard-Coded IDs

When one resource depends on another, reference it symbolically rather than hard-coding its resource ID. Symbolic references create implicit dependencies, let the Bicep compiler optimize the deployment graph, and make the template self-consistent.

// Good – symbolic reference creates implicit dependency
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
  properties: {}
}

resource appService 'Microsoft.Web/sites@2023-01-01' = {
  name: appServiceName
  location: location
  properties: {
    serverFarmId: appServicePlan.id  // symbolic reference
    siteConfig: {
      appSettings: [
        {
          name: 'StorageAccountName'
          value: storageAccount.name  // symbolic reference
        }
      ]
    }
  }
}

// Bad – hard-coded resource ID
resource appService 'Microsoft.Web/sites@2023-01-01' = {
  properties: {
    serverFarmId: '/subscriptions/abc123/resourceGroups/rg-prod/providers/Microsoft.Web/serverfarms/asp-myapp'
  }
}

9. Use existing for Resources Deployed Outside the Template

When a resource already exists in Azure and the template needs to reference it (without managing it), use the existing keyword instead of hard-coding its properties.

// Reference an existing Key Vault in a different resource group
resource keyVault 'Microsoft.KeyVault/vaults@2023-02-01' existing = {
  name: keyVaultName
  scope: resourceGroup(keyVaultResourceGroupName)
}

// Use the existing resource's properties
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  properties: {
    // reference the existing Key Vault's URI
    encryption: {
      keySource: 'Microsoft.Keyvault'
      keyvaultproperties: {
        keyVaultUri: keyVault.properties.vaultUri
        keyName: encryptionKeyName
      }
    }
  }
}

10. Secure Outputs — Never Output Secrets

Outputs are stored in deployment history and visible to anyone with read access to the resource group. Never output secrets, passwords, connection strings with credentials, or storage account keys.

// Bad – outputs a secret value
output storageConnectionString string = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};AccountKey=${storageAccount.listKeys().keys[0].value}'

// Good – output the resource ID and name; let the application retrieve the key at runtime
output storageAccountId string = storageAccount.id
output storageAccountName string = storageAccount.name

// Good – output a Key Vault secret URI reference, not the secret value
output dbPasswordSecretUri string = '${keyVault.properties.vaultUri}secrets/dbPassword'

If an output must contain sensitive data (for passing between templates in the same pipeline), mark it with @secure():

@secure()
output adminPassword string = someSecureValue

11. Use the @secure() Decorator for Sensitive Parameters

Mark any parameter that receives a password, secret, or key with @secure(). This prevents the value from appearing in logs, deployment history, and outputs.

@secure()
@description('Administrator password for the SQL Server.')
param sqlAdminPassword string

@secure()
@description('API key for the external service.')
param externalApiKey string

12. Use targetScope Intentionally

Set the deployment scope explicitly at the top of every template. If deploying to a resource group (the most common case), the default applies, but making it explicit removes ambiguity.

// Resource group deployment (most common)
targetScope = 'resourceGroup'

// Subscription-level deployment (for policy, RBAC, resource groups)
targetScope = 'subscription'

// Management group deployment (for policy across subscriptions)
targetScope = 'managementGroup'

// Tenant-level deployment (rare)
targetScope = 'tenant'

13. Always Run Validation and What-If Before Deploying

Never deploy a Bicep template directly to production without first running validation and what-if. These two steps catch errors before they cause real changes.

# Step 1 – Validate the template (syntax and parameter checks)
az deployment group validate \
  --resource-group rg-myapp-prod \
  --template-file main.bicep \
  --parameters prod.bicepparam

# Step 2 – Preview changes (what-if shows what will be created, modified, or deleted)
az deployment group create \
  --resource-group rg-myapp-prod \
  --template-file main.bicep \
  --parameters prod.bicepparam \
  --what-if \
  --what-if-result-format FullResourcePayloads

# Step 3 – Deploy only after reviewing what-if output
az deployment group create \
  --resource-group rg-myapp-prod \
  --template-file main.bicep \
  --parameters prod.bicepparam \
  --name "deploy-$(date +%Y%m%d-%H%M%S)"

14. Name Every Deployment

Use the --name flag when running deployments. Named deployments are easy to find in Azure deployment history and can be correlated with Git commits and pipeline run numbers.

# Include the date and pipeline run number
az deployment group create \
  --resource-group rg-myapp-prod \
  --template-file main.bicep \
  --parameters prod.bicepparam \
  --name "deploy-20250429-run-142"

15. Use dependsOn Only When Necessary

Bicep infers dependencies automatically from symbolic resource references. Explicit dependsOn should only be used when a dependency exists that cannot be expressed through a property reference — for example, a role assignment that must exist before a resource tries to access a Key Vault.

// Explicit dependsOn needed – role assignment has no property linking it to the storage account deployment
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(storageAccount.id, appIdentityPrincipalId, storageBlobDataReaderRoleId)
  scope: storageAccount
  properties: {
    roleDefinitionId: storageBlobDataReaderRoleId
    principalId: appIdentityPrincipalId
  }
}

resource appService 'Microsoft.Web/sites@2023-01-01' = {
  name: appServiceName
  dependsOn: [roleAssignment]   // App Service must wait for RBAC assignment
  properties: { ... }
}

16. Lint and Format Templates

The Bicep linter catches common mistakes, anti-patterns, and style violations. Run it as part of every CI/CD pipeline and during local development.

# Run the linter manually
az bicep lint --file main.bicep

# Build (compile) to catch errors
az bicep build --file main.bicep

The Bicep extension for VS Code runs the linter in real time and underlines problems as you type. Configure the linter rules in bicepconfig.json at the project root:

// bicepconfig.json
{
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "no-unused-params": { "level": "warning" },
        "no-unused-vars": { "level": "warning" },
        "prefer-interpolation": { "level": "warning" },
        "secure-parameter-default": { "level": "error" },
        "no-hardcoded-env-urls": { "level": "warning" },
        "explicit-values-for-loc-params": { "level": "warning" }
      }
    }
  }
}

17. Version Control Everything

Every Bicep file, parameter file, pipeline YAML, and bicepconfig.json should live in a Git repository. Infrastructure changes should follow the same pull request, review, and merge process as application code.

ItemIn Git?Notes
*.bicep filesYesAll templates and modules
*.bicepparam filesYesAll environment parameter files
bicepconfig.jsonYesLinter configuration
Pipeline YAMLYesCI/CD workflow definitions
Secrets / passwordsNeverUse Key Vault references instead
*.bicep compiled ARM JSONOptionalUsually excluded via .gitignore

Summary

PracticeWhy It Matters
Use a consistent project structureEasy navigation, onboarding, and maintenance
Follow naming conventionsTemplates become self-documenting and predictable
Add @description to all parametersDocumentation in the Portal and IDE tooltips
Constrain parameters with decoratorsCatches invalid values before deployment starts
Never pass secrets as plain parametersPrevents exposure in deployment history
Use variables to eliminate repetitionSingle point of change, no inconsistency
Apply consistent tags to all resourcesCost management, governance, and filtering
Break templates into focused modulesReusability and single responsibility
Pin API versions explicitlyPrevents unexpected behavior from API changes
Use symbolic references over hard-coded IDsImplicit dependencies and portability
Use existing for pre-existing resourcesClean reference without managing the resource
Never output secretsDeployment history is readable by many
Mark sensitive parameters with @secure()Keeps secrets out of logs and history
Run validate and what-if before deployingCatches errors and previews changes safely
Name every deploymentTraceable deployment history in Azure
Lint templates in CI/CD and locallyCatches anti-patterns early
Version control all Bicep and param filesAuditability, collaboration, and rollback

Leave a Comment