Skip to main content
AzureDevOps & IaCintermediate

ARM Templates vs Bicep

Compare ARM JSON templates and Bicep for Azure infrastructure as code deployments.

CloudToolStack Team24 min readPublished Feb 22, 2026

Prerequisites

  • Azure subscription with resource deployment permissions
  • Basic understanding of infrastructure as code concepts

ARM Templates vs Bicep: Infrastructure as Code on Azure

Azure Resource Manager (ARM) templates have been the native Infrastructure as Code (IaC) language for Azure since 2014. Bicep, introduced in 2020, is a domain-specific language (DSL) that compiles down to ARM JSON. It offers a dramatically cleaner syntax while maintaining full feature parity with ARM templates. Both are first-party Azure IaC tools that deploy through the same Azure Resource Manager engine, which means they have day-zero support for every Azure resource type.

This guide provides an in-depth comparison of ARM templates and Bicep, practical migration strategies, module design patterns, CI/CD integration, and guidance on when to choose Bicep, ARM, Terraform, or other IaC tools. Whether you are starting a greenfield Azure project or managing a legacy ARM template library, this guide helps you make informed decisions and adopt best practices.

Side-by-Side Syntax Comparison

The most immediate difference between ARM and Bicep is verbosity. Let's deploy the same resource, a Storage Account, in both languages to see the contrast.

storage-account.json (ARM Template)
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageAccountName": {
      "type": "string",
      "metadata": {
        "description": "Name of the storage account"
      }
    },
    "location": {
      "type": "string",
      "defaultValue": "[resourceGroup().location]"
    },
    "sku": {
      "type": "string",
      "defaultValue": "Standard_LRS",
      "allowedValues": [
        "Standard_LRS",
        "Standard_GRS",
        "Standard_ZRS"
      ]
    }
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-01-01",
      "name": "[parameters('storageAccountName')]",
      "location": "[parameters('location')]",
      "sku": {
        "name": "[parameters('sku')]"
      },
      "kind": "StorageV2",
      "properties": {
        "minimumTlsVersion": "TLS1_2",
        "supportsHttpsTrafficOnly": true,
        "allowBlobPublicAccess": false
      }
    }
  ],
  "outputs": {
    "storageAccountId": {
      "type": "string",
      "value": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
    },
    "blobEndpoint": {
      "type": "string",
      "value": "[reference(parameters('storageAccountName')).primaryEndpoints.blob]"
    }
  }
}
storage-account.bicep (Bicep)
@description('Name of the storage account')
param storageAccountName string

param location string = resourceGroup().location

@allowed(['Standard_LRS', 'Standard_GRS', 'Standard_ZRS'])
param sku string = 'Standard_LRS'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: sku
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
    allowBlobPublicAccess: false
  }
}

output storageAccountId string = storageAccount.id
output blobEndpoint string = storageAccount.properties.primaryEndpoints.blob

The Bicep version is roughly one-third the line count of the ARM equivalent. The reduction comes from eliminating JSON boilerplate (schema declarations, content versions), bracket function syntax ([parameters('name')] vs name), and explicit output type wiring. Bicep infers types, resolves references automatically, and uses a concise declarative syntax that is easier to read and write.

Key Differences

FeatureARM TemplatesBicep
SyntaxJSON (verbose, bracket functions)Clean DSL (concise, type-safe)
File size3-5x larger for same resourceCompact, human-readable
ModularityLinked/nested templates (complex URI handling)Native modules with clean file references
IDE supportBasic JSON IntelliSenseRich VS Code extension (validation, completions, snippets, refactoring)
Learning curveSteep (JSON complexity, bracket functions)Moderate (familiar to Terraform/HCL users)
CompilationDirect: submitted as-is to ARMTranspiles to ARM JSON before deployment
Resource supportDay-zero for all Azure resourcesDay-zero (same resource provider APIs)
State managementNone (Azure Resource Manager is the state)None (Azure Resource Manager is the state)
Dependency managementExplicit dependsOn requiredAutomatic inference from resource references
Linting and validationARM-TTK tool (community)Built-in linter with configurable rules

Bicep Is the Future of Azure IaC

Microsoft has officially stated that Bicep is the recommended language for Azure native IaC. While ARM templates will continue to be supported indefinitely (Bicep compiles to ARM JSON), all new documentation, tooling investment, and feature development is focused on Bicep. If you are starting a new project, choose Bicep. If you have existing ARM templates, plan an incremental migration to Bicep as you update them.

Bicep Language Features

Bicep offers several language features that make infrastructure code more expressive and maintainable compared to raw ARM JSON.

Parameters with Decorators

