How to model Azure infrastructure creation in a microservice environment with Bicep Modules?
Background
Are you operating in a complex Azure environment that contains multiple independent microservices and you're wondering how to model the 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 an environment that contains multiple independent microservices. This presented approach assumes that the whole Azure infrastructure is created via a 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 easy re-usability of Bicep Modules and makes microservice-specific pipelines light and faster to execute.
Another option is to create Azure infrastructure in multiple smaller pieces in 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 into 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.
As said earlier this presented approach assumes that the whole Azure infrastructure is created via a 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 the company's security and compliance requirements. For example, a pre-configured App Service Module can force that the 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 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 the 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 the Bicep Module hierarchy point of view, Productized Resource Collection modules are built on top of Pre-configured Resource modules.
Productized Resource Collection modules are always created for a specific use case. If your environment has multiple microservices that contain API+Database, it's beneficial to create a Productized Resource Collection module for this purpose. With this module template, you can easily create new microservices to your environment in the future which increases productivity and makes maintenance more straightforward.
Typical resource collections that 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 a maintenance point of view, it's easier to separate each service to its 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 utilization of 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 the 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 on my GitHub. Each Bicep Module hierarchy level has its own folder in the 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
Comments