Pushing Azure Infrastructure-as-Code quality left with Bicep

Published on
Reading time
Authors

Many people coming to the cloud from a traditional infrastructure management background will be familiar with using script-based approaches to managing infrastructure. It's not much of a step from writing bash scripts or PowerShell to using an infrastructure-as-code approach (IaC) to define your ideal cloud infrastructure deployment using solutions such as Terraform or Bicep.

What some may be unfamiliar with is the ideas of design-time validation and unit testing - two very developer-centric activities. In this post I am going to look at how you can introduce these two activities into your IaC practices when using Bicep to deploy IaC for Microsoft Azure.

Design-time valdation

Let's start by looking at linting, which is a computer science term that refers to the process for checking program source code for, amongst other things bugs and stylistic errors. Linting is important as it ensures validity of your source code which may be something entirely different to the code actually compiling or running.

For Bicep, the easiest way to lint your infrastructure defintions to is build them as shown.

bicep build demo.bicep

Once you've done this you might face a wall of linter warnings, some of which you don't wish to know about (or resolve). This is where the bicepconfig.json file comes in, which I'll talk about next.

bicepconfig.json

Bicep's linter (amongst other items) can be configured using a file named bicepconfig.json. The location of the file doesn't matter as long as it exists in a location that is in the current environment's path defintion.

The Bicep documentation is a bit vague about what happens when you have multiple bicepconfig.json files in your path, but the best advice I can give is - place the file in the path hierarchy so it covers the Bicep files you wish to control the linter behaviour for. So, let's say you have the following layout:

.
├── Modules
│   ├── Microsoft.Web
│   └── ...
└── Templates
    ├── deployAppService.bicep
    └── storageaccount.bicep

If you want to control what happens when the Bicep linter runs across both the Modules and Templates folders you would put the file as shown below.

.
├── bicepconfig.json
├── Modules
│   ├── Microsoft.Web
│   └── ...
└── Templates
    ├── deployAppService.bicep
    └── storageaccount.bicep

How do you know what bicepconfig.json file you are using? If you set the vebose property to true you will see the location of the bicepconfig.json file used when your run a build. The location is displayed on the first line of output.

"analyzers": {
    "core": {
      "enabled": true,
      "verbose": true, // this line
      "rules": {}
    }
}

You will see this type of message when you have verbosity enabled.

WARNING: /home/runner/work/swaight/sample/testing/deploy.bicep(1,1) : Info Bicep Linter Configuration: Custom bicepconfig.json file found (/home/runner/work/swaight/sample/bicepconfig.json).

Visual Studio Code will also tell you which file you are using, though at time of writing this post, it's a little odd how it renders as you get a warning on the first line of the Bicep file you have open, regardless of it there are any errors or not.

VS Code display Bicep cnfig file location

Supressing linter rules

I have a repository that has a bunch of Common Azure Resource Module Library (CARML) modules held locally which I don't want get warnings on, so I could either move the bicepconfig.json file to my Templates folder, or I could disable the rules I don't want warnings with.

The largest number of warnings with CARML modules are for decompiler cleanup which is triggered because CARML modules are ARM templates converted into Bicep and the conversion has created Bicep variables that end with _var.

You can disable this rule in the bicepconfig.json file as follows.

"analyzers": {
    "core": {
      "enabled": true,
      "verbose": true,
      "rules": {
        "decompiler-cleanup": {
          "level": "off"
        }
      }
    }
}

Parameter files - goodbye JSON, hello bicepparam

This sailed a little under the radar, but Bicep now has a new format for paramater files, and one you should absolutely move to.

The main issues with the old JSON-formatted parameter files was the lack of design-time validation in your code editor. You didn't know that you were missing required entries, or that parameter values you had provided were invalid given the parameter restrictions in the bicep file you wanted to deploy. Fret no more!

Thankfully with bicepparam files these issues go away as you now editing a Bicep file, just with a different extension!

old-format.json
{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "exampleString": {
      "value": "test string"
    },
    "exampleInt": {
      "value": 4
    },
    "exampleBool": {
      "value": true
    },
    "exampleArray": {
      "value": [
        "value 1",
        "value 2"
      ]
    },
    "exampleObject": {
      "value": {
        "property1": "value1",
        "property2": "value2"
      }
    }
  }
}

Now let's look at this same parameter file in the new bicepparam format.

new-format.bicepparam
using './main.bicep'

param exampleString = 'test string'
param exampleInt = 2 + 2
param exampleBool = true
param exampleArray = [
  'value 1'
  'value 2'
]
param exampleObject = {
  property1: 'value 1'
  property2: 'value 2'
}

You'll notice immediately that this new format is much more compact and is using the same syntax as the Bicep files you've already been authoring. What you can't see is we get automatic warnings in our editor for any missing parameters, along with validation that the provided values match parameter restrictions applied in the main.bicep file.

As this new file format is just a Bicep file we can also use it to help us with our next improvement.