Bicep uses decorators (similar to Python or TypeScript) to add metadata and constraints to parameters. This is much more readable than ARM's nested JSON properties.

Parameters with rich decorators
@description('The Azure region for all resources')
param location string = resourceGroup().location

@description('Environment name used for resource naming')
@allowed(['dev', 'staging', 'production'])
param environment string

@description('The SKU for the App Service Plan')
@minLength(2)
@maxLength(10)
param appServiceSku string = environment == 'production' ? 'P2v3' : 'B1'

@description('Admin email for alerts')
@metadata({ example: 'admin@company.com' })
param adminEmail string

@secure()
@description('Database admin password')
param dbPassword string

// User-defined types (Bicep v0.12+)
type networkConfig = {
  vnetName: string
  subnetName: string
  @description('CIDR prefix for the subnet')
  addressPrefix: string
}

param networkSettings networkConfig

Conditional Deployments and Loops

Conditions and loops in Bicep
param environment string
param regions string[] = ['eastus2', 'westeurope']
param deployWaf bool = environment == 'production'

// Conditional resource deployment
resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2023-05-01' = if (deployWaf) {
  name: 'waf-policy'
  location: regions[0]
  properties: {
    policySettings: {
      mode: 'Prevention'
      state: 'Enabled'
    }
  }
}

// Loop over regions to deploy resources
resource storageAccounts 'Microsoft.Storage/storageAccounts@2023-01-01' = [for (region, i) in regions: {
  name: 'storage${environment}${i}'
  location: region
  sku: { name: 'Standard_ZRS' }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
  }
}]

// Loop with filtering
var prodRegions = filter(regions, r => r != 'westeurope' || environment == 'production')

// Output array from loop
output storageAccountIds array = [for i in range(0, length(regions)): storageAccounts[i].id]

Bicep Modules: Clean Reusability

One of Bicep's biggest advantages over ARM templates is its native module system. Modules let you encapsulate resource definitions into reusable, parameterized components. In ARM, modularity required linked templates with complex URI handling, SAS tokens for private storage, and nested deployment orchestration. In Bicep, you simply reference another .bicep file.

modules/appService.bicep: Reusable module
@description('Name for the App Service and plan')
param appName string

param location string = resourceGroup().location

@allowed(['B1', 'B2', 'P1v3', 'P2v3', 'P3v3'])
param sku string = 'B1'

param subnetId string = ''

resource appServicePlan 'Microsoft.Web/serverfarms@2023-01-01' = {
  name: '${appName}-plan'
  location: location
  sku: {
    name: sku
  }
  kind: 'linux'
  properties: {
    reserved: true
  }
}

resource webApp 'Microsoft.Web/sites@2023-01-01' = {
  name: appName
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    virtualNetworkSubnetId: !empty(subnetId) ? subnetId : null
    siteConfig: {
      minTlsVersion: '1.2'
      ftpsState: 'Disabled'
      alwaysOn: sku != 'B1'
      linuxFxVersion: 'NODE|20-lts'
    }
  }
}

output appUrl string = 'https://${webApp.properties.defaultHostName}'
output appId string = webApp.id
output principalId string = webApp.identity.principalId
main.bicep: Consuming modules
targetScope = 'resourceGroup'

param environment string = 'production'
param location string = resourceGroup().location

// Deploy frontend using the reusable module
module frontendApp 'modules/appService.bicep' = {
  name: 'frontend-deployment'
  params: {
    appName: 'myapp-frontend-${environment}'
    location: location
    sku: environment == 'production' ? 'P1v3' : 'B1'
  }
}

// Deploy backend using the same module with different params
module backendApp 'modules/appService.bicep' = {
  name: 'backend-deployment'
  params: {
    appName: 'myapp-backend-${environment}'
    location: location
    sku: environment == 'production' ? 'P2v3' : 'B1'
  }
}

// Use outputs from modules as inputs to other resources
module keyVaultAccess 'modules/keyVaultRbac.bicep' = {
  name: 'keyvault-access'
  params: {
    keyVaultName: 'myapp-${environment}-kv'
    principalId: frontendApp.outputs.principalId
    roleName: 'Key Vault Secrets User'
  }
}

output frontendUrl string = frontendApp.outputs.appUrl
output backendUrl string = backendApp.outputs.appUrl

Bicep Registry for Shared Modules

For organizations with multiple teams, the Bicep module registry allows you to publish and consume modules from Azure Container Registry. This creates a centralized library of approved, versioned infrastructure modules that teams can reference.

Terminal: Publish and consume registry modules
# Create an Azure Container Registry for Bicep modules
az acr create --name myorgbicepregistry --resource-group infra-rg --sku Basic

