Waqas Ahmad — Software Architect & Technical Consultant - Available USA, Europe, Global

Waqas Ahmad — Software Architect & Technical Consultant

Specializing in

Distributed Systems

.NET ArchitectureCloud-Native ArchitectureAzure Cloud EngineeringAPI ArchitectureMicroservices ArchitectureEvent-Driven ArchitectureDatabase Design & Optimization

👋 Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.

Experienced across engineering ecosystems shaped by Microsoft, the Cloud Native Computing Foundation, and the Apache Software Foundation.

Available for remote consulting (USA, Europe, Global) — flexible across EST, PST, GMT & CET.

services
Article

Azure Bicep and Infrastructure as Code: Basics

Bicep for Azure: modules, parameters, deployment. Bicep vs ARM vs Terraform.

services
Read the article

Introduction

This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).

Managing Azure infrastructure by hand does not scale and leaves no audit trail or repeatability. This article explains Bicep—Microsoft’s DSL for Azure IaC—including syntax, modules, parameters, deployment, and when to choose Bicep over ARM or Terraform. For architects and platform engineers, IaC with Bicep gives versioned, reviewable infrastructure and consistent deployments across environments.

For a deeper overview of this topic, explore the full Cloud-Native Architecture guide.

Decision Context

  • System scale: Bicep applies from single-resource scripts to large multi-module deployments; typical use is resource groups or subscriptions with parameterised environments.
  • Team size: Any team that owns Azure infrastructure; often DevOps or platform engineers; developers may author modules with review.
  • Time / budget pressure: IaC pays off when you deploy repeatedly or need consistency; one-off manual setup may be faster short-term but doesn’t scale.
  • Technical constraints: Azure-only; .NET/CI pipelines (Azure DevOps, GitHub Actions) for deployment. Multi-cloud or heavy non-Azure use → consider Terraform.
  • Non-goals: This article does not cover Terraform in depth, or advanced ARM/Bicep patterns (nested deployments, deployment scripts); boundaries are stated where they matter.

What is Infrastructure as Code?

IaC means defining infrastructure (VMs, databases, networks, storage) in code files that can be:

  • Versioned: Track changes in Git
  • Reviewed: Pull requests for infrastructure changes
  • Tested: Validate before deployment
  • Deployed repeatably: Same template, same result
Benefit Description
Consistency Same resources every time
Repeatability Deploy to dev, test, prod with same template
Audit trail Git history shows who changed what
Speed Automated deployments vs manual clicking
Disaster recovery Rebuild infrastructure from code

What is Bicep?

Bicep is a DSL for deploying Azure resources. It compiles to ARM JSON but is:

  • Concise: Less verbose than ARM JSON
  • Readable: Clean syntax, no nested brackets
  • Type-safe: IntelliSense in VS Code
  • Native: First-class support in Azure CLI and Azure DevOps
// Simple storage account in Bicep
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'mystorageaccount'
  location: 'eastus'
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

Equivalent ARM JSON would be 3-4x longer.

Bicep vs ARM vs Terraform

Aspect Bicep ARM JSON Terraform
Syntax Clean DSL Verbose JSON HCL
Azure support Native Native Provider-based
Multi-cloud No No Yes
State management No No Yes (required)
Ecosystem Azure only Azure only Large provider ecosystem
Learning curve Low Medium Medium

When to use Bicep:

  • Azure-only infrastructure
  • Want simplest tooling
  • Already using ARM; want easier syntax

When to use Terraform:

  • Multi-cloud (Azure + AWS + GCP)
  • Need non-Azure providers (Kubernetes, Datadog, etc.)
  • Team already knows Terraform

Bicep syntax basics

Bicep files declare resources (e.g. storage, web app) with a simple, declarative syntax; you can add parameters and variables to avoid repetition and support multiple environments.

Resources

resource <symbolic-name> '<resource-type>@<api-version>' = {
  name: '<resource-name>'
  location: '<location>'
  properties: { ... }
}

Example: Web App with App Service Plan

// main.bicep
param location string = resourceGroup().location
param appName string

// App Service Plan
resource appServicePlan 'Microsoft.Web/serverfarms@2022-09-01' = {
  name: '\${appName}-plan'
  location: location
  sku: {
    name: 'B1'
    tier: 'Basic'
  }
}

// Web App
resource webApp 'Microsoft.Web/sites@2022-09-01' = {
  name: appName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
  }
}

output webAppUrl string = 'https://\${webApp.properties.defaultHostName}'

Parameters and variables

Parameters are inputs to your template. Use for values that change per environment.

