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 like itemDisplayName, description, and summary, used by generateBicepReadme.ps1 to auto-generate documentation.
  • tests subfolder: Contains main.bicep and main.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 from metadata.json. It leverages PSDocs (developed by Microsoft’s Bernie White, known for PSRules) to convert main.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!