Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Azure Bicep: Infrastructure as Code

By Kokil Thapa | Last reviewed: August 2026

Managing cloud infrastructure through manual portal clicks creates drift, security gaps, and recovery nightmares. Azure Bicep: Infrastructure as Code solves this by letting you define Azure resources in a clean, declarative DSL that transpiles to standard ARM JSON. For developers building Laravel or Node.js applications on Azure, Bicep offers a maintainable alternative to raw ARM templates while retaining full platform support. If you are evaluating cloud hosting services in Nepal or managing global Azure deployments, understanding Bicep is now essential for reliable operations.

What is Azure Bicep: Infrastructure as Code and why replace ARM?

ARM (Azure Resource Manager) templates have been the standard for Azure automation since 2015, but their JSON verbosity makes them painful to write and review. A simple storage account definition can span 50+ lines of nested JSON objects, parameters, and variables. Bicep abstracts this complexity into a readable syntax that compiles directly to valid ARM JSON at deployment time.

In practice, Bicep reduces template size by 40–60% compared to equivalent ARM JSON. More importantly, it eliminates common JSON errors like missing commas, bracket mismatches, and incorrect nesting that plague large ARM templates. The Azure Bicep CLI handles transpilation transparently — you never commit generated JSON to source control.

main.bicepDeclarative DSLType-safe syntaxModular structureARM JSONGenerated artifactNot committedStandard formatAzure RMResource ProviderIdempotent deployState trackingAzure Bicep: Infrastructure as Code Pipeline
Azure Bicep Infrastructure as Code workflow: Bicep files transpile to ARM JSON before deployment to Azure Resource Manager

Key advantages over raw ARM templates include:

  • Type safety and validation: VS Code Bicep extension provides real-time error checking and autocomplete for resource types, API versions, and properties
  • Simplified syntax: No need for parameters, variables, and resources wrapper objects; declare everything at root level
  • Native module support: Split infrastructure into reusable components without complex linked template URLs or artifact staging
  • Symbolic naming: Reference resources by logical name instead of reference() functions with resourceId strings
  • Zero runtime dependency: Bicep CLI ships with Azure CLI; no separate installation or build server configuration needed

For teams already using Terraform, Bicep occupies a different niche. Terraform manages multi-cloud state and uses HCL; Bicep is Azure-native, stateless (relies on ARM state), and receives day-one support for new Azure features. Choose Bicep when your stack is Azure-only and you want tightest platform integration.

How do you write your first Azure Bicep template?

Start with the Azure CLI (v2.65+ recommended for 2026). Verify installation:

az bicep version
# Output: Bicep CLI version 0.30.x (latest stable 2026)

Create a basic storage account deployment:

// main.bicep
param location string = resourceGroup().location
param storageAccountName string = 'stapp${uniqueString(resourceGroup().id)}'

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

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

Deploy to a resource group:

# Create resource group if needed
az group create --name rg-demo-bicep --location eastus

# Deploy Bicep file directly (CLI transpiles automatically)
az deployment group create \
  --resource-group rg-demo-bicep \
  --template-file main.bicep \
  --parameters location=eastus

Notice the API version @2023-05-01 pinned explicitly. Always use specific API versions rather than latest to prevent breaking changes during redeployment. The Bicep extension shows available versions via autocomplete.

Handling dependencies and outputs

Bicep infers implicit dependencies when you reference one resource inside another. Explicit dependencies use dependsOn:

resource container 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
  parent: storageAccount  // Implicit dependency established
  name: 'default/images'
  properties: {
    publicAccess: 'None'
  }
}

// Explicit dependency when no direct reference exists
resource functionApp 'Microsoft.Web/sites@2023-12-01' = {
  name: 'func-app-demo'
  location: location
  dependsOn: [storageAccount]  // Wait for storage even without reference
}

Outputs enable downstream consumption in CI pipelines or other modules. Use existing keyword to reference resources not managed by current template:

resource existingVnet 'Microsoft.Network/virtualNetworks@2023-11-01' existing = {
  name: 'vnet-production'
}

output vnetId string = existingVnet.id

How do you structure Azure Bicep modules for reusability?

