Microsoft Azure — ARM Templates & Bicep
ARM templates, Bicep DSL, modules, parameters, resource declarations, and deployment workflows..
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
- ✓Azure subscription, Azure CLI (version 2.40+), Bicep CLI (version 0.20+), basic knowledge of Azure resources (storage, networking, compute), familiarity with JSON/YAML, and a code editor (VS Code with Bicep extension recommended).
Azure is Microsoft's cloud computing platform offering over 200 services. This article covers arm templates & bicep with production-ready configurations, best practices, and hands-on examples.
Why ARM Templates and Bicep Exist
Azure Resource Manager (ARM) templates are the native infrastructure-as-code (IaC) format for Azure. They define resources declaratively in JSON. Bicep is a domain-specific language (DSL) that transpiles to ARM templates, offering cleaner syntax, modularity, and type safety. Both are idempotent: deploying the same template multiple times yields the same state. ARM templates are verbose and error-prone due to JSON's lack of expressions and modularity. Bicep solves this with simpler syntax, file modularity, and built-in linting. In production, you should prefer Bicep for new projects but understand ARM templates for legacy systems and debugging. The Azure platform itself uses ARM under the hood for all resource manager operations.
bicep decompile. Use Bicep for authoring; ARM is the runtime format.dependsOn incorrectly, causing a race condition. Bicep's automatic dependency detection would have prevented it.Authoring Your First Bicep File
Start with a simple resource group and storage account. Bicep uses declarative syntax: you define what you want, not how to create it. Parameters allow customization without changing the template. The uniqueString function generates a deterministic hash based on inputs. Always use parameters for values that change between environments (dev, test, prod). Avoid hardcoding names, locations, or SKUs. Use resourceGroup().location as default to align with the resource group's region. Bicep supports all ARM template functions plus additional ones like resourceGroup(), subscription(), and uniqueString(). The output block returns values after deployment, useful for passing to other templates or scripts.
uniqueString for globally unique names. For resources with naming rules (e.g., storage accounts: 3-24 chars, lowercase alphanumerics), validate with @minLength and @maxLength decorators.uniqueString to avoid collisions.uniqueString for unique names.Parameters, Variables, and Outputs
Parameters are inputs to your template. Use decorators like @minLength, @maxLength, @allowed, and @secure for validation and security. Variables are computed values that simplify expressions. Outputs return values after deployment. In production, use parameters for environment-specific settings (e.g., VM size, number of instances) and variables for derived values (e.g., full resource names). Avoid using parameters for secrets; use @secure('password') for secure strings or reference Key Vault. Bicep supports complex types like arrays and objects. Use object parameters for grouping related settings. Always provide sensible defaults for parameters to enable quick deployments.
@secure('password') for passwords and keys. In production, reference Key Vault secrets directly with keyVault.getSecret.@secure for secrets.Resource Dependencies and Referencing
Bicep automatically detects dependencies when you reference a resource's symbolic name. For example, if a virtual machine references a network interface, Bicep knows the NIC must exist first. You can also use dependsOn explicitly, but it's rarely needed. Implicit dependencies are safer and more maintainable. For cross-resource references, use the properties of the resource. For example, nic.properties.ipConfigurations[0].properties.subnet.id. Avoid hardcoding resource IDs; use the resourceId() function or symbolic reference. In production, incorrect dependency ordering is a common cause of deployment failures. Let Bicep handle it.
parent or referencing a resource's symbolic name (e.g., subnet.id) creates an implicit dependency. Bicep will deploy in the correct order.dependsOn with wrong names, causing a deployment that skipped dependencies. Implicit dependencies are more reliable.dependsOn unless necessary.Modules: Reusing and Composing Templates
Modules are Bicep files that can be referenced from other Bicep files. They enable reuse and composition. A module can be a simple resource group or a complex application stack. Modules have their own parameters and outputs. Use module keyword to reference a module file. Modules can be local files or published to a registry. In production, use modules to standardize common patterns (e.g., a standard storage account module with security defaults). Modules also enforce naming conventions and tagging policies. Always version your modules and use semantic versioning. Avoid modules that are too large or too small; aim for a single responsibility.
br: prefix to reference registry modules.Deploying with Azure CLI and PowerShell
Deploy Bicep files using az deployment group create for resource groups or az deployment sub create for subscriptions. Use --parameters to pass parameter values. For CI/CD, use --parameters @params.json to load a parameter file. PowerShell uses New-AzResourceGroupDeployment. Always use --what-if to preview changes before deployment. In production, integrate deployment into pipelines (Azure DevOps, GitHub Actions). Use deployment stacks for managing resource lifecycles. Avoid deploying directly from a developer's machine; use service principals with least privilege. Monitor deployment logs for errors and use --verbose for debugging.
what-if before production deployments. It shows changes without applying them, preventing accidental resource deletion.what-if would have caught it. Now it's mandatory in our pipeline.what-if to preview changes; automate deployments with CI/CD pipelines.Handling Secrets and Key Vault Integration
Never hardcode secrets in templates. Use Azure Key Vault to store secrets and reference them in Bicep. Use the getSecret function to retrieve a secret from Key Vault. The Key Vault must be in the same subscription and the deploying identity must have read access. For secure parameters, use @secure('password') decorator. In production, use managed identities for the deployment service principal to access Key Vault. Avoid passing secrets as outputs; they can be logged. Use @secure() on outputs if necessary. For database connection strings, construct them in the template using secrets.
Key Vault Secrets User role. Use az keyvault set-policy or RBAC. Never grant more permissions than needed.Advanced: Conditions, Loops, and Nested Deployments
Bicep supports conditions (if), loops (for), and nested deployments via modules. Use conditions to deploy resources only in certain environments (e.g., skip a diagnostic setting in dev). Use loops to create multiple instances of a resource (e.g., multiple VMs). For complex orchestration, use nested deployments with module and deploymentScript for custom scripts. In production, avoid deep nesting; prefer modules. Loops can be resource-level or property-level. Use batchSize for parallel deployment. Conditions can also be used on properties. Always test loops with small counts first to avoid hitting API limits.
batchSize(1) for sequential deployment when resources have interdependencies. Default is parallel, which can cause throttling.batchSize caused Azure API throttling and partial deployment. We now use batchSize(10) and monitor API limits.Testing and Validation
Test Bicep templates with bicep build to check syntax and bicep decompile to round-trip. Use az deployment group validate to validate against Azure Resource Manager. For unit testing, use Pester (PowerShell) or custom scripts to verify outputs. In production, integrate validation into CI/CD: run bicep build and what-if in pull requests. Use Azure Policy to enforce compliance (e.g., require tags). Test in a sandbox subscription before production. Use deployment stacks to manage resource groups as a unit. Always test parameter combinations that could break the template.
bicep build and what-if to your pipeline. Fail the build if validation errors or unexpected changes are detected.validate against a live subscription in CI.bicep build and what-if in CI/CD.Production Patterns: Deployment Stacks and Policy
Deployment stacks (preview) allow you to manage a collection of resources as a single unit. They support deny-settings to prevent manual changes. Combine with Azure Policy to enforce tagging, allowed locations, and SKUs. Use deploymentScripts for custom provisioning steps (e.g., running a script after resource creation). In production, use policy to enforce that all resources are deployed via Bicep (deny manual creation). Use deployment stacks to detect drift. Always use what-if before updating a stack. Avoid using deployment stacks for critical resources without testing.
Debugging and Troubleshooting
Common issues: syntax errors, missing API versions, incorrect resource IDs, and permission errors. Use bicep build to catch syntax errors. Use az deployment group validate for semantic errors. Check deployment logs with az deployment group show -n <deployment-name> -g <rg> --query properties.error. For runtime errors, enable debug logging with --debug. Use --verbose for detailed output. In production, set up alerts for deployment failures. Use what-if to understand changes. If a deployment fails, roll back by redeploying the previous version. Never manually fix resources; always fix the template and redeploy.
az provider list --query "[?namespace=='Microsoft.Network'].resourceTypes[].apiVersions". Permission denied: ensure service principal has Contributor role.az provider register in the pipeline before deployment.--verbose and --debug for troubleshooting; always fix the template, not the resource.Migrating from ARM to Bicep
Use bicep decompile to convert ARM templates to Bicep. Review the output; decompilation may not be perfect. Manually refactor to use modules and remove hardcoded values. Use bicep build to verify the round-trip. In production, migrate incrementally: start with simple resources, then move to complex ones. Use --force to overwrite existing Bicep files. After migration, test with what-if and deploy to a non-production environment. Keep the ARM template as a backup until the Bicep version is proven. Document any manual changes made during migration.
bicep decompile to start migration, but always review and refactor the output.| File | Command / Code | Purpose |
|---|---|---|
| main.bicep | param location string = resourceGroup().location | Why ARM Templates and Bicep Exist |
| storage.bicep | param location string = resourceGroup().location | Authoring Your First Bicep File |
| params.bicep | @minLength(3) | Parameters, Variables, and Outputs |
| dependencies.bicep | param location string = resourceGroup().location | Resource Dependencies and Referencing |
| deploy.sh | resourceGroupName='myResourceGroup' | Deploying with Azure CLI and PowerShell |
| secrets.bicep | param keyVaultName string | Handling Secrets and Key Vault Integration |
| advanced.bicep | param environment string = 'dev' | Advanced |
| test.sh | bicep build main.bicep | Testing and Validation |
| stack.bicep | param location string = resourceGroup().location | Production Patterns |
| debug.sh | az deployment group create \ | Debugging and Troubleshooting |
| migrate.sh | bicep decompile template.json --force | Migrating from ARM to Bicep |
Key takeaways
bicep build, validate, and what-if in CI/CD to catch errors before deployment.@secure decorators. Never commit secrets to source control.Frequently Asked Questions
20+ years shipping production infrastructure and CI/CD at scale. Written from production experience, not tutorials.
That's Azure. Mark it forged?
4 min read · try the examples if you haven't