# Publish a module to the registry
az bicep publish \
  --file modules/appService.bicep \
  --target br:myorgbicepregistry.azurecr.io/bicep/modules/app-service:v1.2.0

# In your main.bicep, reference the registry module:
# module frontend 'br:myorgbicepregistry.azurecr.io/bicep/modules/app-service:v1.2.0' = {
#   name: 'frontend'
#   params: { ... }
# }

# Configure bicepconfig.json for module aliases
# This simplifies registry references in Bicep files
bicepconfig.json: Registry aliases and linting rules
{
  "moduleAliases": {
    "br": {
      "myorg": {
        "registry": "myorgbicepregistry.azurecr.io",
        "modulePath": "bicep/modules"
      }
    }
  },
  "analyzers": {
    "core": {
      "enabled": true,
      "rules": {
        "no-hardcoded-location": { "level": "error" },
        "no-unused-params": { "level": "warning" },
        "no-unused-vars": { "level": "warning" },
        "prefer-interpolation": { "level": "warning" },
        "secure-parameter-default": { "level": "error" },
        "use-recent-api-versions": { "level": "warning" },
        "use-resource-symbol-reference": { "level": "warning" }
      }
    }
  }
}

Module Versioning Strategy

Use semantic versioning (major.minor.patch) for registry modules. Bump the major version for breaking changes (removed parameters, changed resource types), minor for new features (added parameters with defaults), and patch for bug fixes. Pin module versions in production deployments and use version ranges only in development. This prevents unexpected changes from breaking production infrastructure.

Migrating from ARM to Bicep

Azure provides a built-in decompiler that converts ARM JSON to Bicep. While the output may need manual cleanup (especially for complex templates with nested deployments or advanced copy loops), it provides a solid starting point for migration.

Migration Strategy

  1. Inventory: Catalog all ARM templates in your repository. Prioritize actively maintained templates for migration.
  2. Decompile: Use az bicep decompile to convert each template. Review and fix any warnings or errors.
  3. Refactor: Clean up the generated Bicep code. Extract modules, improve naming, add descriptions, and remove redundant code.
  4. Validate: Run az deployment group what-if to compare the Bicep output against the current state. Ensure no unintended changes.
  5. Deploy: Deploy the Bicep version to a non-production environment first. Verify resource configurations match expectations.
  6. Retire: Once validated, update CI/CD pipelines to use Bicep files and archive the old ARM templates.
Terminal: Decompile, validate, and deploy
# Decompile an existing ARM template to Bicep
az bicep decompile --file azuredeploy.json
# Creates azuredeploy.bicep - review and clean up the output

# Build Bicep to ARM JSON (to verify compilation succeeds)
az bicep build --file main.bicep
# Creates main.json - the ARM template Bicep generates

# What-if deployment (preview changes without applying them)
az deployment group what-if \
  --resource-group myRG \
  --template-file main.bicep \
  --parameters environment=staging

# Deploy a Bicep file directly (CLI handles transpilation automatically)
az deployment group create \
  --resource-group myRG \
  --template-file main.bicep \
  --parameters environment=production

# Deploy with a parameters file
az deployment group create \
  --resource-group myRG \
  --template-file main.bicep \
  --parameters @parameters.production.json

# Subscription-level deployment (for resource groups, policies, etc.)
az deployment sub create \
  --location eastus2 \
  --template-file subscription.bicep \
  --parameters @parameters.json

# Management group deployment (for governance across subscriptions)
az deployment mg create \
  --location eastus2 \
  --management-group-id "MyOrg" \
  --template-file governance.bicep

Decompile Limitations

The decompiler handles most ARM constructs, but complex nested templates, certain copy loop patterns, some bracket functions with dynamic property access, and templates using the reference() function with unusual parameters may need manual adjustment. Always review the generated Bicep code, run the linter, and perform a what-if deployment before deploying decompiled templates to production. Treat decompilation as a starting point, not a finished product.

Deployment Scopes

Both ARM and Bicep support four deployment scopes, each targeting a different level of the Azure resource hierarchy. Understanding scopes is essential for deploying governance resources, subscriptions, and cross-resource-group architectures.

ScopeBicep targetScopeCLI CommandWhat You Deploy
Resource GrouptargetScope = 'resourceGroup'az deployment group createVMs, storage, databases, app services
SubscriptiontargetScope = 'subscription'az deployment sub createResource groups, policies, role assignments
Management GrouptargetScope = 'managementGroup'az deployment mg createPolicies, blueprints, cross-subscription governance
TenanttargetScope = 'tenant'az deployment tenant createManagement groups, tenant-level policies
subscription-level.bicep: Deploy resource groups and policies
targetScope = 'subscription'

