Overview
Bicep is Azure's domain-specific language for declarative infrastructure as code. Instead of writing imperative scripts that create resources step by step, a Bicep file describes the desired Azure resources, properties, relationships, parameters, and outputs. Azure Resource Manager then orchestrates deployment.
Bicep matters because production Azure environments need repeatable infrastructure, reviewable changes, environment consistency, secure parameter handling, and predictable deployments. It is commonly used for resource groups, App Service, Azure SQL, Key Vault, managed identities, role assignments, networking, monitoring, and platform configuration.
For interviews, candidates should be able to explain idempotency, scopes, parameters, variables, resources, outputs, modules, .bicepparam files, secrets, what-if, complete versus incremental thinking, module registries, environment-specific configuration, and how Bicep fits into CI/CD.
Core Concepts
Declarative Infrastructure
Bicep is declarative. You describe what Azure should look like, not the exact sequence of commands to get there.
param location string = resourceGroup().location
param storageAccountName string
resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
The deployment engine determines ordering from resource references and dependencies.
Idempotency
Bicep deployments are intended to be repeatable. If a resource already exists with the desired configuration, redeploying should not recreate it unnecessarily.
This enables:
- CI/CD-driven infrastructure changes.
- Recovery from failed deployments.
- Drift correction.
- Environment rebuilds.
- Pull-request review of infrastructure changes.
Idempotency does not mean every change is safe. Changing a SKU, name, region, network rule, or data setting can still have operational consequences.
Bicep and ARM Templates
Bicep is a more readable abstraction over Azure Resource Manager templates. Bicep compiles to ARM JSON templates during deployment.
Important implications:
- Bicep supports Azure resource types and API versions supported by ARM.
- ARM deployment behavior still applies.
- Bicep does not maintain a separate state file.
- Azure stores deployment state and resource state.
- You can use what-if to preview changes.
Deployment Scopes
Bicep can deploy to different scopes:
- Resource group.
- Subscription.
- Management group.
- Tenant.
Choose scope based on what the template creates. A resource group-scoped template can deploy App Service and storage accounts. A subscription-scoped template can create resource groups and assign policies. Management group and tenant scopes are used for governance.
Parameters
Parameters are values supplied to a Bicep file at deployment time. They make templates reusable across environments.
@allowed([
'dev'
'test'
'prod'
])
param environmentName string
@minValue(1)
@maxValue(10)
param appInstanceCount int = 1
Use decorators such as @allowed, @minValue, @maxValue, @description, and @secure to make contracts clearer and safer.
Variables
Variables compute values inside a template. They reduce repetition and make naming or tagging rules consistent.
var namePrefix = 'contoso-${environmentName}'
var commonTags = {
environment: environmentName
workload: 'orders'
}
Use variables for derived values, not for values that should be supplied by the environment or pipeline.
Resources
Resources declare Azure objects.
resource appPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: '${namePrefix}-plan'
location: location
sku: {
name: environmentName == 'prod' ? 'P1v3' : 'B1'
capacity: environmentName == 'prod' ? 2 : 1
}
}
Good resource declarations use stable names, explicit API versions, consistent tags, and environment-aware settings.
Outputs
Outputs expose values from a deployment for later pipeline steps or dependent templates.
output appServiceName string = app.name
output appUrl string = 'https://${app.properties.defaultHostName}'
Avoid outputting secrets. Outputs can be visible in deployment history.
Existing Resources
Use existing when a template needs to reference a resource it does not create.
resource keyVault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
name: keyVaultName
}
This is common for shared Key Vaults, existing virtual networks, shared log workspaces, or centralized identities.
Dependencies
Bicep infers dependencies when one resource references another by symbolic name.
resource site 'Microsoft.Web/sites@2023-12-01' = {
name: appName
location: location
properties: {
serverFarmId: appPlan.id
}
}
Avoid manual dependsOn unless inference is not possible. Excessive explicit dependencies can serialize deployments and hide the actual resource relationship.
Modules
A module is a Bicep file deployed by another Bicep file. Modules encapsulate related resources and support reuse.
module webApp './modules/web-app.bicep' = {
params: {
name: appName
location: location
appServicePlanId: appPlan.id
tags: commonTags
}
}
Use modules for stable boundaries such as:
- Web app plus settings.
- Storage account plus containers.
- Key Vault plus diagnostics.
- SQL server plus database.
- Monitoring resources.
Do not create tiny modules for every single property. Module boundaries should match ownership and reuse.
Module Outputs
Modules can expose outputs to the parent template.
module app './modules/web-app.bicep' = {
params: {
name: appName
location: location
}
}
output appHostname string = app.outputs.defaultHostName
Use outputs to connect modules without duplicating naming logic.
Module Registry and Template Specs
Shared modules can be published to a private registry or packaged as template specs. This helps organizations standardize patterns such as secure storage, diagnostics, networking, and identity.
Use shared modules when:
- The pattern is used by multiple teams.
- Security and compliance settings must be consistent.
- Versioning and review are required.
- A platform team owns the baseline.
Avoid central modules that become so generic they are impossible to understand.
Parameter Files
.bicepparam files separate environment values from the main template.
using './main.bicep'
param environmentName = 'dev'
param location = 'eastus'
param appSku = 'B1'
param minInstanceCount = 1
Use separate parameter files such as:
main.dev.bicepparam
main.test.bicepparam
main.prod.bicepparam
This keeps the infrastructure definition consistent while allowing environment-specific capacity, region, naming, and feature choices.
Reusable Environment Definitions
A reusable environment definition usually combines:
- A shared
main.bicep. - Modules for resource groups, app hosting, data, identity, networking, and monitoring.
- Environment-specific
.bicepparamfiles. - Tags for ownership and cost attribution.
- Naming conventions.
- Secure parameter handling.
- Pipeline stages that pass the correct parameters.
Example layout:
infra/
main.bicep
main.dev.bicepparam
main.test.bicepparam
main.prod.bicepparam
modules/
app-service.bicep
key-vault.bicep
monitoring.bicep
sql-database.bicep
This is easier to review than maintaining unrelated templates for every environment.
Secrets
Do not store passwords, connection strings, or certificates in parameter files. Parameter files are source-controlled plain text unless you build a separate secure process.
Use:
- Key Vault references.
- Managed identities.
@secure()parameters only when the secret must be passed.- Pipeline secret variables only when unavoidable.
- No secret outputs.
Prefer identity and Key Vault over injecting secrets into templates.
What-If
What-if previews planned resource changes before deployment. It helps reviewers understand whether a template will create, modify, or delete resources.
Typical pipeline flow:
bicep build
bicep lint
az deployment group what-if
approval
az deployment group create
What-if is not a substitute for review or testing, but it is a strong guardrail.
Validation and Linting
Use Bicep build and linting in CI:
az bicep build --file infra/main.bicep
az bicep lint --file infra/main.bicep
Validation catches syntax, type, and rule issues before deployment. Organization-specific policies should also be enforced with Azure Policy, pipeline checks, and pull-request review.
Incremental and Complete Thinking
Most deployments use incremental behavior: resources declared in the template are created or updated, and unrelated resources are left alone.
Complete-mode style thinking is dangerous unless the template truly owns everything in scope. Accidentally removing resources can cause outages.
For environment definitions, be explicit about ownership:
- Which resources are managed by this template?
- Which resources are referenced as existing?
- Which resources are intentionally outside the deployment?
Naming and Tags
Names and tags are not decoration. They support operations, cost attribution, automation, and security.
Common tags:
environment
workload
owner
costCenter
dataClassification
managedBy
Names should be deterministic but not leak sensitive business details.
Common Mistakes
- Copying portal-generated templates without simplifying them.
- Hardcoding production values in
main.bicep. - Storing secrets in parameter files.
- Creating modules with unclear ownership.
- Overusing explicit
dependsOn. - Ignoring what-if output.
- Not pinning meaningful API versions.
- Mixing manually managed and IaC-managed settings without ownership rules.
- Using one giant template with no modular boundaries.
- Making dev and prod templates drift.
Best Practices
- Keep the desired infrastructure in source control.
- Use modules for real reuse and ownership boundaries.
- Use
.bicepparamfiles for environment differences. - Use what-if before production deployments.
- Use linting and pull requests.
- Keep secrets in Key Vault or identity-based access.
- Tag resources consistently.
- Prefer managed identities over connection strings.
- Document module contracts with parameters and outputs.
- Review cost and reliability differences between environments intentionally.