// Parameters
param environment string = 'dev'
param location string = 'eastus'
param sku string = 'Standard_LRS'

// Variables (computed from parameters)
var storageAccountName = 'stg\${environment}\${uniqueString(resourceGroup().id)}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageAccountName
  location: location
  sku: { name: sku }
  kind: 'StorageV2'
}

Parameter files for different environments:

// parameters.dev.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environment": { "value": "dev" },
    "sku": { "value": "Standard_LRS" }
  }
}
// parameters.prod.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environment": { "value": "prod" },
    "sku": { "value": "Standard_GRS" }
  }
}

Modules

Modules let you split templates into reusable pieces.

// modules/storage.bicep
param name string
param location string
param sku string = 'Standard_LRS'

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: name
  location: location
  sku: { name: sku }
  kind: 'StorageV2'
}

output id string = storageAccount.id
output connectionString string = 'DefaultEndpointsProtocol=https;AccountName=\${storageAccount.name};AccountKey=\${storageAccount.listKeys().keys[0].value}'
// main.bicep
param location string = resourceGroup().location

module storage 'modules/storage.bicep' = {
  name: 'storageDeployment'
  params: {
    name: 'myappstg'
    location: location
  }
}

output storageId string = storage.outputs.id

Deploying Bicep

Azure CLI

# Deploy to resource group
az deployment group create   --resource-group my-rg   --template-file main.bicep   --parameters parameters.dev.json

# What-if (preview changes)
az deployment group what-if   --resource-group my-rg   --template-file main.bicep   --parameters parameters.prod.json

PowerShell

