Skip to content

Bicep IaC Modules

Microtec ERP infrastructure is defined as code using Azure Bicep. The infrastructure is subscription-scoped (deploying multiple resource groups), organized into reusable modules per resource type, and driven by a PowerShell parameter-builder script that converts services-config.json into Bicep parameters.


Repository Location

All Bicep files are under Devops/azure/infrastructure/:

Devops/azure/infrastructure/
├── main.bicep                    # Subscription-scoped entry point
├── main.bicepparam               # Default parameters (overridden per env)
├── modules/
│   ├── resource-groups.bicep     # Creates 8 RGs per environment
│   ├── networking.bicep          # VNet, subnets, NSGs, NAT Gateway
│   ├── container-apps.bicep      # Public + Private CAE, container apps
│   ├── key-vault.bicep           # Key Vault + RBAC assignments
│   ├── container-registry.bicep  # ACR per environment
│   ├── redis.bicep               # Azure Cache for Redis
│   ├── service-bus.bicep         # Azure Service Bus namespace + topics
│   ├── storage-backend.bicep     # Backend blob storage (attachments)
│   ├── storage-frontend.bicep    # Frontend static hosting storage
│   ├── static-web-apps.bicep     # One SWA per Angular app (9 total)
│   ├── managed-identity.bicep    # User-assigned MI + role assignments
│   ├── front-door.bicep          # AFD profile, origins, routes, WAF
│   ├── log-analytics.bicep       # Log Analytics Workspace
│   └── dns.bicep                 # Private DNS zones
└── scripts/
    └── infra/
        └── Build-BicepParams.ps1  # services-config.json → Bicep params

main.bicep — Subscription-Scoped Entry Point

bicep
targetScope = 'subscription'

param environment   string  // dev | stage | preprod | uat | prod
param location      string = 'uksouth'
param resourceConfig object  // built from services-config.json

// ── Resource Group Creation ──────────────────────────────────────
module rgs 'modules/resource-groups.bicep' = {
  name: 'rgs-${environment}'
  params: {
    environment: environment
    location:    location
  }
}

// ── Networking (must deploy before apps) ──────────────────────────
module networking 'modules/networking.bicep' = {
  name: 'networking-${environment}'
  scope: resourceGroup(rgs.outputs.networkRgName)
  dependsOn: [rgs]
  params: {
    environment:   environment
    vnetCidr:      resourceConfig.vnetCidr  // e.g. '10.0.0.0/16' for dev
    location:      location
  }
}

// ── Managed Identity ──────────────────────────────────────────────
module identity 'modules/managed-identity.bicep' = {
  name: 'identity-${environment}'
  scope: resourceGroup(rgs.outputs.appsRgName)
  dependsOn: [rgs]
  params: { environment: environment, location: location }
}

// ── Data Layer ────────────────────────────────────────────────────
module redis 'modules/redis.bicep' = {
  name: 'redis-${environment}'
  scope: resourceGroup(rgs.outputs.dataRgName)
  dependsOn: [networking]
  params: {
    environment:  environment
    location:     location
    subnetId:     networking.outputs.dataSubnetId
  }
}

module serviceBus 'modules/service-bus.bicep' = {
  name: 'asb-${environment}'
  scope: resourceGroup(rgs.outputs.dataRgName)
  dependsOn: [rgs]
  params: { environment: environment, location: location }
}

// ── Container Registry ────────────────────────────────────────────
module acr 'modules/container-registry.bicep' = {
  name: 'acr-${environment}'
  scope: resourceGroup(rgs.outputs.storageRgName)
  dependsOn: [rgs]
  params: {
    environment:       environment
    location:          location
    managedIdentityId: identity.outputs.identityId
  }
}

// ── Container Apps Environments ───────────────────────────────────
module cae 'modules/container-apps.bicep' = {
  name: 'cae-${environment}'
  scope: resourceGroup(rgs.outputs.appsRgName)
  dependsOn: [networking, identity, acr, redis, serviceBus]
  params: {
    environment:    environment
    location:       location
    services:       resourceConfig.services
    // ... other params
  }
}

// Additional modules: key-vault, front-door, storage, swa, dns, monitoring

8 Resource Groups per Environment

The resource-groups.bicep module creates a consistent set of RGs:

