Back to Blog
7 min read

Bicep Modules for Modular Azure Infrastructure

Introduction

Bicep is Azure’s domain-specific language for deploying Azure resources declaratively. As a transparent abstraction over ARM templates, Bicep offers cleaner syntax, better modularity, and improved developer experience. This guide focuses on creating modular, reusable Bicep templates.

Bicep Basics

Simple Resource Definition

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

var namePrefix = 'myapp-${environment}'

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-02-01' = {
  name: 'st${replace(namePrefix, '-', '')}${uniqueString(resourceGroup().id)}'
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    minimumTlsVersion: 'TLS1_2'
    supportsHttpsTrafficOnly: true
    accessTier: 'Hot'
  }
}

output storageAccountName string = storageAccount.name
output storageAccountId string = storageAccount.id

Creating Modules

App Service Module

// modules/appService.bicep
@description('Name prefix for resources')
param namePrefix string

@description('Location for resources')
param location string

@description('Environment (dev, staging, prod)')
@allowed([
  'dev'
  'staging'
  'prod'
])
param environment string

@description('Application Insights instrumentation key')
param appInsightsInstrumentationKey string = ''

@description('Additional app settings')
param additionalAppSettings array = []

var isProd = environment == 'prod'
var skuName = isProd ? 'S1' : 'B1'
var skuTier = isProd ? 'Standard' : 'Basic'

resource appServicePlan 'Microsoft.Web/serverfarms@2021-02-01' = {
  name: 'asp-${namePrefix}'
  location: location
  kind: 'linux'
  sku: {
    name: skuName
    tier: skuTier
  }
  properties: {
    reserved: true
  }
}

var baseAppSettings = [
  {
    name: 'ASPNETCORE_ENVIRONMENT'
    value: isProd ? 'Production' : 'Development'
  }
  {
    name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
    value: appInsightsInstrumentationKey
  }
]

resource webApp 'Microsoft.Web/sites@2021-02-01' = {
  name: 'app-${namePrefix}'
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|6.0'
      alwaysOn: isProd
      http20Enabled: true
      minTlsVersion: '1.2'
      appSettings: concat(baseAppSettings, additionalAppSettings)
    }
  }
}

// Staging slot for production
resource stagingSlot 'Microsoft.Web/sites/slots@2021-02-01' = if (isProd) {
  parent: webApp
  name: 'staging'
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    serverFarmId: appServicePlan.id
    httpsOnly: true
    siteConfig: {
      linuxFxVersion: 'DOTNETCORE|6.0'
      alwaysOn: true
    }
  }
}

output appServiceName string = webApp.name
output appServiceUrl string = 'https://${webApp.properties.defaultHostName}'
output appServicePrincipalId string = webApp.identity.principalId
output appServicePlanId string = appServicePlan.id

SQL Database Module

// modules/sqlDatabase.bicep
@description('Name prefix for resources')
param namePrefix string

@description('Location for resources')
param location string

@description('Environment (dev, staging, prod)')
param environment string

@description('SQL administrator login')
@secure()
param adminLogin string

@description('SQL administrator password')
@secure()
param adminPassword string

@description('Azure AD admin object ID')
param aadAdminObjectId string = ''

@description('Azure AD admin login name')
param aadAdminLogin string = ''

var isProd = environment == 'prod'

resource sqlServer 'Microsoft.Sql/servers@2021-05-01-preview' = {
  name: 'sql-${namePrefix}'
  location: location
  properties: {
    administratorLogin: adminLogin
    administratorLoginPassword: adminPassword
    version: '12.0'
    minimalTlsVersion: '1.2'
  }
}

