Enhancing Bicep Module Testing with PSRule: A Continuation Guide

In my previous article on creating and testing Bicep modules for the Azure Bicep Registry, I detailed the step-by-step process for developing, structuring, and deploying Bicep modules using an Azure DevOps pipeline. We explored repository structuring, pipeline stages for validation, deployment, post-deployment cleanup, and README generation. In this follow-up, we'll expand on how to improve Bicep module testing using PSRule, a tool essential for enforcing best practices and ensuring compliance in your CI/CD pipeline.

Brief Recap of the Original Blog Post

The initial guide covered:

  • Structuring your Bicep modules and test files.
  • Developing and deploying modules using Azure DevOps pipelines.
  • Running What-If simulations and performing post-deployment validations.
  • Integrating automated README generation with PSDocs.

While this setup ensured basic module testing, we can take it a step further by incorporating PSRule to add comprehensive best practice checks during the testing phase.

Why Integrate PSRule for Bicep Module Testing?

The Importance of PSRule: PSRule is an essential tool for validating Infrastructure-as-Code (IaC), ensuring that your Bicep modules adhere to Azure best practices. Integrating PSRule into your testing pipeline improves the quality, security, and compliance of your cloud infrastructure by proactively catching issues and enforcing standards automatically.

Why Do We Need PSRule?

  1. Enforcing Best Practices: PSRule helps enforce Azure best practices by analyzing your Bicep templates for common misconfigurations, security vulnerabilities, and compliance gaps. This results in more robust and secure deployments by ensuring that your modules align with industry standards. Including modules such as PSRule.Rules.Azure, PSRule.Rules.CAF, and PSRule.Rules.Kubernetes ensures comprehensive coverage of Azure-specific, Cloud Adoption Framework (CAF), and Kubernetes-related best practices.

    include:
      module:
      - PSRule.Rules.Azure
      - PSRule.Rules.CAF
      - PSRule.Rules.Kubernetes
    
  2. Automated Quality Checks: Integrating PSRule into your CI/CD pipeline automates the validation process. This means potential issues can be identified early in the development cycle, saving time and reducing the risk of deploying configurations with errors to production. Automated checks help ensure that infrastructure changes meet organizational and Azure best practice standards without manual reviews.

  3. Compliance and Security Assurance: PSRule checks are crucial for maintaining compliance with organizational policies and cloud security standards. This helps prevent configuration flaws that could lead to security vulnerabilities or non-compliance with regulatory requirements. By using modules like PSRule.Rules.CAF, organizations can align their IaC with strategic governance and best practices set by the Cloud Adoption Framework.

  4. Improved Code Reliability: By catching potential issues during the development phase, PSRule enhances the reliability and predictability of your infrastructure deployments. Ensuring that code follows best practices leads to fewer errors and more stable, long-term performance. This proactive approach supports a strong foundation for continuous deployment and infrastructure scaling.

Why Include Specific PSRule Modules?

  • PSRule.Rules.Azure: Provides rules that focus on Azure best practices for resource configuration, security, and compliance.
  • PSRule.Rules.CAF: Aligns your Bicep modules with the Microsoft Cloud Adoption Framework, ensuring your infrastructure meets recommended governance and operational standards.
  • PSRule.Rules.Kubernetes: Helps validate Kubernetes resource definitions, making sure your templates for Azure Kubernetes Service (AKS) or other Kubernetes setups meet best practices.

By including these modules in your PSRule configuration, you benefit from a broad range of checks that cover various aspects of infrastructure security, reliability, and compliance. This comprehensive approach helps streamline your development process and enhances the overall quality of your IaC.

The Challenge of Running PSRule Directly on Bicep Modules

When running PSRule directly on a module's main.bicep file, you may encounter errors such as: "The parameter named 'someParam' was not set or a defaultValue was defined."
This occurs because PSRule requires actual parameter values to fully expand and analyze the Bicep code. Without these values, PSRule cannot reliably run its checks, leading to incomplete or inaccurate validation.