Unit testing

If you come from a software development background, the idea of unit testing is not a new one. If you come from an infrastructure background...

Keep calm and say hello to my little friend

Source: Keep Calms

Unit testing allows software developers to test specific parts of their application code in isolation to either other parts of their code or external dependencies. When you talk about unit testing in the context of software development you will often talk about it in terms of code coverage, or the percentage of your application codebase that is covered by unit tests.

Now imagine bringing this unit testing capability to your IaC solutions where you test your deployments before you even go anywhere near an actual cloud console!

What If?!

The first one we can look at is the use of the deployment what-if operation for Bicep. You can use it to check the impact of, say, a deployment targetting a resource group as follows.

az deployment group what-if \
   --resource-group testrg \
   --name rollout01 \
   --template-file main.bicep \
   --parameter main.env.bicepparam

This won't deploy the resources defined in the Bicep, but it will provide a "what will be affected by this deployment" view. It's important to know that this is the most useful for existing deployments as the what-if has existing resources to compare itself against. You also need to have a cloud environment for this command to have any value.

Regardless, it's a good tool to have in the toolbelt.

One PSRule to rule them all!

Let's wrap up by looking at what is probably the most valuable tool you can use for your Azure IaC validation and testing. If you adopt the fantastic open source PSRule solution you can test the validity of your Bicep along with also checking for compliance of your Bicep with the Microsoft Cloud Adoption Framework (CAF).