param location string = 'eastus2'
param environment string

// Create resource groups
resource prodRG 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: 'myapp-${environment}-rg'
  location: location
  tags: {
    environment: environment
    'cost-center': 'CC-1234'
    'managed-by': 'bicep'
  }
}

// Deploy resources into the resource group using a module
module infrastructure 'modules/infrastructure.bicep' = {
  name: 'infra-deployment'
  scope: prodRG
  params: {
    location: location
    environment: environment
  }
}

// Assign a policy at subscription level
resource requireTagPolicy 'Microsoft.Authorization/policyAssignments@2022-06-01' = {
  name: 'require-env-tag'
  properties: {
    policyDefinitionId: '/providers/Microsoft.Authorization/policyDefinitions/871b6d14-10aa-478d-b466-ef391786538f'
    displayName: 'Require environment tag on resource groups'
    enforcementMode: 'Default'
    parameters: {
      tagName: { value: 'environment' }
    }
  }
}

CI/CD Integration

Bicep integrates seamlessly with all major CI/CD platforms. The Azure CLI handles Bicep transpilation automatically, so your pipeline simply runs az deploymentcommands against .bicep files.

GitHub Actions Workflow

.github/workflows/deploy-infra.yml
name: Deploy Infrastructure
on:
  push:
    branches: [main]
    paths: ['infra/**']
  pull_request:
    branches: [main]
    paths: ['infra/**']

permissions:
  id-token: write   # Required for OIDC authentication
  contents: read

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Lint Bicep files
        run: az bicep lint --file infra/main.bicep

      - name: Build Bicep (verify compilation)
        run: az bicep build --file infra/main.bicep

  preview:
    needs: validate
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (OIDC - no secrets)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: What-If Preview
        run: |
          az deployment group what-if \
            --resource-group myapp-production-rg \
            --template-file infra/main.bicep \
            --parameters @infra/parameters.production.json

  deploy:
    needs: preview
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Deploy Infrastructure
        run: |
          az deployment group create \
            --resource-group myapp-production-rg \
            --template-file infra/main.bicep \
            --parameters @infra/parameters.production.json

Use OIDC Authentication in Pipelines

The GitHub Actions workflow above uses OpenID Connect (OIDC) federated credentials instead of storing client secrets. This is the most secure approach for CI/CD authentication to Azure. Configure workload identity federation in Azure AD to trust tokens from your GitHub repository. This eliminates the need to store and rotate Azure credentials in GitHub Secrets.

Set up workload identity federation for CI/CD pipelines

Bicep vs Terraform Comparison

Terraform is the other major IaC tool used for Azure deployments. While both are excellent tools, they have fundamentally different architectures and strengths.

DimensionBicepTerraform
Cloud supportAzure onlyMulti-cloud (Azure, AWS, GCP, 1000+ providers)
State managementNone (ARM is the state)Required (state file in backend)
Resource supportDay-zero for all Azure resourcesDepends on provider version (usually days-weeks lag)
Drift detectionWhat-if preview (pre-deployment)Plan + drift detection (continuous)
Resource removalRequires complete deployment modeAutomatically removes unmanaged resources
Learning curveAzure-specific, simpler for Azure-only teamsBroader ecosystem, transferable skills
Module ecosystemGrowing (Bicep registry, Azure Verified Modules)Mature (Terraform Registry, thousands of modules)
CostFree (open source)Free (open source) / Paid (HCP Terraform for state, governance)

Choosing Between Bicep and Terraform

Choose Bicep if you are Azure-only, want the tightest Azure integration, prefer no state file management, and need day-zero support for new Azure features. Choose Terraform if you manage infrastructure across multiple cloud providers, want mature drift detection, need the extensive Terraform provider ecosystem, or your team already has Terraform expertise. Many organizations use both: Bicep for core Azure infrastructure and Terraform for multi-cloud and SaaS provider integrations.

Best Practices and Project Structure

A well-organized Bicep project follows consistent patterns for file structure, naming, and module design.

Recommended Project Structure