New-AzResourceGroupDeployment `
  -ResourceGroupName my-rg `
  -TemplateFile main.bicep `
  -TemplateParameterFile parameters.dev.json

CI/CD with Bicep

Azure DevOps

# azure-pipelines.yml
trigger:
  - main

pool:
  vmImage: ubuntu-latest

steps:
  - task: AzureCLI@2
    inputs:
      azureSubscription: 'my-service-connection'
      scriptType: bash
      scriptLocation: inlineScript
      inlineScript: |
        az deployment group create           --resource-group my-rg           --template-file infra/main.bicep           --parameters infra/parameters.prod.json

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy Infrastructure

on:
  push:
    branches: [main]
    paths: ['infra/**']

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: azure/login@v1
        with:
          creds: \${{ secrets.AZURE_CREDENTIALS }}
      
      - name: Deploy Bicep
        run: |
          az deployment group create             --resource-group my-rg             --template-file infra/main.bicep             --parameters infra/parameters.prod.json

Enterprise best practices

1. Use modules. Split large templates into reusable modules (storage, networking, app).

2. Use parameter files per environment. parameters.dev.json, parameters.prod.json.

3. Never commit secrets. Use Key Vault references or pipeline variables.

4. Use what-if before deploying. Review changes before applying.

5. Version templates in Git. Track all changes; require PRs for infrastructure.

6. Use Azure Policy. Enforce naming conventions, allowed SKUs, required tags.

7. Validate in CI. Use az bicep build to catch errors before deployment.

8. Document modules. Add comments; create a README for your infra folder.

Common issues

Issue Cause Fix
Deployment fails Syntax error, invalid API version Run az bicep build locally
Resource already exists Name collision Use unique naming (e.g. uniqueString())
Drift Manual portal changes Deploy only from code; use Policy
Secrets in repo Connection strings committed Use Key Vault; pipeline variables
Large single file Hard to maintain Split into modules
Wrong API version Deprecated or missing properties Check Azure docs for current version

Summary

Bicep is the recommended way to write Azure IaC: concise, type-safe, and integrated with Azure CLI and DevOps; use modules, parameter files, and CI/CD for repeatable deployments. Getting this right gives you versioned infrastructure and consistent environments instead of drift and manual fixes. Next, parameterise one existing resource group or app and add a single Bicep module to your pipeline so the pattern is in place before scaling out.


Position & Rationale

I use Bicep for Azure-only infrastructure when we want the simplest path: native tooling, no state file, compiles to ARM. I prefer modules for reusability and parameter files per environment so we don’t duplicate templates. I deploy via CI/CD (Azure DevOps or GitHub Actions) so every change is versioned and repeatable. I choose Terraform when we need multi-cloud or non-Azure providers; I don’t use Bicep for that. I avoid secrets in repo—use Key Vault references or pipeline variables. I reject deploying from the portal once IaC is in place; drift makes the codebase a lie. For small or one-off Azure setups, a single Bicep file may be enough; for enterprise I use modules, naming (e.g. uniqueString()), and Policy to prevent manual drift.


Trade-Offs & Failure Modes

  • What this sacrifices: Bicep is Azure-only; no state management (unlike Terraform)—Azure Resource Manager is the source of truth. Learning and tooling are Azure-centric.
  • Where it degrades: When people deploy manually and drift accumulates; when secrets are committed; when a single huge file is unmaintainable; when API versions are wrong or deprecated.
  • How it fails when misapplied: Using Bicep for multi-cloud—wrong tool. Committing connection strings or keys. No parameterisation so dev/prod differ by hand. Skipping what-if or validation so broken templates reach the pipeline.
  • Early warning signs: “We’re not sure what’s actually deployed”; “someone changed it in the portal”; “our Bicep file is 2000 lines”; “deployments keep failing with name conflicts.”

What Most Guides Miss

Guides often show a single file and skip modules and parameter files—in practice, reusability and environment separation (dev/test/prod) require both. Drift (manual changes in the portal) is underplayed; without Policy or discipline, the code and reality diverge. Secrets—Key Vault references vs pipeline variables—and naming (uniqueString(), naming conventions) to avoid collisions are rarely stressed. what-if and validation (az bicep build) before merge are easy to skip; they catch errors early.


Decision Framework

  • If you’re on Azure only and want simple IaC → Use Bicep; modules and parameters; deploy via CI/CD.
  • If you need multi-cloud or non-Azure providers → Use Terraform; Bicep is not the right fit.
  • If you have multiple environments → Use parameter files per environment; never hardcode env-specific values in the template.
  • If you have secrets → Key Vault references or pipeline variables; never commit secrets.
  • If drift is a problem → Deploy only from code; use Azure Policy to restrict manual creation/changes where possible.
  • If the template is huge → Split into modules; one concern per file.

You can also explore more patterns in the Cloud-Native Architecture resource page.

Key Takeaways

  • Bicep for Azure-only IaC; modules and parameter files for reusability and environments; deploy via CI/CD; no secrets in repo.
  • Use what-if and az bicep build before merge; use uniqueString() and naming conventions to avoid collisions; prevent drift by deploying only from code.
  • Revisit Bicep vs Terraform when multi-cloud or non-Azure needs appear.

When I Would Use This Again — and When I Wouldn’t

I would use Bicep again for any Azure-only infrastructure that we version and deploy via pipeline—resource groups, app services, storage, SQL, etc. I’d use modules and parameters from day one. I wouldn’t use Bicep for multi-cloud or when we need non-Azure providers (e.g. Kubernetes, Datadog)—Terraform is the right choice there. I wouldn’t deploy from the portal once we have Bicep; I’d treat drift as a failure. For a one-off lab or throwaway env, a single Bicep file might be enough; for anything that outlives a sprint, I’d add CI/CD and parameter files. If the team can’t own Git and pipelines, I’d still write Bicep and treat automation as a prerequisite for production.


services
Frequently Asked Questions

Frequently Asked Questions

What is IaC?

Infrastructure as Code means defining infrastructure in code files that are versioned, reviewed, and deployed repeatably.

What is Bicep?

Bicep is Microsoft’s DSL for Azure IaC. It compiles to ARM JSON but is easier to write and read.

Bicep vs ARM JSON?

Bicep is concise and readable. ARM JSON is verbose. Bicep compiles to ARM, so you get the same deployment.

Bicep vs Terraform?

Use Bicep for Azure-only. Use Terraform for multi-cloud or non-Azure providers.

How do I deploy Bicep?

az deployment group create --template-file main.bicep --parameters parameters.json

What are modules?

Reusable Bicep files that you reference from your main template. Split templates into logical units.

How do I handle secrets?

Never commit secrets. Use Key Vault references or inject via pipeline variables.

What is what-if?

Preview command that shows what changes will be made without actually deploying.

How do I validate Bicep?

az bicep build --file main.bicep compiles and validates syntax.

Can I convert ARM to Bicep?

Yes. az bicep decompile --file template.json converts ARM JSON to Bicep.

How do I reference existing resources?

Use existing keyword: resource existingStorage 'Microsoft.Storage/storageAccounts@2023-01-01' existing = { name: 'mystg' }

What is uniqueString?

Function that generates a deterministic hash. Use for unique resource names.

How do I output values?

output storageId string = storageAccount.id outputs the resource ID.

Can I use Bicep with Azure DevOps?

Yes. Use Azure CLI task or dedicated Bicep task in your pipeline.

What is Azure Policy?

Service that enforces rules on Azure resources. Use to ensure IaC compliance (naming, tags, allowed SKUs).

services
Related Guides & Resources

services
Related services