The Building Blocks of Infrastructure as Code with Azure Bicep
Trent Steenholdt
July 11, 2024
6 minutes to read
In the fast-evolving world of cloud computing and DevOps, Infrastructure as Code (IaC) has become a foundational practice. IaC allows for the management and provisioning of computing resources through machine-readable configuration files rather than physical hardware configuration or interactive configuration tools. Azure Bicep, a domain-specific language (DSL) for deploying Azure resources, embodies this practice by offering a more concise and readable way to define your infrastructure compared to traditional ARM templates. However, an important design principle within IaC often gets overlooked: the importance of modularity. Instead of having one monolithic file that deploys everything, it’s beneficial to break down the code into smaller, reusable pieces, much like building blocks.
Why Modularity Matters
-
Simplicity and Readability Large monolithic scripts can be daunting and difficult to understand. By breaking down infrastructure code into smaller, logical units, each module becomes easier to read and comprehend. This modular approach helps developers and operations teams quickly grasp what each part of the code does, making troubleshooting and modifications simpler and faster.
-
Reusability One of the key advantages of modular IaC is the ability to reuse code. When infrastructure components are defined as discrete modules, they can be used across different projects and environments. This not only saves time but also ensures consistency in how resources are provisioned.
-
Scalability As projects grow, infrastructure requirements become more complex. Managing a single, large script can become unmanageable. Modular code, on the other hand, scales more effectively. Each module can be developed, tested, and deployed independently, allowing teams to scale their infrastructure incrementally without being bogged down by the complexity of a massive codebase.
-
Maintainability Modular code is easier to maintain. Updates and patches can be applied to individual modules without affecting the entire system. This reduces the risk of introducing bugs and makes the infrastructure more resilient to changes. Maintenance becomes a matter of updating specific modules rather than rewriting large portions of code.
-
Collaboration In a collaborative environment, multiple teams often work on different parts of the infrastructure. Modular code allows teams to work independently on their respective components without interfering with each other’s work. This parallel development process increases productivity and reduces the time to deploy new features and updates.
Practical Implementation with Azure Bicep
To illustrate the benefits of modular IaC, let’s consider a simple example using Azure Bicep.
Monalithic Approach
A monolithic Azure Bicep script might look like this:
param location string = 'australiaeast'
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: 'mystorageaccount'
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
resource appServicePlan 'Microsoft.Web/serverfarms@2021-01-15' = {
name: 'myAppServicePlan'
location: location
sku: {
tier: 'Free'
size: 'F1'
}
}
resource webApp 'Microsoft.Web/sites@2021-02-01' = {
name: 'myWebApp'
location: location
properties: {
serverFarmId: appServicePlan.id
}
}
This script is straightforward but becomes cumbersome as more resources are added.
Modular Approach
A modular approach breaks down the infrastructure into smaller, reusable components:
- main.bicep
param location string = 'australiaeast'
module storageAccount 'modules/storageAccount.bicep' = {
name: 'storageModule'
params: {
location: location
storageAccountName: 'mystorageaccount'
}
}
module appServicePlan 'modules/appServicePlan.bicep' = {
name: 'appServicePlanModule'
params: {
location: location
appServicePlanName: 'myAppServicePlan'
}
}
module webApp 'modules/webApp.bicep' = {
name: 'webAppModule'
params: {
location: location
webAppName: 'myWebApp'
appServicePlanId: appServicePlan.outputs.appServicePlanId
}
}
- modules/storageAccount.bicep
param location string
param storageAccountName string
resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
name: storageAccountName
location: location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
}
- modules/appServicePlan.bicep
param location string
param appServicePlanName string
resource appServicePlan 'Microsoft.Web/serverfarms@2021-01-15' = {
name: appServicePlanName
location: location
sku: {
tier: 'Free'
size: 'F1'
}
}
output appServicePlanId string = appServicePlan.id
- modules/webApp.bicep
param location string
param webAppName string
param appServicePlanId string
resource webApp 'Microsoft.Web/sites@2021-02-01' = {
name: webAppName
location: location
properties: {
serverFarmId: appServicePlanId
}
}
Direct Module Calls and CI/CD Pipelines
In some cases, it can be easier to call the modules directly from your Continuous Integration/Continuous Deployment (CI/CD) pipelines, passing outputs as inputs in each step. This approach can streamline the deployment process by breaking it down into clear, manageable stages, making it easier to identify and resolve issues.
For example, a CI/CD pipeline might have separate steps for deploying the storage account, app service plan, and web app, passing the necessary outputs between steps:
- Step 1: Deploy Storage Account
steps:
- task: AzureCLI@2
inputs:
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create --resource-group myResourceGroup --template-file modules/storageAccount.bicep --parameters location='australiaeast' storageAccountName='mystorageaccount'
- Step 2: Deploy App Service Plan
steps:
- task: AzureCLI@2
inputs:
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az deployment group create --resource-group myResourceGroup --template-file modules/appServicePlan.bicep --parameters location='australiaeast' appServicePlanName='myAppServicePlan'
- Step 3: Deploy Web App
steps:
- task: AzureCLI@2
inputs:
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
appServicePlanId=$(az deployment group show --resource-group myResourceGroup --name appServicePlanModule --query 'properties.outputs.appServicePlanId.value' -o tsv)
az deployment group create --resource-group myResourceGroup --template-file modules/webApp.bicep --parameters location='australiaeast' webAppName='myWebApp' appServicePlanId=$appServicePlanId
Conclusion
Adopting a modular approach to Infrastructure as Code brings numerous benefits, from improved readability and reusability to better maintainability and collaboration. By thinking of IaC as building blocks, you can create a flexible, scalable, and robust infrastructure that meets the demands of modern cloud environments. Additionally, leveraging CI/CD pipelines to manage these modules can further streamline your deployment processes. So, next time you sit down to write your infrastructure code, consider breaking it down into smaller, manageable pieces. Your future self (and your team) will thank you.