Bernie White, the creator of PSRule, highlighted this in a GitHub discussion, explaining why PSRule cannot reliably analyze raw Bicep templates due to the absence of provided parameter values.

Why This Problem Occurs:

  • Incomplete Context: Raw Bicep templates often lack parameter values, which prevents PSRule from expanding and validating the template fully.
  • Parameter Dependencies: Without these parameter values, PSRule may raise errors or skip checks that rely on specific configurations, resulting in incomplete analysis.

The Solution: Test Deployment Files with PSRule

To overcome this challenge, it's recommended to use test deployment files (e.g., tests/main.bicep) and test parameter files (e.g., tests/main.bicepparam). These files provide the context and data needed for PSRule to perform accurate analysis.

How It Works:

  • Test Deployment File: A file that references your main.bicep module and passes in actual parameter values.
  • Test Parameter File: A file that supplies realistic parameter values to the test deployment file, simulating a real deployment.

Summary:

Why Use PSRule:

  • To enforce best practices, automate quality checks, ensure compliance, and improve the reliability of your code.

Why Use Test Deployment Files:

  • To provide PSRule with the necessary parameter values for complete validation and to avoid errors when running checks directly on raw Bicep templates.

Updated Repository Structure

To integrate PSRule, your repository structure should look like this:


  /modules
    /storageAccount
        - main.bicep
        - metadata.json
        /tests
            - main.bicep (references parent module for validation)
            - main.bicepparam
    /keyVault
        - main.bicep
        - metadata.json
        /tests
            - main.bicep
            - main.bicepparam
    /sqlServer
        - main.bicep
        - metadata.json
        /tests
            - main.bicep
            - main.bicepparam
  /Pipelines
        - %your_main_pipeline%.yaml
        /ps-rule    
         - ps-rule.yaml  # PSRule configuration file
/generateBicepReadme.ps1  # Script for generating README.md files

Example of Test Deployment main.bicep File

Ensure the tests subfolder includes a Bicep file that references the main module:


param storageAccountName string
param location string = 'eastus'
param skuName string = 'Standard_LRS'
param kind string = 'StorageV2'
param accessTier string = 'Hot'
param enableHttpsTrafficOnly bool = true
param tags object = {}

module testStorageAccount '../main.bicep' = {
  name: 'testStorageAccount'
  params: {
    storageAccountName: storageAccountName
    location: location
    skuName: skuName
    kind: kind
    accessTier: accessTier
    enableHttpsTrafficOnly: enableHttpsTrafficOnly
    tags: tags
  }
}

Example of Test Deployment main.bicepparam File

using 'main.bicep'

param storageAccountName = 'teststorageacct001'
param location = 'eastus'
param skuName = 'Standard_GRS'
param kind = 'StorageV2'
param accessTier = 'Hot'
param enableHttpsTrafficOnly = true
param tags = {
  Environment: 'Test'
  Project: 'PSRuleDemo'
}

Integrating PSRule in Your DevOps Pipeline

To add PSRule to your existing DevOps pipeline, follow these steps:

  1. Add PSRule Configuration: To integrate PSRule effectively into your Bicep module testing pipeline, you need to create a detailed ps-rule.yaml file. This configuration file will define the specific PSRule modules to include, any custom rules you wish to enforce, and essential settings for the behavior of PSRule during execution. Ensure that this file is placed in the /Pipelines/ps-rule/ directory or any other directory within your repository that aligns with your pipeline structure. The ps-rule.yaml file acts as the control center for PSRule's operations, allowing you to customize the analysis scope, manage input paths, and exclude or include specific checks, giving you precise control over how compliance is validated in your CI/CD pipeline:

  2. 
    configuration:
      # Enable expansion for Bicep source files.
      AZURE_BICEP_FILE_EXPANSION: true
    
      # Expand Bicep module from Azure parameter files.
      AZURE_PARAMETER_FILE_EXPANSION: true
    
      # Set timeout for expanding Bicep source files.
      AZURE_BICEP_FILE_EXPANSION_TIMEOUT: 15
    
    execution:
      # Ignore warnings for resources and objects that don't have any matching rules.
      unprocessedObject: Ignore
    
    input:
      pathIgnore:
        - "**/bicepconfig.json"
        - "*.yaml"
        - "*.yml"
        - "*.md"
        - "*.ps1"
        - "**/*.bicepparam"
        - "Modules/**/*.json"
        - "Modules/**/*.bicep"
        - "!Modules/**/tests/*.bicep" # Include only .bicep files in tests subfolders
    
    include:
      module:
        - PSRule.Rules.Azure
        - PSRule.Rules.CAF
        - PSRule.Rules.Kubernetes
    
      # Include any custom rule files if needed.
      source:
        - Pipelines/ps-rule/custom-rules.ps-rule.yaml
    
    rule:
      exclude:
        - Azure.Resource.UseTags
    
    output:
      culture: ["en-GB"]
    
  3. Update the Pipeline: Add a new job to the existing pipeline for running PSRule checks.