There's a lot in PSRule, and Bicep validation is only part of its value, but here's my quick steps to get going with PSRule in your Bicep solutions.

  1. Add a bicepconfig.json file (you've aleady done this... right?)
  2. Switch to using bicepparam files (ditto...)
  3. Install PSRule's PowerShell support on your development machine (even on Mac and Linux...)
  4. Create a psrule.yaml file in the root of your solution.
  5. Profit!

Well, maybe not the last one...

Here's a good starting psrule.yaml file, and one that supports using bicepparam files by setting AZURE_BICEP_PARAMS_FILE_EXPANSION to true. Also, having this file means you can install the PSRule extension for VSCode and run the validation in VSCode instead.

#
# PSRule for Azure configuration
#

# Please see the documentation for all configuration options:
# https://aka.ms/ps-rule-azure
# https://aka.ms/ps-rule-azure/options
# https://aka.ms/ps-rule/options
# https://aka.ms/ps-rule-azure/bicep

# Use rules from the following modules/
include:
  module:
    - 'PSRule.Rules.Azure'

# Require a minimum version of modules that include referenced baseline.
requires:
  PSRule: '@pre >=2.3.2'
  PSRule.Rules.Azure: '@pre >=1.18.1'

# Reference the repository in output.
#repository:
#  url: <<full repo url>>

execution:
  # Ignore warnings for resources and objects that don't have any rules.
  unprocessedObject: Ignore

configuration:
  # Enable expansion for Bicep source files.
  AZURE_BICEP_FILE_EXPANSION: true

  # Expand Bicep module from Bicep parameter files.
  AZURE_BICEP_PARAMS_FILE_EXPANSION: true

  # Set timeout for expanding Bicep source files.
  AZURE_BICEP_FILE_EXPANSION_TIMEOUT: 45

output:
  culture: ['en-AU', 'en-US']

input:
  pathIgnore:
    # Ignore common files that don't need analysis.
    - '**/bicepconfig.json'
    - '*.md'
    - '*.png'
    - '.github/'
    - '.vscode/'
    - '.devcontainer/'

binding:
  preferTargetInfo: true
  targetType:
    - resourceType
    - type

rule:
  exclude:
    # Ignore these recommendations for this repo.
    - Azure.Resource.UseTags # False positive for Management Groups.

Now we have a baseline set for PSRule we can do a couple of more useful steps.

Ensure changes to parameters don't break deployments

If you take a look at the PSRule for Azure Quickstart GitHub repository you will find that within the modules folders each Bicep module has a corresponding .tests folder that contains a Bicep file. If you inspect the one for Storage you will see it looks similar to the below.

If you think it looks a lot like a deployment configuration you'd be right! The value of having this file is that even if we won't be deploying the module we can now enforce testing of the module and ensure any changes that introduce required paramaters, or modify parameter retrictions (length, type) will now break when we attempt to deploy using an existing configuration.

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

@description('Configures the location to deploy the Azure resources.')
param location string = resourceGroup().location

// Test with only required parameters
module test_required_params '../main.bicep' = {
  name: 'test_required_params'
  params: {
    name: 'sttest001'
    location: location
    tags: {
      env: 'test'
    }
  }
}

If you run PSRule against this Bicep file and you've modified the referenced main.bicep file and changed the restrictions on parameter values you'd know immediately you may have broken any consuming deployment. While using bicep build will highlight the same issue, it's really clear there's a breaking change using PSRule!

If you have the PSRule PowerShell module installed you can run the following command at the prompt and see PSRule in action.

Assert-PSRule -InputPath bicep/modules/storage/v1/.tests/main.tests.bicep

You'll notice there is one warning - that a suppression was applied. If you inspect the ps-rule.yaml file in the repository you can see the suppression setting at the bottom of the file. This is one way to limit what PSRule considers an error and is useful if you find a rule is not applicable, or not yet something you can resolve in your enviornment.

PSRUle suppression warning.

Go ahead and modify a parameter in the main.bicep file - add a maxLength restriction or remove a default value. Then re-run PSRule and see how the breaking change is flagged.

You can also write more than one of these test harnesses. Perhaps you have optional deployment of some resources based on a parameter value - you can test that Bicep code out by creating another test file and setting the appropriate parameter to a value that would trigger the optional deployment.

At this point, anyone working locally with Bicep can now validate code changes before they try and check-in. But what about if they do check in something that breaks the Bicep deployments?

Fear not!

PSRule on commit

We'd all love to catch bad code before it gets committed to source control, but sometimes stuff sneaks in, or we're retrofitting something like PSRule to an existing solution. The good news is you can configure PSRule easily for both GitHub Actions and Azure Pipelines using existing tasks.

Depending on how large your solution is, you might want to create multiple Actions or Pipelines, where each one covers only a subset of your solution. You can limit which code changes trigger an automated check so that you don't try and validate your entire repository if only one resource was updated. I will show a sample below.

Azure Pipelines

Install the Task in your environment by reviewing the PSRule installation guide and configure it up against your repository.

Note that in this, and the following sample, I'm using the bicepparam files to drive the tests. This way I can validate all my intended deployments before attempting to actually deploy them anywhere. Perfect!

analyse-web-pipeline.yaml
pr: none
trigger:
  branches:
    include:
      - main
  paths:
    include:
      - src/web/**

pool:
  vmImage: 'ubuntu-latest'

steps:

- task: ps-rule-install@2
  displayName: 'Install PSRule Azure module'
  inputs:
    module: PSRule.Rules.Azure

- task: ps-rule-assert@2
  displayName: 'Run PSRule tests'
  inputs:
    inputType: inputPath
    inputPath: src/web/*.bicepparam
    modules: 'PSRule.Rules.Azure'
    baseline: Azure.Default
    source: '.ps-rule/'
    outputFormat: NUnit3
    outputPath: reports/ps-rule-resources.xml

- task: PublishTestResults@2
  displayName: 'Publish PSRule results'
  inputs:
    testRunTitle: 'PSRule'
    testRunner: NUnit
    testResultsFiles: 'reports/ps-rule-*.xml'
    mergeTestResults: true
  condition: succeededOrFailed()

Once you have a Pipeline built you can have it run and it will produce a result that gives you great visibility around the state of your Bicep.

PSRUle output as Test Results in Azure Pipelines.

GitHub Actions

Head over and install the task as per the PSRule installation guide, then create an Action similar to the one below (make sure to look at using templates to reduce duplicate code where you can).

analyse-web-action.yaml
name: Analyse Web resources

on:
  push:
  pull_request:
    paths:
      - 'src/web/**'
    branches:
      - main
  workflow_dispatch:

jobs:

 analyze:
    name: Analyze bicep resources
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      # Run analysis by using the PSRule GitHub action.
      - name: Run PSRule analysis
        uses: microsoft/ps-rule@v2.9.0
        with:
          modules: PSRule.Rules.Azure
          baseline: Azure.Default
          outputFormat: NUnit3
          outputPath: reports/ps-rule-resources.xml
          inputPath: src/web/*.bicepparam

      # Publish PSRule outputs as test results
      - name: Publish Test Results
        uses: EnricoMi/publish-unit-test-result-action/composite@v2
        if: always()
        with:
          job_summary: true
          files: |
            reports/ps-rule-resources.xml

This Action results in a build summary similar to the one shown below.

PSRUle output as Test Results in GitHub Actions.

Wait, there's more!

The Bicep team revealed during their July 2023 community call that they've been working on validation and testing as a core part of Bicep. Keep an eye out for this to ship at some point in the future!

Wrapping it all up

There's a been a massive amount of ground covered in this post so let's quickly summarise what we covered.

First off we looked at design-time validation using bicep build and the bicep linter, along with how you can control the behaviour of bicep using the bicepconfig.json file.

To help further with design-time validation we looked at how switching to bicepparam files allows us to validate deployments locally, something that was hard to do using the traditional JSON parameter files.

Finally, we introduced the idea of unit testing, looking at how we can use what-if on deployments to see what impact a deployment will have, before wrapping up with PSRule for testing and validating your Bicep both on your developer machine and on your CI server.

Phew! 😮‍💨

We made it! Hopefully off the back of this post your Bicep quality is going to jump and your deployment-time failures are going to decrease!

What are you doing in your enviornment? Drop a comment below to share!

Happy Days! 😎