Bicep project directory structure
infra/
  main.bicep                    # Entry point for deployment
  bicepconfig.json              # Linter rules, registry aliases
  parameters/
    dev.json                    # Development environment parameters
    staging.json                # Staging environment parameters
    production.json             # Production environment parameters
  modules/
    networking/
      vnet.bicep                # VNet module
      nsg.bicep                 # NSG module
      privateEndpoint.bicep     # Private endpoint module
    compute/
      appService.bicep          # App Service module
      functionApp.bicep         # Function App module
      aks.bicep                 # AKS cluster module
    data/
      sqlDatabase.bicep         # Azure SQL module
      cosmosDb.bicep            # Cosmos DB module
      storageAccount.bicep      # Storage account module
    security/
      keyVault.bicep            # Key Vault module
      rbac.bicep                # RBAC assignment module
    monitoring/
      logAnalytics.bicep        # Log Analytics workspace
      alerts.bicep              # Alert rules
  scripts/
    deploy.sh                   # Deployment helper script
    validate.sh                 # Validation script

Naming and Coding Conventions

  • Use camelCase for parameter and variable names: storageAccountName, appServiceSku
  • Add @description to all parameters: This generates documentation and improves IntelliSense
  • Use @secure() for sensitive parameters: Prevents values from appearing in deployment logs
  • Prefer string interpolation over concat(): 'prefix-${name}-suffix' is clearer
  • Use meaningful resource symbolic names: resource prodStorage not resource sa1
  • Keep modules focused: Each module should deploy one logical resource or a tightly coupled set

Avoid Secrets in Parameter Files

Never store secrets (passwords, API keys, connection strings) in parameter files that are committed to source control. Instead, use Key Vault references in your parameter files, pass secrets through pipeline variables, or use the @secure()decorator and provide values at deployment time via --parameters overrides.

When to Use What

While Bicep is the clear winner for most Azure-native scenarios, different tools may be more appropriate depending on your context:

  • New Azure-native projects: Use Bicep. It is the most productive and best-supported option for Azure-only infrastructure.
  • Multi-cloud environments: Consider Terraform. If you manage AWS, GCP, and Azure resources, Terraform provides a single language across all providers.
  • Existing ARM template libraries: Migrate incrementally to Bicep. Use the decompiler and convert templates as you make changes to them.
  • CI/CD pipeline templates: Bicep works in all pipelines that support Azure CLI. GitHub Actions, Azure DevOps, GitLab CI, and Jenkins all work seamlessly.
  • Template Specs: Bicep files can be compiled to ARM and published as Template Specs for portal-based deployments by non-engineer stakeholders.
  • Azure Verified Modules (AVM): The AVM initiative provides production-ready Bicep modules for common Azure resources, tested and maintained by Microsoft. Use these as building blocks instead of writing everything from scratch.
Apply IaC best practices from the Operational Excellence pillarDeploy hub-and-spoke networks using Bicep modulesManage secrets securely in your Bicep deployments with Key VaultTag resources for cost tracking using Bicep parameter conventions

Key Takeaways

  1. 1Bicep is a domain-specific language that compiles to ARM JSON templates.
  2. 2Bicep is dramatically more concise, typically 50-70% fewer lines than equivalent ARM JSON.
  3. 3Bicep provides first-class VS Code support with IntelliSense and validation.
  4. 4ARM templates remain the deployment engine; Bicep is an authoring improvement.
  5. 5Bicep modules enable reuse and composition of infrastructure patterns.
  6. 6Existing ARM templates can be decompiled to Bicep for gradual migration.

Frequently Asked Questions

Is Bicep replacing ARM templates?
Bicep replaces ARM JSON as the authoring language, but ARM templates remain the deployment engine. Bicep compiles to ARM JSON before deployment. Microsoft recommends Bicep for all new Azure IaC projects.
Can I convert existing ARM templates to Bicep?
Yes. Use 'az bicep decompile' to convert ARM JSON to Bicep. The conversion handles most constructs automatically. Some complex expressions may need manual adjustment. This enables incremental migration.
How does Bicep compare to Terraform for Azure?
Bicep is Azure-native with same-day support for new Azure features. Terraform is cloud-agnostic with a larger ecosystem. Bicep is simpler to learn for Azure-only teams. Terraform is better for multi-cloud environments.
Does Bicep support all ARM template features?
Yes. Bicep compiles to ARM JSON and supports every Azure resource type. New resource types are available in Bicep immediately. Bicep also adds features ARM lacks, like modules, loops, and conditional deployments with cleaner syntax.
What is a Bicep module?
Modules are reusable Bicep files that encapsulate related resources. They accept parameters and expose outputs. You can publish modules to a private registry for team-wide sharing. This promotes consistency and reduces duplication across deployments.

Written by CloudToolStack Team

Cloud engineers and architects with hands-on experience across AWS, Azure, and GCP. We write guides based on real-world production patterns, not just documentation rewrites.

Disclaimer: This guide is for educational purposes. Cloud services change frequently; always refer to official documentation for the latest information. AWS, Azure, and GCP are trademarks of their respective owners.