Pipeline YAML Snippet for PSRule Integration:

trigger: none
name: $(SourceBranchName)_$(date:yyyyMMdd)$(rev:.r)
pool:
  vmImage: "ubuntu-latest"

jobs:
- job: 'PSRuleAnalysis'
  displayName: "Run PSRule analysis"
  steps:
  - script: |
      pwsh -Command "Install-Module -Name Az.Resources -AllowClobber -Force"
    displayName: "Install Az.Resources Module (Latest Version)"

  - task: PowerShell@2
    inputs:
      targetType: 'inline'
      script: |
        $env:PSRULE_AZURE_RESOURCE_MODULE_NOWARN = 'true'
    displayName: "Suppress PSRule Azure Module Warning"

  - task: bewhite.ps-rule.assert.ps-rule-assert@2
    inputs:
      source: "$(System.DefaultWorkingDirectory)/Modules"
      configuration: "$(System.DefaultWorkingDirectory)/Pipelines/rules/ps-rules.yaml"
      modules: PSRule.Rules.Azure, PSRule.Rules.CAF
      outputFormat: Sarif
      outputPath: "$(Pipeline.Workspace)/a/CodeAnalysisLogs/ps-rule-results.sarif"
    continueOnError: true

  # Optional Troubleshooting Step: Verify SARIF file in artifact staging directory
  - script: |
      echo "Listing contents of $(Pipeline.Workspace)/a/CodeAnalysisLogs:"
      ls -l "$(Pipeline.Workspace)/a/CodeAnalysisLogs"
    displayName: "Check SARIF File in Artifact Staging Directory"

  # Publish the SARIF file as an artifact
  - task: PublishBuildArtifacts@1
    inputs:
      path: "$(Pipeline.Workspace)/a/CodeAnalysisLogs"
      artifactName: "CodeAnalysisLogs"
      publishLocation: "Container"

PSRule Module Scan Insights: Examples and Best Practices

Integrating PSRule into your Bicep module testing pipeline provides valuable feedback on whether your templates align with Azure best practices. Below, we’ll illustrate how a PSRule scan can highlight potential issues and how adjustments to your module can help achieve successful validation.

PSRule Module Scan and Identified Issues

Consider a Bicep module where the skuName parameter for an Azure Storage Account is set as follows:

param storageAccountName string 
param location string = resourceGroup().location
param skuName string = 'Standard_LRS'
param kind string = 'StorageV2'
param accessTier string = 'Hot'
param enableHttpsTrafficOnly bool = true
param tags object = {}

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: skuName
  }
  kind: kind
  properties: {
    supportsHttpsTrafficOnly: enableHttpsTrafficOnly
    accessTier: accessTier
  }
  tags: tags
}

output storageAccountId string = storageAccount.id

Running a PSRule scan on this configuration may result in warnings or errors, such as:

Example Failure Messages:

  • [FAIL] Azure.Storage.Firewall (AZR-000202)
    • Recommendation: Configure storage firewalls to restrict network access to permitted clients only. Consider enforcing this setting with Azure Policy.
    • Reason: The field properties.networkAcls.defaultAction is missing, indicating that network access is not restricted.

PSRule scan results

