Bicep coding standards, patterns, and governance for Azure deployments across Learning, Project, and Client workspaces. USE FOR: Bicep Azure code generation, module scaffolding, deployment stacks, parameter files, tagging, naming conventions, validation, security, Bicep style, Bicep best practices, ARM template alternatives, subscription-scoped deployments. DO NOT USE FOR: Terraform deployments (use terraform-azure), non-Azure IaC, ARM JSON templates.
Provide authoritative Bicep coding standards, patterns, and governance for all Azure deployments. This skill consolidates guidance from workspace-level governance documents, shared-contract rules, and real-world patterns observed across the Learning, Project, and Client workspaces.
This skill synthesizes guidance from:
instructions/Azure Governance Guidelines.instructions.md — cross-workspace naming, tagging, cost, securityLearningAzure/.github/skills/bicep-scaffolding/SKILL.md — lab-specific scaffolding rules (R-130 through R-138)LearningAzure/.github/skills/shared-contract/SKILL.md — cross-cutting lab rules (R-001 through R-030)LearningAzure/Governance-Lab.md — Learning workspace governanceLearningAzure/.assets/shared/bicep/bicep.ps1 — shared deployment wrapper scriptLearningAzure/AZ-104/hands-on-labs/ — proven patterns in production labsbicep build to validate syntax before deployment.bicepconfig.json for lint/compile configuration.bicep/
├── main.bicep # Root module (subscription or resource group scope)
├── main.bicepparam # Parameter values file
├── bicepconfig.json # Lint and compile configuration
├── bicep.ps1 # Deployment wrapper script (shared, never modify)
└── modules/
└── <module>.bicep # Individual module files
| File | Content |
|---|---|
main.bicep | Root orchestration — target scope, parameters, resource group, module calls |
main.bicepparam | Parameter values using using './main.bicep' syntax |
bicepconfig.json | Bicep linter and compiler settings |
bicep.ps1 | Shared deployment wrapper — copy from .assets/shared/bicep/bicep.ps1, never create custom |
modules/*.bicep | Individual module files — one concern per module |
<EXAM>/hands-on-labs/<domain>/lab-<topic>/
├── README.md
├── bicep/
│ ├── main.bicep
│ ├── main.bicepparam
│ ├── bicepconfig.json
│ ├── bicep.ps1
│ └── modules/
│ └── <module>.bicep
└── validation/
└── <validation-script>.ps1
Use targetScope = 'subscription' for labs and projects that create resource groups:
targetScope = 'subscription'
resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: resourceGroupName
location: location
tags: commonTags
}
module networking 'modules/networking.bicep' = {
name: 'networking-deployment'
scope: rg
params: {
commonTags: commonTags
}
}
Pattern: stack-<domain>-<topic> — no exam code in stack name.
The shared bicep.ps1 supports these actions:
| Command | Purpose |
|---|---|
validate | Syntax and schema validation |
plan | Deployment preview (what-if) |
apply | Create or update deployment stack |
destroy | Delete deployment stack |
show | Show stack details |
list | List all stacks |
output | Show stack outputs |
# Validate
.\bicep.ps1 validate
# Preview
.\bicep.ps1 plan
# Deploy
.\bicep.ps1 apply
# Cleanup
.\bicep.ps1 destroy
The wrapper script auto-derives the stack name from the parameters file and validates subscription context before any deployment action.
Cleanup destroy command:
az stack sub delete --name $stackName --yes --force-deletion-types $forceTypes
| Workspace | Pattern | Example |
|---|---|---|
| Learning | <exam>-<domain>-<topic>-bicep | az104-compute-enable-boot-diagnostics-bicep |
| Project | project-<workspace>-<environment>-<purpose>[-<instance>]-bicep | project-docwriter-dev-data-01-bicep |
| Client | client-<client>-<environment>-<purpose>[-<instance>]-bicep | client-tcu-prod-security-01-bicep |
Pattern: <prefix>-<descriptive-name>[-<instance>]
Use Azure CAF-aligned abbreviations:
| Resource Type | Prefix | Example |
|---|---|---|
| Virtual Network | vnet | vnet-core-01 |
| Subnet | snet | snet-app-01 |
| Network Security Group | nsg | nsg-web-01 |
| Network Interface | nic | nic-vm-web-01 |
| Virtual Machine | vm | vm-web-01 |
| Storage Account | st | staz104bootdiag |
| Key Vault | kv | kv-secrets-01 |
| App Service Plan | asp | asp-web-01 |
| Function App | func | func-processor-01 |
| Log Analytics | log | log-monitor-01 |
| Application Insights | appi | appi-web-01 |
| OpenAI Account | oai | oai-gpt-01 |
| Cognitive Services | cog | cog-lang-7k3m |
| Recovery Services Vault | rsv | rsv-backup-4h8n |
| Bastion Host | bas | bas-hub-01 |
-bicep) is mandatory and always last for resource groups.camelCase for Bicep parameter names and local variables.All resource names must be static and predictable. Random suffixes are only permitted for resources subject to soft-delete name reservation:
| Resource | Retention | Random Suffix Required |
|---|---|---|
| Cognitive Services | 48 hrs | Yes |
| Key Vault | 7–90 days | Yes |
| API Management | 48 hrs | Yes |
| Recovery Vault | 14 days | Yes |
Random suffix format: uniqueString(resourceGroup().id) truncated or scoped.
var suffix = uniqueString(resourceGroup().id)
var cogName = 'cog-${topic}-${suffix}'
var commonTags = {
Environment: environment
Category: category // 'Learning', 'Project', 'Client'
Workspace: workspace
Purpose: purpose
Owner: owner
DateCreated: dateCreated // Static YYYY-MM-DD — never use utcNow()
DeploymentMethod: 'Bicep'
ManagedBy: 'bicep'
}
DateCreated must be a static string. Never use utcNow() or any dynamic function.commonTags.commonTags as an explicit parameter to all modules.var commonTags = {
Environment: 'Lab'
Project: '<EXAM>' // 'AI-102', 'AZ-104'
Domain: '<Domain>' // 'Networking', 'Compute'
Purpose: '<Purpose>' // 'Enable Boot Diagnostics'
Owner: owner
DateCreated: dateCreated
DeploymentMethod: 'Bicep'
}
Every parameter must have:
@description() decorator — clear, concise purpose@allowed() decorator when values are constrained@secure() decorator for sensitive values@description('Azure region for resource deployment')
param location string = 'eastus'
@description('Resource owner name')
param owner string = 'Greg Tate'
@description('Static date for DateCreated tag (YYYY-MM-DD)')
param dateCreated string
@description('Administrator password for virtual machines')
@secure()
param adminPassword string
.bicepparam)using './main.bicep'
param location = 'eastus'
param owner = 'Greg Tate'
param dateCreated = '<YYYY-MM-DD>'
Use variables for computed values and repeated expressions:
var resourceGroupName = 'az104-${domain}-${topic}-bicep'
var commonTags = {
Environment: 'Lab'
Project: 'AZ-104'
Domain: 'Compute'
Purpose: 'Enable Boot Diagnostics'
Owner: owner
DateCreated: dateCreated
DeploymentMethod: 'Bicep'
}
camelCase for parameter names, variable names, and resource symbolic names.@description() on every parameter — not inline comments.Use modules when 2+ related resource types are deployed together.
main.bicepEach module is a .bicep file in modules/:
location, tags (object), and cross-module references as parameters.@description() decorators on all parameters.// modules/networking.bicep
@description('Common tags applied to all networking resources')
param commonTags object
@description('Location for networking resources')
param location string = resourceGroup().location
resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' = {
name: 'vnet-core-01'
location: location
tags: commonTags
properties: {
addressSpace: {
addressPrefixes: ['10.0.0.0/16']
}
}
}
@description('Virtual network resource ID')
output vnetId string = vnet.id
@description('Virtual network name')
output vnetName string = vnet.name
module networking 'modules/networking.bicep' = {
name: 'networking-deployment'
scope: rg
params: {
commonTags: commonTags
}
}
module compute 'modules/compute.bicep' = {
name: 'compute-deployment'
scope: rg
params: {
commonTags: commonTags
subnetId: networking.outputs.subnetId
}
}
principalId) as explicit inputs for RBAC.dependsOn only when implicit dependencies are insufficient.scope: rg).@secure() decorator for sensitive parameters.Use uniqueString(), a static pattern, or supply in .bicepparam. Mark with @secure().
Target pattern for labs: AzureLab2026!
@description('Administrator password')
@secure()
param adminPassword string
resource keyVault 'Microsoft.KeyVault/vaults@2024-04-01-preview' = {
...
properties: {
...
enableSoftDelete: false
}
}
For Cognitive Services:
// softDeleteState is not always available — check API version support
var suffix = uniqueString(resourceGroup().id)
var cogName = 'cog-${topic}-${suffix}'
var kvName = 'kv-${topic}-${suffix}'
The shared bicep.ps1 wrapper validates subscription context automatically before apply, destroy, and plan actions. It checks against the configured lab subscription ID.
# Switch to correct profile
Use-AzProfile Lab
# Validate syntax
.\bicep.ps1 validate
# Capacity tests for constrained services (R-019)
# Preview deployment
.\bicep.ps1 plan
# Deploy
.\bicep.ps1 apply
| Resource Type | Default SKU |
|---|---|
| Virtual Machine | Standard_B2s |
| Storage Account | Standard_LRS |
| Load Balancer | Basic |
| Public IP | Basic |
| SQL Database | Basic / S0 |
| Managed Disk | Standard_HDD |
| Bastion | Developer |
| AI Services | F0 → S0 fallback |
All VMs must include auto-shutdown via Microsoft.DevTestLab/schedules:
| Setting | Default Value |
|---|---|
| Time | 0800 (8:00 AM) |
| Time Zone | Central Standard Time |
DateCreated tag.Bicep automatically infers dependencies when one resource references another. Prefer implicit dependencies.
Use dependsOn only when implicit dependencies are insufficient:
resource bastion 'Microsoft.Network/bastionHosts@2024-10-01' = {
name: bastionName
...
dependsOn: [
networkingModule // Bastion requires VNet in Succeeded state
]
}
Bastion exception: Always declare explicit dependency so Bastion creation waits for all networking resources (race condition with Developer SKU).
Common current API versions:
| Resource Type | Example API Version |
|---|---|
Microsoft.Resources/resourceGroups | 2024-03-01 |
Microsoft.Network/* | 2024-05-01 |
Microsoft.Compute/* | 2024-07-01 |
Microsoft.Storage/* | 2024-01-01 |
Microsoft.KeyVault/vaults | 2024-04-01-preview |
Microsoft.CognitiveServices/* | 2024-10-01 |
Include in all .bicep files using // comment syntax:
// -------------------------------------------------------------------------
// Program: <filename>
// Description: <purpose>
// Context: <workspace context>
// Author: Greg Tate
// Date: <YYYY-MM-DD>
// -------------------------------------------------------------------------
@description() on every output.@description('Resource group name')
output resourceGroupName string = rg.name
@description('Virtual machine name')
output vmName string = compute.outputs.vmName
@description('Storage account name for boot diagnostics')
output storageAccountName string = storage.outputs.storageName
utcNow() for DateCreated tags.bicep.ps1 wrapper scripts — always copy from shared.main.bicep instead of using modules.When rules conflict, apply this precedence (highest wins):
Governance-Lab.md)instructions/Azure Governance Guidelines.instructions.md)instructions/General Coding Guidelines.instructions.md)| Setting | Value |
|---|---|
| Default (Lab) | centralus (supports Bastion Developer) |
| Default (Other) | eastus |
| Fallback | eastus2 → westus2 → northcentralus |
| Allowed | Any US region |
Use default region unless capacity requires fallback. Document the choice if deviating.