RG Name PatternPurpose
mic-erp-be-{env}-network-rgVNet, subnets, NSGs, NAT Gateway
mic-erp-be-{env}-apps-rgContainer Apps Environments, container apps
mic-erp-be-{env}-data-rgRedis, Service Bus, private endpoints
mic-erp-be-{env}-storage-rgACR, backend blob storage
mic-erp-be-{env}-keyvault-rgKey Vault
mic-erp-be-{env}-monitor-rgLog Analytics, Application Insights
mic-erp-fr-{env}-storage-rgFrontend storage (MFE blobs)
mic-erp-fr-{env}-swa-rgStatic Web Apps (9 per env)

Shared SQL RG

mic-backend-shared-sql-rg is NOT created by per-environment Bicep. It is a shared, manually managed resource group. Do NOT add it to the deprovision.yml teardown pipeline.


VNet CIDR Allocation

The vnetCidr parameter is set per environment in Build-BicepParams.ps1:

EnvironmentVNet CIDR
dev10.0.0.0/16
stage10.1.0.0/16
preprod10.6.0.0/16
uat10.5.0.0/16
production10.2.0.0/16

Build-BicepParams.ps1 — Config to Parameters

This PowerShell script is the bridge between services-config.json (the master service configuration) and Bicep deployment parameters. It runs as the first step in every infrastructure pipeline.

Location: Devops/azure/scripts/infra/Build-BicepParams.ps1

powershell
param(
    [string]$Environment,
    [string]$ConfigPath = "Devops/azure/config/container-backend/services-config.json",
    [string]$OutputPath = "infra.bicepparam"
)

$config = Get-Content $ConfigPath | ConvertFrom-Json
$envConfig = $config.environments.$Environment

# Build Bicep parameter file
$params = @{
    environment  = $Environment
    location     = $envConfig.location
    vnetCidr     = $envConfig.vnetCidr
    redisSku     = $envConfig.redisSku
    acrSku       = $envConfig.acrSku
    resourceConfig = @{
        services = $config.services | ForEach-Object {
            @{
                name       = $_.name
                image      = "$($_.repository):$($envConfig.imageTag)"
                minReplicas = $_.scaling.$Environment.minReplicas
                maxReplicas = $_.scaling.$Environment.maxReplicas
                cpu        = $_.resources.cpu
                memory     = $_.resources.memory
                envVars    = $_.envVars.$Environment
            }
        }
        vnetCidr = $envConfig.vnetCidr
    }
}

$params | ConvertTo-Json -Depth 10 | Set-Content $OutputPath
Write-Host "Generated: $OutputPath for environment: $Environment"

Usage in pipeline:

yaml
- task: PowerShell@2
  displayName: 'Build Bicep Parameters'
  inputs:
    filePath: 'Devops/azure/scripts/infra/Build-BicepParams.ps1'
    arguments: '-Environment $(environment)'

- task: AzureCLI@2
  displayName: 'Deploy Bicep Infrastructure'
  inputs:
    script: |
      az deployment sub create \
        --location uksouth \
        --template-file Devops/azure/infrastructure/main.bicep \
        --parameters infra.bicepparam \
        --name "infra-$(environment)-$(Build.BuildId)"

Idempotency

All Bicep deployments are idempotent (ARM complete vs incremental mode):

  • Default mode: incremental — only changes are applied, existing resources not deleted
  • deprovision.yml uses explicit az group delete commands (not Bicep complete mode)

Re-running a Bicep deployment on an unchanged environment is safe and produces no changes.


What-If Preview

Before applying infrastructure changes, the infra-only.yml pipeline runs a what-if preview:

bash
az deployment sub what-if \
  --location uksouth \
  --template-file Devops/azure/infrastructure/main.bicep \
  --parameters infra.bicepparam

This outputs a diff of all planned resource changes without applying them. Review this output in the pipeline log before proceeding.


Module Output Reference

Key outputs passed between modules:

ModuleOutput NameUsed By
resource-groupsappsRgName, dataRgName, networkRgNameAll subsequent modules
networkingpublicAppsSubnetId, privateAppsSubnetId, dataSubnetIdCAE, Redis, Service Bus
managed-identityidentityId, identityPrincipalIdACR, Key Vault, Container Apps
container-registryacrLoginServer, acrIdContainer Apps
key-vaultkeyVaultUri, keyVaultIdContainer Apps (secret refs)
container-appspublicCaeFqdn, privateCaeFqdnFront Door origins

Internal Documentation — Microtec Platform Team