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
Last updated: August 2026

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 optional dependsOn.
  • 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 explicit dependsOn strings.
  • 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 .bicep file that deploys a related set of resources, invoked from a parent template with module 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 build compiles a .bicep file to an ARM template JSON.
  • az bicep decompile converts 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:

ScopeUse
Resource groupMost Azure SQL deployments (logical server, database, MI, pool)
SubscriptionResource group creation, role assignments at subscription scope
Management groupPolicy assignments across subscriptions
TenantTenant-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.

Test Your Knowledge

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?

A
B
C
D

Deploying Azure SQL Resources

Bicep deploys the full Azure SQL surface area. Common resource types:

ResourceType and use
Logical serverMicrosoft.Sql/servers - the container for single databases and elastic pools
DatabaseMicrosoft.Sql/servers/databases - a child resource of the server
Elastic poolMicrosoft.Sql/servers/elasticPools - shared compute for multiple databases
Managed InstanceMicrosoft.Sql/managedInstances - instance-level PaaS
Managed Instance databaseMicrosoft.Sql/managedInstances/databases
Firewall ruleMicrosoft.Sql/servers/firewallRules - per-server IP allowlist
Virtual network ruleMicrosoft.Sql/servers/virtualNetworkRules - allow a subnet to the server
Private endpointMicrosoft.Network/privateEndpoints - private connectivity to the server
Server/MI adminMicrosoft.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:

  1. Build/validate: az bicep build --file main.bicep and az deployment group validate -g rg -f main.bicep -e @main.parameters.prod.json.
  2. What-if: az deployment group what-if -g rg -f main.bicep -e @main.parameters.prod.json to preview changes.
  3. 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_2 for vCore, S0 for DTU - and must match the apiVersion's expectations.
  • Parameter file path: -e @main.parameters.prod.json requires 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/privateEndpoints and a privateDnsZoneGroup; a scenario allowing a subnet to reach the server points to a virtual network rule on a subnet delegated to Microsoft.Sql/servers.
Test Your Knowledge

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?

A
B
C
D