// Azure AD administrator
resource aadAdmin 'Microsoft.Sql/servers/administrators@2021-05-01-preview' = if (!empty(aadAdminObjectId)) {
  parent: sqlServer
  name: 'ActiveDirectory'
  properties: {
    administratorType: 'ActiveDirectory'
    login: aadAdminLogin
    sid: aadAdminObjectId
    tenantId: tenant().tenantId
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2021-05-01-preview' = {
  parent: sqlServer
  name: 'sqldb-${namePrefix}'
  location: location
  sku: {
    name: isProd ? 'S1' : 'Basic'
    tier: isProd ? 'Standard' : 'Basic'
  }
  properties: {
    collation: 'SQL_Latin1_General_CP1_CI_AS'
    maxSizeBytes: isProd ? 53687091200 : 2147483648
    zoneRedundant: isProd
  }
}

// Allow Azure services
resource firewallRule 'Microsoft.Sql/servers/firewallRules@2021-05-01-preview' = {
  parent: sqlServer
  name: 'AllowAzureServices'
  properties: {
    startIpAddress: '0.0.0.0'
    endIpAddress: '0.0.0.0'
  }
}

output sqlServerName string = sqlServer.name
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
output databaseName string = sqlDatabase.name
output connectionString string = 'Server=tcp:${sqlServer.properties.fullyQualifiedDomainName},1433;Database=${sqlDatabase.name};'

Key Vault Module

// modules/keyVault.bicep
@description('Name prefix for resources')
param namePrefix string

@description('Location for resources')
param location string

@description('Environment (dev, staging, prod)')
param environment string

@description('Principal IDs to grant secret access')
param secretAccessPrincipalIds array = []

@description('Secrets to create')
param secrets array = []

var isProd = environment == 'prod'

resource keyVault 'Microsoft.KeyVault/vaults@2021-06-01-preview' = {
  name: 'kv-${namePrefix}'
  location: location
  properties: {
    sku: {
      family: 'A'
      name: 'standard'
    }
    tenantId: tenant().tenantId
    enableSoftDelete: true
    softDeleteRetentionInDays: 7
    enablePurgeProtection: isProd
    accessPolicies: [for principalId in secretAccessPrincipalIds: {
      tenantId: tenant().tenantId
      objectId: principalId
      permissions: {
        secrets: [
          'get'
          'list'
        ]
      }
    }]
  }
}

// Create secrets
resource keyVaultSecrets 'Microsoft.KeyVault/vaults/secrets@2021-06-01-preview' = [for secret in secrets: {
  parent: keyVault
  name: secret.name
  properties: {
    value: secret.value
  }
}]

output keyVaultName string = keyVault.name
output keyVaultUri string = keyVault.properties.vaultUri
output keyVaultId string = keyVault.id

Main Deployment Using Modules

// main.bicep
targetScope = 'subscription'

@description('Environment name')
@allowed([
  'dev'
  'staging'
  'prod'
])
param environment string

@description('Location for resources')
param location string = 'australiaeast'

@description('SQL admin username')
@secure()
param sqlAdminLogin string

@description('SQL admin password')
@secure()
param sqlAdminPassword string

var namePrefix = 'myapp-${environment}'

// Resource Group
resource rg 'Microsoft.Resources/resourceGroups@2021-04-01' = {
  name: 'rg-${namePrefix}'
  location: location
  tags: {
    Environment: environment
    ManagedBy: 'Bicep'
  }
}

// Application Insights
module appInsights 'modules/appInsights.bicep' = {
  name: 'appInsights'
  scope: rg
  params: {
    namePrefix: namePrefix
    location: location
  }
}

// App Service
module appService 'modules/appService.bicep' = {
  name: 'appService'
  scope: rg
  params: {
    namePrefix: namePrefix
    location: location
    environment: environment
    appInsightsInstrumentationKey: appInsights.outputs.instrumentationKey
  }
}

// SQL Database
module sqlDatabase 'modules/sqlDatabase.bicep' = {
  name: 'sqlDatabase'
  scope: rg
  params: {
    namePrefix: namePrefix
    location: location
    environment: environment
    adminLogin: sqlAdminLogin
    adminPassword: sqlAdminPassword
  }
}

// Key Vault
module keyVault 'modules/keyVault.bicep' = {
  name: 'keyVault'
  scope: rg
  params: {
    namePrefix: namePrefix
    location: location
    environment: environment
    secretAccessPrincipalIds: [
      appService.outputs.appServicePrincipalId
    ]
    secrets: [
      {
        name: 'SqlConnectionString'
        value: '${sqlDatabase.outputs.connectionString}Authentication=Active Directory Managed Identity;'
      }
    ]
  }
}

// Outputs
output resourceGroupName string = rg.name
output appServiceUrl string = appService.outputs.appServiceUrl
output keyVaultUri string = keyVault.outputs.keyVaultUri
output sqlServerFqdn string = sqlDatabase.outputs.sqlServerFqdn

Application Insights Module

// modules/appInsights.bicep
param namePrefix string
param location string

resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2021-06-01' = {
  name: 'log-${namePrefix}'
  location: location
  properties: {
    sku: {
      name: 'PerGB2018'
    }
    retentionInDays: 30
  }
}

