11.3 Automating Deployment with ARM and Bicep Templates
Key Takeaways
- An ARM template is a JSON document with schema, contentVersion, parameters, variables, functions, resources, and outputs; Bicep is a transparent DSL that compiles to the same ARM template
- Bicep adds modules, parameters, variables, resource loops (copy), conditions (if), and decorators for parameter validation; a parameter file supplies per-environment values
- Deployment supports incremental and complete modes: incremental leaves unchanged resources in place, complete deletes resources in the resource group that are not in the template
- The what-if operation (az deployment group what-if) previews create/update/delete/no-change actions before you run a deployment, and is idempotent
- Bicep can deploy Azure SQL resources including logical servers, databases, managed instances, elastic pools, firewall rules, virtual network rules, and private endpoints, and integrates with Azure DevOps and GitHub Actions CI/CD pipelines
ARM Template Structure
An Azure Resource Manager (ARM) template is a JSON document describing the resources you want to deploy. The top-level structure:
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {},
"variables": {},
"functions": [],
"resources": [],
"outputs": {}
}
- $schema: pins the template language version.
- contentVersion: a version you set for your own tracking (any value).
- parameters: inputs supplied at deployment time (resource SKU, admin password, environment name).
- variables: computed values used to reduce repetition.
- functions: user-defined functions for reusable expressions.
- resources: the actual resources to deploy, each with
type,name,apiVersion,location,properties, and optionaldependsOn. - outputs: values returned after deployment (a connection string, a resource ID).
ARM templates are declarative and idempotent: deploying the same template repeatedly produces the same end state. The engine diffs the desired state against the current state and applies only the changes needed.
Bicep DSL
Bicep is a Domain-Specific Language that compiles transparently to ARM templates. The exam treats Bicep as the recommended authoring format because it is more readable and less verbose than JSON, while producing the same deployments. Bicep key features:
- Resource declarations with a symbolic name:
resource srv 'Microsoft.Sql/servers@2023-08-01-preview' = { ... }- the symbolic name lets other resources reference it directly, replacing explicitdependsOnstrings. - Parameters with decorators for validation:
@allowed([...]),@secure(),@minLength(n),@maxLength(n),@description('...'). Secure parameters hide values in deployment output. - Variables for computed values, including string interpolation:
var fqdn = '${srv.name}.database.windows.net'. - Modules: a
.bicepfile that deploys a related set of resources, invoked from a parent template withmodule dbModule 'modules/database.bicep' = { name: 'dbDeploy', params: { ... } }. Modules enable composition and reuse across environments. - Loops:
resource pools 'Microsoft.Sql/servers/elasticPools@...' = [for i in range(0, poolCount): { ... }]deploys multiple similar resources. - Conditions:
resource monitor 'Microsoft.Insights/components@...' = if (enableMonitoring) { ... }deploys a resource only when a condition is true. - Parameter files: one file per environment (
main.parameters.dev.json,main.parameters.prod.json) with{ "$schema": "...", "parameters": { ... } }to supply environment-specific values.
Deployment Modes and What-If
ARM deployments run in two modes:
- Incremental (default): resources in the template are created or updated; resources that exist in the resource group but are not in the template are left unchanged.
- Complete: resources in the template are created or updated, and any resources in the resource group that are not in the template are deleted. Use with caution - it is destructive for anything not declared in the template.
The what-if operation previews what a deployment would do without applying it: az deployment group what-if -g rg-prod -f main.bicep -e @main.parameters.prod.json. The output classifies each resource as Create, Update, Delete, Ignore (no change), or NoChange. What-if is idempotent and safe to run before every deployment; it is the recommended pre-flight in CI/CD pipelines.
Idempotency and Bicep Build/Decompile
Idempotency means the same template applied multiple times converges to the same state, and re-running a deployment after a manual change will reconcile the resource back to the template's declared state (for incremental mode, only for properties the template declares). Bicep tooling supports two conversions:
az bicep buildcompiles a.bicepfile to an ARM template JSON.az bicep decompileconverts an existing ARM template JSON back to a Bicep file, useful for migrating legacy templates.
The bicep restore command pulls external modules referenced via a registry path into the local cache.
Deployment Scopes
A deployment can target different scopes:
| Scope | Use |
|---|---|
| Resource group | Most Azure SQL deployments (logical server, database, MI, pool) |
| Subscription | Resource group creation, role assignments at subscription scope |
| Management group | Policy assignments across subscriptions |
| Tenant | Tenant-level definitions (rare for SQL) |
Choose the scope based on what the template manages: a template that creates a resource group and then resources inside it is a subscription-scope template; a template that creates a SQL MI and its databases is a resource-group template.
You deploy a Bicep template to a resource group using the default deployment mode. The template defines one logical server and one database. The resource group already contains an unrelated storage account that is NOT in the template. After deployment, what happens to the storage account?
Deploying Azure SQL Resources
Bicep deploys the full Azure SQL surface area. Common resource types:
| Resource | Type and use |
|---|---|
| Logical server | Microsoft.Sql/servers - the container for single databases and elastic pools |
| Database | Microsoft.Sql/servers/databases - a child resource of the server |
| Elastic pool | Microsoft.Sql/servers/elasticPools - shared compute for multiple databases |
| Managed Instance | Microsoft.Sql/managedInstances - instance-level PaaS |
| Managed Instance database | Microsoft.Sql/managedInstances/databases |
| Firewall rule | Microsoft.Sql/servers/firewallRules - per-server IP allowlist |
| Virtual network rule | Microsoft.Sql/servers/virtualNetworkRules - allow a subnet to the server |
| Private endpoint | Microsoft.Network/privateEndpoints - private connectivity to the server |
| Server/MI admin | Microsoft.Sql/servers/administrators (Azure AD admin) |
A minimal Bicep for a logical server and a database:
@secure()
param adminPassword string
param dbName string = 'appdb'
resource srv 'Microsoft.Sql/servers@2023-08-01-preview' = {
name: 'sql-prod-srv'
location: resourceGroup().location
properties: {
administratorLogin: 'sqladmin'
administratorLoginPassword: adminPassword
version: '12.0'
}
}
resource db 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
parent: srv
name: dbName
location: resourceGroup().location
sku: { name: 'GP_Gen5_2', tier: 'GeneralPurpose' }
properties: {
collation: 'SQL_Latin1_General_CP1_CI_AS'
maxSizeBytes: 10737418240
}
}
Note the symbolic reference parent: srv - this replaces the older dependsOn array and ensures the server is created before the database.
Tags, Dependencies, and Outputs
Tags are key/value pairs applied to resources for billing, automation, and governance. In Bicep, add a tags object on the resource:
resource srv 'Microsoft.Sql/servers@...' = {
name: 'sql-prod-srv'
location: resourceGroup().location
tags: {
env: 'prod'
owner: 'data-platform'
}
properties: { ... }
}
Dependencies are expressed implicitly through symbolic references (parent: srv, or by referencing a property like srv.id) or explicitly via dependsOn. Prefer implicit dependencies in Bicep; the compiler computes the correct ordering.
Outputs return values to the caller or to the next pipeline stage:
output serverFqdn string = srv.properties.fullyQualifiedDomainName
output serverId string = srv.id
CI/CD Integration
The exam expects you to wire Bicep into a pipeline. The standard flow:
- Build/validate:
az bicep build --file main.bicepandaz deployment group validate -g rg -f main.bicep -e @main.parameters.prod.json. - What-if:
az deployment group what-if -g rg -f main.bicep -e @main.parameters.prod.jsonto preview changes. - Deploy:
az deployment group create -g rg -f main.bicep -e @main.parameters.prod.json.
In Azure DevOps, use the AzureCLI@2 task with an Azure service connection to run these commands, or the AzureResourceManagerTemplateDeployment@3 task for the create step. In GitHub Actions, the azure/login@v1 action authenticates and azure/cli@v1 runs the Bicep commands, or the azure/arm-deploy@v1 action wraps the deployment. A common pattern is a pipeline per environment (dev, test, prod) that loads the matching parameter file and runs validate, what-if, and deploy as stages, with approvals gating prod.
Exam Traps for ARM and Bicep
- Complete vs incremental: the only mode that deletes resources not in the template is complete mode; incremental never deletes.
- Secure parameters: passwords passed as plain parameters appear in deployment output and the deployment history; use
@secure()in Bicep (or"type": "secureString"in ARM) to redact them. - apiVersion: each resource declares its own
apiVersion; using a stale one can miss newer properties. Bicep surfaces warnings for outdated versions. - apiVersion and SKU names: Azure SQL SKU names are version-specific -
GP_Gen5_2for vCore,S0for DTU - and must match the apiVersion's expectations. - Parameter file path:
-e @main.parameters.prod.jsonrequires the@prefix; omitting it passes a literal string. - Resource loops vs modules: a loop deploys multiple instances of one resource type; a module deploys a reusable bundle of different resources. A scenario describing shared components across teams points to modules.
- Private endpoints and VNet rules: a scenario requiring private connectivity to a logical server points to
Microsoft.Network/privateEndpointsand aprivateDnsZoneGroup; a scenario allowing a subnet to reach the server points to a virtual network rule on a subnet delegated toMicrosoft.Sql/servers.
You need to deploy an Azure SQL Managed Instance, its virtual network and subnet delegation, and a private endpoint, across multiple environments (dev, test, prod), reusing the same template with different parameter values. Which Bicep approach is most appropriate?