Monolithic Bicep files become unmaintainable beyond 200–300 lines. Modules encapsulate related resources behind stable interfaces. This pattern mirrors service extraction in application code — something I apply regularly when architecting Laravel backends or configuring DevOps automation for web projects.

main.bicepOrchestration Layernetworking.bicepVNet + SubnetsNSG RulesPrivate Endpointscompute.bicepApp Service PlanWeb App / FunctionManaged Identitydatabase.bicepSQL / PostgreSQLRedis CacheConnection StringsModular Azure Bicep: Infrastructure as Code Architecture
Modular Azure Bicep Infrastructure as Code architecture separating networking, compute, and database concerns

Create a reusable App Service module:

// modules/app-service.bicep
param name string
param location string
param appServicePlanId string
param runtimeStack string = 'DOTNET|8.0'

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' existing = {
  name: split(appServicePlanId, '/')[8]
}

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: name
  location: location
  serverFarmId: appServicePlanId
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    siteConfig: {
      netFrameworkVersion: 'v8.0'
      alwaysOn: true
      ftpsState: 'Disabled'
      minTlsVersion: '1.2'
    }
    httpsOnly: true
  }
}

output principalId string = webApp.identity.principalId
output defaultHostName string = webApp.properties.defaultHostName

Consume in main template:

module webApp './modules/app-service.bicep' = {
  name: 'deploy-web-app'
  params: {
    name: 'app-frontend-${environment}'
    location: location
    appServicePlanId: appServicePlan.id
    runtimeStack: 'NODE|22-lts'
  }
}

// Use module output for RBAC assignment
resource blobContributor 'Microsoft.Authorization/roleAssignments@2023-04-01-preview' = {
  scope: storageAccount
  name: guid(storageAccount.id, webApp.outputs.principalId, 'BlobDataContributor')
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'ba92f5b4-2d11-453d-a403-e96b0029c9fe')
    principalId: webApp.outputs.principalId
    principalType: 'ServicePrincipal'
  }
}

Module best practices:

  1. Parameterize everything environment-specific — names, SKUs, regions, feature flags
  2. Expose only necessary outputs — avoid leaking internal resource IDs unless consumed externally
  3. Pin API versions per module — allows independent upgrade cycles
  4. Use Bicep registries (ACR or MCR) for cross-team sharing instead of Git submodules
  5. Document parameters with decorators: @description(), @allowed(), @minLength()

Azure Bicep vs Terraform vs ARM: Which should you choose?

Selecting an IaC tool depends on team skills, cloud commitment, and operational requirements. Here is a practical comparison based on production usage patterns:

CriteriaAzure BicepTerraformARM Templates
Learning curveLow (Azure-native syntax)Medium (HCL + providers)High (verbose JSON)
Multi-cloud supportAzure onlyAll major cloudsAzure only
State managementARM-managed (stateless)Explicit state files/backendARM-managed (stateless)
New Azure feature supportDay-zeroProvider-dependent (weeks)Day-zero
Tooling maturityVS Code ext + CLIMature ecosystemLegacy tooling
Community modulesAVM (growing)Registry (extensive)Quickstarts (dated)
Drift detectionManual / What-ifNative plan commandManual / What-if
Best forAzure-pure shops, MSFT partnersMulti-cloud, complex stateLegacy maintenance only

Choose Azure Bicep: Infrastructure as Code when your organization is committed to Azure, wants minimal abstraction overhead, and values immediate access to new platform capabilities. Choose Terraform when managing resources across AWS/GCP/Azure simultaneously or when mature state locking and drift detection are non-negotiable. Avoid starting new projects with ARM templates in 2026 — migrate existing ones to Bicep using az bicep decompile.

How do you integrate Azure Bicep into CI/CD pipelines?

Infrastructure changes must flow through the same review and testing process as application code. Here is a GitHub Actions workflow validated for 2026:

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

permissions:
  id-token: write
  contents: read

jobs:
  validate-and-deploy:
    runs-on: ubuntu-latest
    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: Validate Bicep
        run: |
          az deployment group validate \
            --resource-group rg-production \
            --template-file infra/main.bicep \
            --parameters infra/prod.parameters.json
      
      - name: Preview Changes (What-If)
        run: |
          az deployment group what-if \
            --resource-group rg-production \
            --template-file infra/main.bicep \
            --parameters infra/prod.parameters.json \
            --result-format FullResourcePayloads
      
      - name: Deploy
        run: |
          az deployment group create \
            --resource-group rg-production \
            --template-file infra/main.bicep \
            --parameters infra/prod.parameters.json \
            --name "deploy-${{ github.run_number }}"