resource appInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: 'appi-${namePrefix}'
  location: location
  kind: 'web'
  properties: {
    Application_Type: 'web'
    WorkspaceResourceId: logAnalytics.id
  }
}

output instrumentationKey string = appInsights.properties.InstrumentationKey
output connectionString string = appInsights.properties.ConnectionString
output appInsightsId string = appInsights.id

Conditional Deployments

// modules/networking.bicep
param namePrefix string
param location string
param deployPrivateEndpoints bool = false

resource vnet 'Microsoft.Network/virtualNetworks@2021-03-01' = {
  name: 'vnet-${namePrefix}'
  location: location
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.0.0/16'
      ]
    }
    subnets: [
      {
        name: 'snet-app'
        properties: {
          addressPrefix: '10.0.1.0/24'
          delegations: [
            {
              name: 'delegation'
              properties: {
                serviceName: 'Microsoft.Web/serverFarms'
              }
            }
          ]
        }
      }
      {
        name: 'snet-data'
        properties: {
          addressPrefix: '10.0.2.0/24'
          privateEndpointNetworkPolicies: 'Disabled'
        }
      }
    ]
  }
}

// Only deploy private DNS zone if private endpoints are enabled
resource privateDnsZone 'Microsoft.Network/privateDnsZones@2020-06-01' = if (deployPrivateEndpoints) {
  name: 'privatelink.database.windows.net'
  location: 'global'
}

resource privateDnsZoneLink 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2020-06-01' = if (deployPrivateEndpoints) {
  parent: privateDnsZone
  name: '${vnet.name}-link'
  location: 'global'
  properties: {
    virtualNetwork: {
      id: vnet.id
    }
    registrationEnabled: false
  }
}

output vnetId string = vnet.id
output appSubnetId string = vnet.properties.subnets[0].id
output dataSubnetId string = vnet.properties.subnets[1].id

Deployment Commands

# Deploy to subscription scope
az deployment sub create \
  --location australiaeast \
  --template-file main.bicep \
  --parameters environment=dev \
  --parameters sqlAdminLogin=adminuser \
  --parameters sqlAdminPassword='SecureP@ssw0rd!'

# What-if deployment
az deployment sub what-if \
  --location australiaeast \
  --template-file main.bicep \
  --parameters environment=prod

# Deploy with parameter file
az deployment sub create \
  --location australiaeast \
  --template-file main.bicep \
  --parameters @parameters.prod.json

Parameter File

// parameters.prod.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "environment": {
      "value": "prod"
    },
    "location": {
      "value": "australiaeast"
    },
    "sqlAdminLogin": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{kv}"
        },
        "secretName": "sqlAdminLogin"
      }
    },
    "sqlAdminPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/{sub-id}/resourceGroups/{rg}/providers/Microsoft.KeyVault/vaults/{kv}"
        },
        "secretName": "sqlAdminPassword"
      }
    }
  }
}

Conclusion

Bicep modules enable clean, maintainable Azure infrastructure code. By separating concerns into modules, you create reusable components that can be shared across projects. The clear syntax, type checking, and VS Code integration make Bicep an excellent choice for Azure-native infrastructure as code.

References

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.