Enhancing the Module for Compliance

To address these issues and meet Azure best practices, adjustments can be made to the module as shown below:

param storageAccountName string 
param location string = resourceGroup().location
param skuName string = 'Standard_LRS'
param kind string = 'StorageV2'
param accessTier string = 'Hot'
param enableHttpsTrafficOnly bool = true
param tags object = {}
param allowBlobPublicAccess bool = false  // Added parameter to control public access
param minimumTlsVersion string = 'TLS1_2'  // Added parameter for minimum TLS version

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' = {
  name: storageAccountName
  location: location
  sku: {
    name: skuName
  }
  kind: kind
  properties: {
    supportsHttpsTrafficOnly: enableHttpsTrafficOnly
    accessTier: accessTier
    allowBlobPublicAccess: allowBlobPublicAccess  // Configured to disable public access
    minimumTlsVersion: minimumTlsVersion  // Enforces TLS 1.2 or higher
    networkAcls: {  // Added network ACLs to restrict access
      defaultAction: 'Deny'  // Denies all traffic by default
      bypass: 'AzureServices'  // Allows trusted Azure services by default
      ipRules: [
        {
          value: '203.0.113.0/24'  // Replace with permitted IP ranges
        }
      ]
    }
  }
  tags: tags
}

output storageAccountId string = storageAccount.id

Outcome of Module Adjustments

By incorporating these changes, the PSRule scan will no longer flag the issues related to network access control or TLS version settings:

Additional Examples of PSRule Checks and Failures for Common Modules

To illustrate further, let's look at potential failures and successes when testing other Azure modules, such as Azure Key Vault, Azure Windows App Service, and Azure SQL Server.

1. Azure Key Vault Module

Potential PSRule Check Failure:

// File: ./modules/keyVault/tests/main.bicep
module testKeyVault '../keyVault.bicep' = {
  name: 'testKeyVault'
  params: {
    location: resourceGroup().location
    skuName: 'Standard'  // Potential best practice issue if 'Premium' is recommended for security
    tags: {
      Environment: 'Test'
    }
  }
}

Failure Message:

Recommendation: Consider using 'Premium' SKU for enhanced security features in production environments.

PSRule Module Scan Success: Adjusting the skuName to 'Premium' will satisfy the PSRule check.

2. Azure Windows App Service Module

Potential PSRule Check Failure:

// File: ./modules/appService/tests/main.bicep
module testAppService '../appService.bicep' = {
  name: 'testAppService'
  params: {
    location: resourceGroup().location
    skuName: 'F1'  // Potential issue for production scenarios
    tags: {
      Environment: 'Development'
    }
  }
}

Failure Message:

Warning: Free and shared SKUs (e.g., 'F1') are suitable for development only. Use a production-grade SKU (e.g., 'S1') for live environments.

PSRule Module Scan Success: Changing skuName to 'S1' or another suitable production SKU will resolve the warning.

3. Azure SQL Server Module

Potential PSRule Check Failure:

// File: ./modules/sqlServer/tests/main.bicep
module testSqlServer '../sqlServer.bicep' = {
  name: 'testSqlServer'
  params: {
    location: resourceGroup().location
    adminUsername: 'adminUser'  // Potential issue if not secure enough
    tags: {
      Environment: 'Test'
    }
  }
}

Failure Message:

Security Risk: Avoid using generic admin usernames like 'adminUser'. Use a more complex and unique admin name.

PSRule Module Scan Success: Changing adminUsername to a more secure and unique value (e.g., 'secureAdmin123') ensures compliance and prevents potential security issues.

Final Thoughts

Integrating PSRule into your Azure DevOps pipeline adds a crucial layer of validation to your Bicep module testing. This approach helps maintain compliance, ensures best practices are met, and increases the reliability of your infrastructure deployments. By building on the foundation laid out in the original guide, you now have a comprehensive process for developing, testing, validating, and deploying Bicep modules in an enterprise-grade pipeline.

Do you have your own best practices or insights for testing Bicep modules? Share your thoughts in the comments below and let’s learn together!