Critical pipeline practices:

  • Use OIDC federation instead of long-lived service principal secrets — eliminates credential rotation burden
  • Always run what-if before deploy — catches destructive changes (deletions, replacements) in PR reviews
  • Parameterize environments separatelydev.parameters.json, prod.parameters.json with distinct SKUs and naming
  • Tag deployments with Git SHA — enables rollback correlation between app and infra versions
  • Enforce policy via Azure Policy — Bicep cannot prevent post-deployment drift; policies guard compliance continuously
PR / PushLint + BuildUnit TestsValidateWhat-IfChange PreviewDrift CheckComment on PRApprovalManual GateProd OnlyAudit LogDeployaz deployment createOutput CaptureSmoke TestsAzure Bicep CI/CD Pipeline Stages
CI/CD pipeline stages for Azure Bicep Infrastructure as Code from validation through approved deployment

For teams using CI/CD pipeline setups with GitLab CI (common in Nepal-based dev shops), replace GitHub Actions with equivalent azure-cli container jobs and OIDC via workload identity federation. The core commands remain identical.

Common Azure Bicep mistakes and how to avoid them

After reviewing dozens of Bicep implementations, these issues recur consistently:

Hardcoding resource names without uniqueness

Global resources (storage accounts, Key Vaults) require globally unique names. Always incorporate uniqueString() or environment prefixes:

// BAD: Will fail on second deployment
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: 'my-keyvault'
}

// GOOD: Deterministic uniqueness per resource group
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {
  name: 'kv-${uniqueString(resourceGroup().id)}'
}

Ignoring deployment scope mismatches

Bicep defaults to resource group scope. Subscription-level resources (RBAC, policy, resource groups themselves) require explicit scope:

// In main.bicep deployed at subscription scope
targetScope = 'subscription'

resource rg 'Microsoft.Resources/resourceGroups@2023-07-01' = {
  name: 'rg-app-${environment}'
  location: 'eastus'
}

// Module targeting specific resource group
module network './networking.bicep' = {
  name: 'network-deploy'
  scope: rg  // Explicit scope override
  params: { /* ... */ }
}

Missing secret handling discipline

Never pass secrets as plain parameters. Use Key Vault references or managed identities:

// Secure parameter declaration
@secure()
param dbPassword string

// Better: Reference existing Key Vault secret directly
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'kv-shared-secrets'
}

module sqlDb './modules/sql-database.bicep' = {
  name: 'sql-deploy'
  params: {
    adminPassword: kv.getSecret('sql-admin-password')
  }
}

Neglecting what-if before production deploys

The what-if operation reveals unintended deletions caused by renamed resources or changed scopes. Treat any "Delete" action in what-if output as a mandatory review checkpoint. Automate this check in PR comments so reviewers see infrastructure impact alongside code diffs.

Implementing Azure Bicep: Infrastructure as Code in production

Azure Bicep: Infrastructure as Code delivers tangible value when treated as engineering discipline, not just syntax. Start small — migrate a single resource group or non-production environment first. Establish module conventions and CI validation before tackling production workloads. Invest time in learning the Bicep registry and Azure Verified Modules (AVM) to avoid reinventing networking and identity patterns.

For Nepal-based teams adopting Azure, remember that Bicep's stateless nature means lower operational overhead than Terraform state backends — beneficial when infrastructure expertise is limited. Pair Bicep with Azure Policy for governance, and always version-control parameter files alongside templates.

If you need hands-on guidance implementing Azure Bicep for your application infrastructure, integrating it with existing Laravel or Node.js deployments, or setting up compliant CI/CD pipelines, reach out to discuss your specific requirements. Practical experience beats documentation reading every time.

Frequently Asked Questions

Azure Bicep is a domain-specific language for deploying Azure resources declaratively. It transpiles to ARM JSON but offers cleaner syntax, native validation, and better IDE support, reducing boilerplate by roughly 40% compared to raw ARM templates while maintaining full Azure Resource Manager compatibility.

