Background
Are you operating in a complex Azure environment which contains multiple independent microservices and you're wondering how to model environment with infrastructure as a code? This blog post is for you.
This blog post shows how to structure/model infrastructure creation effectively with Bicep Modules in a environment which contains multiple independent microservices. This presented approach assumes that whole Azure infrastructure is created via single centralized Azure DevOps pipeline. Microservice specific CI/CD pipelines are only responsible for deploying the application not Azure infrastructure.
Centralized Azure infrastructure creation pipeline enables easily re-usability of Bicep Modules and makes microservice specific pipelines more light and faster to execute.
Another option is to a create Azure infrastructure in multiple smaller pieces in a microservice specific Azure DevOps pipelines. That approach requires some extra work and basically Azure Container Registry to effectively share Bicep Modules between multiple Azure DevOps pipelines/repositories. You can read more about this from my previous blog post.
Bicep module hierarchy
I have divided Bicep modules to four hierarchy levels to enable powerful re-usability and modularity. I'll go through these hierarchy levels from down to top in the below sections.
Like said earlier this presented approach assumes that whole Azure infrastructure is created via single centralized Azure DevOps pipeline and all Bicep Modules are in the same repository.
Pre-configured Resources
Pre-configured Resources (Bicep Modules) are prepared and configured according to company's security and compliance requirements. For example pre-configured App Service Module can force that FTP state is disabled and HTTP 2.0 & HTTPS are always enabled. Each module is independent and Azure resource specific. This model also enables to you can force specific resource naming policy efficiently. Pre-configured Resource modules are the foundation for Productized Resource Collections which I explain next.
Sample Pre-configured App Service Module
This App Service Module forces automatically that FTP state is disabled and HTTP 2.0 & HTTPS are enabled.
param location string
param tags object
param servicePrefix string
param abbreviation string
param appServicePlanId string
param applicationInsightsConnectionString string
param resourceToken string
resource appService 'Microsoft.Web/sites@2022-03-01' = {
name: '${abbreviation}${servicePrefix}-${location}-${resourceToken}'
location: location
tags: tags
properties: {
serverFarmId: appServicePlanId
siteConfig: {
ftpsState: 'Disabled'
http20Enabled: true
netFrameworkVersion: 'v6.0'
}
httpsOnly: true
}
identity: {
type: 'SystemAssigned'
}
resource appSettings 'config' = {
name: 'appsettings'
properties: {
SCM_DO_BUILD_DURING_DEPLOYMENT: 'false'
APPLICATIONINSIGHTS_CONNECTION_STRING: applicationInsightsConnectionString
ASPNETCORE_ENVIRONMENT: 'Development'
}
}
}
output appServiceId string = appService.id
output defaultHostName string = appService.properties.defaultHostName
Productized Resource Collections
From Bicep Module hierarchy point of view Productized Resource Collection modules are built top of Pre-configured Resource modules.
Productized Resource Collection modules are always created for a specific use case. If your environment has multiple microservices which contains API+Database, it's beneficial to create Productized Resource Collection module for this purpose. With this module template you can easily create new microservices to you environment in the future which increase productivity and make maintenance more straightforward.
Typical resource collections which can be productized:
- Microservice 1: App Service API + Application Insights + App Service Plan + Azure SQL database
- Microservice 2: App Service API + Application Insights + App Service Plan + CosmosDB database
- Publish Subscribe: Azure Service Bus + Azure Function
Sample Productized Resource Collection for Publish Subscribe type of scenario
targetScope='resourceGroup'
param location string = resourceGroup().location
param tags object
param servicePrefix string
param resourceToken string
module appServicePlan '../../preconfigured/appservice/appServicePlan.bicep' = {
name: 'appServicePlan-resources'
params: {
location: location
tags: tags
servicePrefix: servicePrefix
sku: 'B1'
kind: 'app,windows'
resourceToken: resourceToken
}
}
module logAnalyticsWorkspace '../../preconfigured/logging/logAnalyticsWorkspace.bicep' = {
name: 'logAnalyticsWorkspace-resources'
params: {
location: location
tags: tags
servicePrefix: servicePrefix
resourceToken: resourceToken
}
}
module applicationInsightsResources '../../preconfigured/logging/applicationInsights.bicep' = {
name: 'applicationinsights-resources'
params: {
location: location
tags: tags
workspaceId: logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId
servicePrefix: servicePrefix
resourceToken: resourceToken
}
}
module storageAccount '../../preconfigured/storage/storageAccount.bicep' = {
name: 'storageAccount-resources'
params: {
location: location
tags: tags
servicePrefix: servicePrefix
resourceToken: resourceToken
storageAccountType: 'Standard_LRS'
}
}
module azureFunction '../../preconfigured/functions/azureFunction.bicep' = {
name: 'azureFunction-resources'
dependsOn:[
appServicePlan
logAnalyticsWorkspace
applicationInsightsResources
storageAccount
]
params: {
location: location
tags: tags
servicePrefix: servicePrefix
resourceToken: resourceToken
serverfarmId: appServicePlan.outputs.appServicePlanId
instrumentationKey: applicationInsightsResources.outputs.applicationInsightsInstrumentationKey
}
}
module serviceBus '../../preconfigured/messaging/serviceBus.bicep' = {
name: 'serviceBus-resources'
dependsOn:[
azureFunction
]
params: {
location: location
tags: tags
servicePrefix: servicePrefix
resourceToken: resourceToken
sku: 'Standard'
}
}
Core Infra
Core Infra determines each microservice and what Productized Resource Collection or Pre-Configured Resource modules are used to create that specific microservice. Core Infra is not only for microservices and it should determine also other shared resources.
Typically Core Infra contains multiple service specific Bicep Modules. From maintenance point of view it's easier to separate each service to own Bicep module file.
├── core
│ ├── microservices
│ │ ├── invoicing.bicep
│ │ ├── ordering.bicep
│ │ ├── products.bicep
│ ├── shared
│ │ ├── configuration.bicep
Sample Core Infra
This sample determines a Products microservice with SQL database and adds also Publish Subscribe capabilities. This approach enables to utilize multiple different Productized Resource Collections easily.
targetScope='resourceGroup'
param tags object
param location string
var microserviceName = 'products'
module microservicesWithSqlDatabase '../../modules/productized/microservices/microserviceSqlDatabase.bicep' = {
name:'microservicesWithSqlDatabase'
params:{
location: location
resourceToken: toLower(uniqueString(subscription().id, microserviceName, location))
servicePrefix: microserviceName
tags: tags
}
}
module microservicesWithPubSub '../../modules/productized/messaging/publishSubscribeServicebus.bicep' = {
name:'microservicesWithPubSub'
dependsOn:[
microservicesWithSqlDatabase
]
params:{
location: location
resourceToken: toLower(uniqueString(subscription().id, microserviceName, location))
servicePrefix: microserviceName
tags: tags
}
}
Main Bicep
Main Bicep orchestrates creation of the Azure infrastructure which contains multiple microservices and shared resources.
//This Main Bicep Module orchestrates creation of the Azure infrastructure which contains multiple microservices
targetScope='subscription'
param tags object
param location string
@secure()
param sqlAdminLogin string
@secure()
param sqlAdminPassword string
var resourceGroupNames = [
'invoicing'
'ordering'
'products'
'config'
]
//This creates resource groups for each microservice
module resourceGroups './modules/preconfigured/resourceGroup/resourceGroup.bicep' = [for resourceGroup in resourceGroupNames: {
name: resourceGroup
params:{
location: location
name: 'rg-${resourceGroup}'
tags: tags
}
}]
//Shared services
module configuration './modules/core/shared/configuration.bicep' = {
dependsOn:[
resourceGroups
]
scope: resourceGroup('rg-config')
name: 'configuration'
params:{
location: location
tags: tags
}
}
//Invoicing Microservice
module invoicing './modules/core/microservices/invoicing.bicep' = {
dependsOn:[
resourceGroups
]
scope: resourceGroup('rg-invoicing')
name: 'invoicing'
params:{
location: location
tags: tags
sqlAdminLogin:sqlAdminLogin
sqlAdminPassword:sqlAdminPassword
}
}
//Products Microservice
module products './modules/core/microservices/products.bicep' = {
dependsOn:[
resourceGroups
]
scope: resourceGroup('rg-products')
name: 'products'
params:{
location: location
tags: tags
sqlAdminLogin:sqlAdminLogin
sqlAdminPassword:sqlAdminPassword
}
}
//Ordering Microservice
module ordering './modules/core/microservices/ordering.bicep' = {
dependsOn:[
resourceGroups
]
scope: resourceGroup('rg-ordering')
name: 'ordering'
params:{
location: location
tags: tags
sqlAdminLogin:sqlAdminLogin
sqlAdminPassword:sqlAdminPassword
}
}
Full Bicep Module hierarchy
Sample Azure infrastructure creation with Bicep modules can be found from my GitHub. Each Bicep Module hierarchy level has own folder in sample solution.
├── preconfigured
│ ├── appservice
│ │ ├── appservice.bicep
│ ├── configuration
│ │ ├── keyvault.bicep
│ ├── messaging
│ │ ├── servicebus.bicep
├── productized
│ ├── microservices
│ │ ├── microserviceSqlDatabase.bicep
│ ├── messaging
│ │ ├── publishSubscribeServicebus.bicep
├── core
│ ├── microservices
│ │ ├── invoicing.bicep
│ │ ├── ordering.bicep
│ │ ├── products.bicep
│ ├── shared
│ │ ├── configuration.bicep