Develop, test, and publish Azure Bicep modules for the Azure Bicep Registry using an Azure DevOps pipeline. This blog post will walk you through creating, testing, and deploying Bicep modules in Azure DevOps, ensuring your modules are validated and ready for production. By the end of this guide, you’ll have a clear pipeline for Bicep module development, ensuring smooth deployment and future scalability.
1. What Are Azure Bicep Modules?
Azure Bicep is a domain-specific language (DSL) for deploying Azure resources declaratively. Bicep simplifies the process of defining your Azure infrastructure as code (IaC). A Bicep module is essentially a reusable piece of Bicep code that can be shared, versioned, and deployed independently.
With Azure Bicep Registry, you can publish your Bicep modules for reuse across different environments, projects, or teams, enhancing your DevOps pipeline and creating scalable infrastructure-as-code solutions.
2. Prerequisites for Developing and Testing Bicep Modules
Before you start developing and testing your Bicep modules, ensure you have the following:
- Azure Subscription: A development/sandbox subscription where resources can be deployed for testing.
- Azure Bicep Registry: This will store your modules once they are validated and ready for production.
- Azure DevOps: The main platform to run the CI/CD pipeline for testing and deploying your Bicep modules.
- Service Connection: An Azure DevOps service connection with the right permissions to your Azure subscription where you will be deploying your test resources.
3. Step-by-Step Guide: Creating and Testing Bicep Modules in a Single Repository
When managing multiple Bicep modules in a single shared repository, it’s crucial to establish an efficient structure and process. Here’s the best approach:
Repository Structure:
Organize Bicep modules in separate subfolders:
/modules
/storageAccount
- main.bicep
- metadata.json
/tests
- main.bicep (for test deployment referencing parent module file)
- main.bicepparam (for test deployment)
/keyVault
- main.bicep
- metadata.json
/tests
- main.bicep
- main.bicepparam
/sqlServer
- main.bicep
- metadata.json
/tests
- main.bicep
- main.bicepparam
/generateBicepReadme.ps1 # Script for generating README.md files from module metadata
File Descriptions:
main.bicep
: The core Bicep file defining the module’s resources.metadata.json
: Contains metadata likeitemDisplayName
,description
, andsummary
, used bygenerateBicepReadme.ps1
to auto-generate documentation.tests
subfolder: Containsmain.bicep
andmain.bicepparam
files for test deployments, referencing the parent module for isolated testing.generateBicepReadme.ps1
: Authored by Tao Yang, this PowerShell script automates README.md file generation for each Bicep module using metadata frommetadata.json
. It leverages PSDocs (developed by Microsoft’s Bernie White, known for PSRules) to convertmain.bicep
files into ARM templates for documentation purposes, ensuring standardized documentation.
Example of metadata.json
:
{
"itemDisplayName": "Display Name of the Template",
"description": "Template Description.",
"summary": "Summary describing the Bicep template."
}
4. Implementing the Azure DevOps Pipeline with Validation and Deployment
With your repository structured, here’s how to build a robust Azure DevOps pipeline to validate, deploy, and test your Bicep modules.
Azure DevOps Pipeline Breakdown:
- Manual Trigger: The pipeline is manually triggered via the Azure DevOps UI.
- What-If Preview Stage: Simulates deployment and provides a preview of changes without modifying resources.
- Deployment Stage: Deploys the selected module.
- Post-Deployment Validation: Verifies resources are correctly deployed, followed by cleanup.
- README Generation: Uses PSDocs to automatically generate a README.md file for each module.
YAML Pipeline:
name: 'Bicep_Modules_Test_Pipeline_$(Build.SourceBranchName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)'
trigger: none # Manual trigger for controlled publishing
pr: none # No automatic PR validation, triggered manually
variables:
resourceGroup: "my-rg"
serviceConnectionName: "" # Update with your actual service connection name
testBicepFilePath: "./modules/$(moduleName)/tests/main.bicep"
testParamFilePath: "./modules/$(moduleName)/tests/main.bicepparam"
mainBicepFilePath: "./modules/$(moduleName)/main.bicep"
moduleName: "storageAccount" # Default module name, can be overridden in parameters
registryName: "" # Replace with your default registry name
parameters:
- name: moduleName
displayName: 'Module to Test'
type: string
default: 'storageAccount'
values:
- storageAccount
- keyVault
- sqlServer
- anyOtherModule
stages:
# Stage 1: What-If Preview to Simulate the Test Deployment
- stage: WhatIfTestDeployment
displayName: 'What-If Bicep Test Deployment'
jobs:
- job: WhatIfSimulation
displayName: 'Simulate Bicep Deployment with What-If'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
$bicepFile = "$(testBicepFilePath)"
$paramFile = "$(testParamFilePath)"
Write-Host "Simulating Bicep test deployment for: $bicepFile"
# Use What-If to preview changes
New-AzResourceGroupDeployment -ResourceGroupName "$(resourceGroup)" `
-TemplateFile $bicepFile `
-TemplateParameterFile $paramFile `
-WhatIf -Verbose
Write-Host "What-If simulation completed."
displayName: 'Run What-If for Test Deployment'
# Stage 2: Deploy the Bicep Test Deployment
- stage: DeployBicepTestModule
displayName: 'Deploy Bicep Test Module'
dependsOn: WhatIfTestDeployment
jobs:
- job: DeployTestModule
displayName: 'Deploy and Test Selected Bicep Test Module'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
- script: |
Write-Host "Deploying test module: $(moduleName)"
displayName: 'Log Selected Parameters'
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
$resourceGroup = "$(resourceGroup)"
$paramFile = "$(testParamFilePath)"
$bicepFile = "$(testBicepFilePath)"
Write-Host "Deploying $(moduleName) test to resource group: $resourceGroup"
# Deploy the Bicep module using .bicepparam for parameters
New-AzResourceGroupDeployment -ResourceGroupName $resourceGroup `
-TemplateFile $bicepFile `
-TemplateParameterFile $paramFile -Verbose -ErrorAction Stop
Write-Host "Test deployment completed successfully."
displayName: 'Deploy Test Bicep Module'
# Stage 3: Post-Deployment Validation and Cleanup
- stage: ValidateAndCleanupDeployment
displayName: 'Post-Deployment Validation and Cleanup'
dependsOn: DeployBicepTestModule
jobs:
- job: ValidateAndDestroyResources
displayName: 'Validate and Destroy Deployed Test Resources'
pool:
vmImage: 'windows-latest'
steps:
- script: |
Write-Host "Running post-deployment validation for test module: $(moduleName)"
# Placeholder for specific validation checks on deployed resources
# Example: Validate resource properties, existence, etc.
displayName: 'Post-Deployment Validation'
# Destroy test resources after validation
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
$resourceGroup = "$(resourceGroup)"
Write-Host "Deleting all resources within resource group: $resourceGroup"
# Get all resources in the specified resource group
$resources = Get-AzResource -ResourceGroupName $resourceGroup
# Delete each resource in the resource group
foreach ($resource in $resources) {
Write-Host "Deleting resource: $($resource.Name)"
Remove-AzResource -ResourceId $resource.ResourceId -Force -ErrorAction Stop
}
Write-Host "All resources in resource group $resourceGroup have been deleted."
displayName: 'Destroy Test Resources After Validation'
# Stage 4: Generate README Documentation with PSDocs
- stage: GenerateReadme
displayName: 'Generate README Documentation'
jobs:
- job: GenerateReadmeForModule
displayName: 'Generate README for Bicep Module'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
# Install PSDocs and PSDocs.Azure module if not already installed
if (-not (Get-Module -ListAvailable -Name PSDocs)) {
Install-Module PSDocs -Force -Scope CurrentUser
}
if (-not (Get-Module -ListAvailable -Name PSDocs.Azure)) {
Install-Module PSDocs.Azure -Force -Scope CurrentUser
}
# Define paths for Bicep and metadata files
$bicepFile = "$(mainBicepFilePath)"
# Ensure the generateBicepReadme.ps1 script is available
if (-not (Test-Path -Path "./generateBicepReadme.ps1")) {
Write-Error "Script generateBicepReadme.ps1 not found. Please add it to the repository."
exit 1
}
# Run PSDocs to generate README.md file
Write-Host "Generating README for module: $(moduleName)"
.\generateBicepReadme.ps1 -templatePath $bicepFile -verbose
Write-Host "README.md generated for $(moduleName) module."
displayName: 'Run PSDocs to Generate README'
Detailed Pipeline Features and Benefits:
- Manual Trigger: This approach prevents unintended changes, giving developers full control.
- What-If Preview: The
-WhatIf
parameter provides a safe way to preview changes before applying them. - Deployment Stage: Ensures consistent deployments with
.bicepparam
for parameter management. - Post-Deployment Cleanup: Cleans up resources to maintain a clean testing environment.
- README Generation: Keeps documentation up-to-date with automated generation using
generateBicepReadme.ps1
.
5. Versioning Bicep Modules for Publishing
When working with Azure Bicep modules, versioning is essential for publishing to the Azure Bicep Registry. Embed versioning directly within the Bicep files to ensure updates are traceable.
Key Steps for Versioning:
- Include Version in Bicep Files: Increment the version (e.g.,
1.0.0
,1.1.0
) with each change. - Sample Versioned Block:
@description('Version of the Bicep module')
@minLength(1)
@maxLength(10)
param version string = '1.0.0'
resource storageAccount 'Microsoft.Storage/storageAccounts@2023-01-01' = {
name: 'mystorageaccount'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: {}
}
output moduleVersion string = version
Publishing Process:
Ensure the version is updated in main.bicep
before publishing the module to the registry.
6. Publishing Modules to Azure Bicep Registry
Once your Bicep module successfully passes preview and deployment stages, publish it using the following Azure DevOps pipeline.
Publishing Pipeline:
name: 'Publish_Bicep_Modules_Pipeline_$(Build.SourceBranchName)_$(Year:yyyy).$(Month).$(DayOfMonth)$(Rev:.r)'
trigger: none # Manual trigger for controlled publishing
pr: none # No automatic PR validation
parameters:
- name: registryName
displayName: 'Bicep Registry Name'
type: string
default: '' # Replace with your actual registry name
jobs:
- job: PublishModules
displayName: 'Publish Bicep Modules to Registry'
pool:
vmImage: 'windows-latest'
steps:
- checkout: self
# Step to install the Azure CLI Bicep extension if not already installed
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
# Ensure Azure Bicep CLI extension is installed
if (-not (az bicep version)) {
az bicep install
}
# Iterate through each module folder to check versions and publish if needed
- task: AzurePowerShell@5
inputs:
azureSubscription: '$(serviceConnectionName)'
ScriptType: 'InlineScript'
Inline: |
$modulesPath = "./modules"
$registryName = "$(registryName)"
# Iterate over each module directory, excluding "tests" folders
foreach ($moduleFolder in Get-ChildItem -Directory -Path $modulesPath -Exclude 'tests') {
$moduleName = $moduleFolder.Name
$bicepFilePath = "$($moduleFolder.FullName)/main.bicep"
# Check if the main.bicep file exists
if (Test-Path $bicepFilePath) {
# Extract the version from the metadata block in main.bicep
$bicepContent = Get-Content -Path $bicepFilePath -Raw
$versionMatch = $bicepContent -match 'metadata\s*\{\s*version\s*:\s*"([^"]+)"' | Out-Null
$moduleVersion = $matches[1]
if ($moduleVersion) {
# Define the target registry path with version
$targetPath = "br:$registryName.azurecr.io/$moduleName:v$moduleVersion"
# Check if module version already exists in the registry
$existingVersion = az bicep list | Select-String -Pattern "$targetPath" -Quiet
if (-not $existingVersion) {
Write-Host "Publishing $moduleName version $moduleVersion to $targetPath"
# Publish the Bicep module to the registry, excluding tests
az bicep publish --file $bicepFilePath --target $targetPath
Write-Host "$moduleName version $moduleVersion successfully published."
}
else {
Write-Host "$moduleName version $moduleVersion already exists in the registry. Skipping publish."
}
}
else {
Write-Host "Version metadata not found in $moduleName. Skipping."
}
}
else {
Write-Host "Skipping $moduleName as main.bicep is missing."
}
}
Explanation of Steps:
- Manual Trigger: Ensures controlled publishing.
- Version Check: Reads the
main.bicep
file for version metadata and checks if it already exists in the registry. - Conditional Publishing: Publishes the module if the version is new, ensuring existing versions aren't overwritten.
7. Conclusion
By following this guide, you now have a complete pipeline for creating, testing, validating, and deploying Azure Bicep modules. Leveraging the Azure Bicep Registry enables reusable and version-controlled modules across environments.
Key Takeaways:
- A structured repository ensures scalability as new modules are added.
- The pipeline validates Bicep code before deployment, promoting reliability.
- Publishing to the Azure Bicep Registry allows for consistent reuse and version control.
Do you have a different approach for testing, developing, or publishing Azure Bicep modules? I’d love to hear about it! Whether it’s a unique validation method, a streamlined deployment process, or best practices for managing Bicep module files in shared environments, feel free to share your thoughts and experiences in the comments below. Let's learn together!