Yes. The Bicep CLI, VS Code extension, and Azure PowerShell module are completely free open-source tools. You only pay for the Azure resources you provision. There are no licensing fees or usage charges for the Infrastructure as Code tooling itself.

Bicep is Azure-native with day-zero resource support and no state file management. Terraform is multi-cloud with a mature provider ecosystem and state backend. I choose Bicep for pure Azure projects requiring latest API coverage, and Terraform when managing hybrid or multi-vendor infrastructure stacks.

Yes. Run bicep decompile main.json to convert ARM templates to Bicep. The output usually requires manual cleanup since decompilation cannot perfectly infer parameters, variables, or naming conventions. Always validate the generated .bicep file against your original template before using in production pipelines.

Install the Bicep CLI via az bicep install or standalone installer. Add the Bicep VS Code extension for IntelliSense and validation. Ensure Azure CLI 2.60+ or Azure PowerShell Az.Bicep module is configured. Authenticate with az login before running any deployment commands targeting subscriptions or resource groups.

Never hardcode secrets in .bicep files. Reference Azure Key Vault secrets directly using the @secure() decorator on parameters or by passing Key Vault secret references at deployment time. Use managed identities for resource authentication instead of connection strings. Store environment-specific values in pipeline variables, not source control.

Organize by environment and component: modules/ for reusable resource definitions, environments/ for dev/staging/prod parameter files, and main.bicep as the orchestration entry point. Keep modules single-responsibility. Version shared modules via Git tags or Azure Container Registry. This mirrors patterns I use organizing Laravel service classes for maintainability.

Use az deployment group create or New-AzResourceGroupDeployment in pipeline tasks. For GitHub Actions, use azure/bicep-build-action to validate and build artifacts before deploy. Pin Bicep CLI versions in CI to prevent breaking changes. Store parameter files per environment and pass them explicitly rather than relying on defaults during automated releases.

Yes. Publish modules to Azure Container Registry as OCI artifacts using bicep publish. Reference them with br:registry.azurecr.io/bicep/modules/storage:v1.0 syntax. Tag versions semantically. This enables teams to share validated infrastructure components without copying code, similar to how Composer packages work in PHP ecosystems.

Create separate .bicepparam files for each environment (dev.bicepparam, prod.bicepparam). Define common parameters in a base file and override environment-specific values like SKU sizes, replica counts, or naming prefixes. Pass the appropriate parameter file at deploy time. Avoid conditional logic inside modules for environment branching; keep modules generic.

InvalidTemplate errors usually mean missing required properties or wrong API versions; check the error details for exact field names. DeploymentAlreadyExists means a previous deployment failed mid-state; delete the orphaned deployment or use --mode Incremental carefully. Permission errors require checking RBAC on the target scope. Always run bicep build locally before pushing to CI.

Yes. Run bicep build to validate syntax and catch type errors locally. Use what-if deployments with az deployment group what-if to preview changes without applying them. Write unit tests for modules using Pester or bicep-test. Integrate these checks into PR pipelines to catch regressions before merge, just as I run PHPUnit before deploying Laravel apps.

Bicep infers implicit dependencies from property references, so explicit dependsOn is rarely needed. Only add dependsOn when there is a logical ordering not captured by data flow, such as waiting for a policy assignment before creating compliant resources. Overusing dependsOn creates unnecessary serialization and slows deployments. Trust the compiler's dependency graph analysis.

Absolutely. Deploy policy definitions, assignments, and initiatives as Bicep modules alongside application resources. Use management group or subscription scope deployments for governance artifacts. Parameterize policy effects and exclusions per environment. This ensures compliance rules travel with infrastructure code rather than being configured manually in the portal after provisioning.

Developers familiar with Azure Portal can become productive in Bicep within one to two weeks. The syntax resembles TypeScript or C# configuration objects. Start with simple storage accounts or App Services, then progress to networking and identity. Microsoft Learn provides free sandbox labs. Expect initial friction around scoping, loops, and conditionals, but IDE tooling accelerates adoption significantly.

Share this article

Quick Contact Options
Choose how you want to connect me: