Adding linting, validation and what-if to Bicep templates in Azure DevOps
Tags: azure, testing, devops, tutorial
Published: 2023-02-26 22:10:57

We are picking up from my last post where I walked through automating Bicep template deployments through Azure DevOps so if you haven't caught up here is a [link](https://dev.to/lakkimartin/deploying-bicep-template-using-azure-devops-4m53)

The pipeline we set up in the previous post ran a single job that deployed our Azure resource using Bicep. However, in a production scenario we want to run some validation checks to ensure our new code won't breaking anything. Stages in Azure DevOps pipelines comprise of jobs and tasks e.g. running a script and you can think of them as containers for grouping different jobs. 

Let's look at the new stages that we will be adding to our pipeline. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pmxchm4zzigrn6py3hlo.png)

Let's understand each of the stages:

1. Lint: Use the Bicep linter to verify that the Bicep file is well formed and doesn't contain any obvious errors.
2. Validate: Use the Azure Resource Manager preflight validation process to check for problems that might occur when you deploy.
3. Preview: Use the what-if command to validate the list of changes that will be applied against your Azure environment. This allows people to review and approve the changes before deployment. 
4. Deploy: Submit your deployment to Resource Manager and wait for it to finish pending the approval gate.

## Converting our pipeline to a multistage pipeline

Since we want to run multiple stages we need to add some additional code to our YAML template. This is going to look something like this: 

```yaml
stages:

- stage: Lint
  jobs:
  - job: LintCode

- stage: Validate
  dependsOn: Lint
  jobs: 
  - job: ValidateBicepTemplate

- stage: Preview
  dependsOn: Validate
  jobs: 
  - job: Preview

- stage: Deploy
  dependsOn: Preview
  jobs: 
  - job: Deploy
```
By default stages run in parallel if you have enough agents however, we want to run these stages sequentially so I have added a `dependsOn:` to achieve this meaning that each stage can only run once the previous stage completes successfully. 

This is the basic structure and in the next few sections we will build up each stage of the pipeline. 


## Adding Linting 

Linting validation will check for things like unused parameters, unused variables, interpolation, secure parameters and more. The first thing we need to do is add the lint stage to our pipeline: 

```yaml
trigger:
- master

name: Deploy Bicep files


variables:
  vmImageName: 'ubuntu-latest'

  azureServiceConnection: 'devops-poc3-deploy-spi'
  location: 'australiaeast'
  templateFile: 'bicep/azure_resource_group/template.bicep'
  templateParameterFile: 'bicep/azure_resource_group/parameters.json'

pool:
  vmImage: $(vmImageName)

stages:  

- stage: Lint
  jobs:
  - job: LintCode
    displayName: Lint code
    steps:
      - script: |
          az bicep build --file $(Build.SourcesDirectory)/$(templateFile)
        name: LintBicepCode
        displayName: Run Bicep linter
```
Azure pipeline doesn't treat lint warnings as problems, so we need to add a configuration file to the repository where our bicep file is located. Copy and paste the below and name the file `bicepconfig.json`:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5siico73pegbn6mudxh2.png)



```json
{
  "analyzers": {
    "core": {
      "enabled": true,
      "verbose": true,
      "rules": {
        "adminusername-should-not-be-literal": {
          "level": "error"
        },
        "max-outputs": {
          "level": "error"
        },
        "max-params": {
          "level": "error"
        },
        "max-resources": {
          "level": "error"
        },
        "max-variables": {
          "level": "error"
        },
        "no-hardcoded-env-urls": {
          "level": "error"
        },
        "no-unnecessary-dependson": {
          "level": "error"
        },
        "no-unused-params": {
          "level": "error"
        },
        "no-unused-vars": {
          "level": "error"
        },
        "outputs-should-not-contain-secrets": {
          "level": "error"
        },
        "prefer-interpolation": {
          "level": "error"
        },
        "secure-parameter-default": {
          "level": "error"
        },
        "simplify-interpolation": {
          "level": "error"
        },
        "protect-commandtoexecute-secrets": {
          "level": "error"
        },
        "use-stable-vm-image": {
          "level": "error"
        }
      }
    }
  }
}
```
Our pipeline will now fail if any of the linting flags warnings. 

## Adding Bicep template validation

We can use the Azure Resource Manager Deployment Task from the previous post to also run a validation against the ARM API to check for anything that may cause the deployment to fail e.g. reserved name for a storage account. 

Let's get this stage added. Copy and paste the below straight after the lint stage (careful of the YAML indentation): 

```yaml
- stage: Validate
  dependsOn: Lint
  jobs: 
  - job: ValidateBicepTemplate
    steps:
    - task: AzureResourceManagerTemplateDeployment@3
      inputs:
        deploymentScope: 'Subscription'
        azureResourceManagerConnection: '$(azureServiceConnection)'
        location: '$(location)'
        templateLocation: 'Linked artifact'
        csmFile: '$(Build.SourcesDirectory)/$(templateFile)'
        csmParametersFile: '$(Build.SourcesDirectory)/$(templateParameterFile)'
        deploymentMode: 'Validation'  
```

## Adding What-If (Preview) analysis. 

Before deploying the changes, we can run a what-if operation that will give you a preview of the template will change. This is a really cool feature and very similar to Terraform Plan where you can see exactly the number of changes the Bicep template will perform (if any). 

Let's get this stage added. Copy and paste the below straight after the validation stage: 

```yaml
- stage: Preview
  dependsOn: Validate
  jobs: 
  - job: Preview
    steps:
    - task: AzureCLI@2
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          az deployment sub what-if \
            --template-file $(Build.SourcesDirectory)/$(templateFile) \
            --parameters '$(Build.SourcesDirectory)/$(templateParameterFile)' \
            --location '$(location)'
```
Run the pipeline and select the preview stage to look at the results: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/l3kf4e1f3jncr9f2h929.png)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k03vp3ginnzgxoh2w9ch.png)

We can see that the what-if is showing that one resource will be created and in this instance it's the resource group we defined in the previous post. 

## Add approval gates

The final step is to add an approval gate to our deploy stage which will deploy the Bicep template with the desired infrastructure. 

Paste the following code after the preview stage: 

```yaml
- stage: Deploy
  jobs:
    - deployment: Deploy
      environment: BicepEnvironment
      strategy:
        runOnce:
          deploy:
            steps:
            - checkout: self

            - task: AzureResourceManagerTemplateDeployment@3
              name: DeployBicepTemplate
              inputs:
                deploymentScope: 'Subscription'
                azureResourceManagerConnection: '$(azureServiceConnection)'
                action: 'Create Or Update Resource Group'
                location: '$(location)'
                templateLocation: 'Linked artifact'
                csmFile: '$(Build.SourcesDirectory)/$(templateFile)'
                csmParametersFile: '$(Build.SourcesDirectory)/$(templateParameterFile)'
                deploymentMode: 'Incremental'
                deploymentName: 'DeployPipelineTemplate'
```
You will notice that in order to use an environment for approvals we have added a deployment which is structured slightly differently from a regular job. An environment is simply a place where you deploy your solution and allows you to add approval gates and more. 

Let's run the pipeline and see the approval in action:


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t2nt413dauu00bozxfg1.png)

Because we are using an environment, we need to allow our pipeline permissions to deploy using the environment: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a0bfaf6bv2odlbjt7t4f.png)

Once we allow our pipeline access to the environment in Azure DevOps we can go ahead and approve the changes: 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1c58jqyg63hamdujkzfr.png)

Here is our Resource Group deployed in Azure: 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n1tj998201fd64cl35j7.png)

Here is the complete multi-stage pipeline. You can also find the code on my GitHub page [here](https://github.com/lakovmartin/azure/blob/main/arm/resource_group/azure-pipelines-full.yaml). 

```yaml
trigger:
- master

name: Deploy Bicep files


variables:
  vmImageName: 'ubuntu-latest'

  azureServiceConnection: 'devops-poc3-deploy-spi'
  location: 'australiaeast'
  templateFile: 'bicep/azure_resource_group/template.bicep'
  templateParameterFile: 'bicep/azure_resource_group/parameters.json'

pool:
  vmImage: $(vmImageName)

stages:  

- stage: Lint
  jobs:
  - job: LintCode
    displayName: Lint code
    steps:
      - script: |
          az bicep build --file $(Build.SourcesDirectory)/$(templateFile)
        name: LintBicepCode
        displayName: Run Bicep linter

- stage: Validate
  dependsOn: Lint
  jobs: 
  - job: ValidateBicepTemplate
    steps:
    - task: AzureResourceManagerTemplateDeployment@3
      inputs:
        deploymentScope: 'Subscription'
        azureResourceManagerConnection: '$(azureServiceConnection)'
        location: '$(location)'
        templateLocation: 'Linked artifact'
        csmFile: '$(Build.SourcesDirectory)/$(templateFile)'
        csmParametersFile: '$(Build.SourcesDirectory)/$(templateParameterFile)'
        deploymentMode: 'Validation'        

- stage: Preview
  dependsOn: Validate
  jobs: 
  - job: Preview
    steps:
    - task: AzureCLI@2
      inputs:
        azureSubscription: '$(azureServiceConnection)'
        scriptType: 'bash'
        scriptLocation: 'inlineScript'
        inlineScript: |
          az deployment sub what-if \
            --template-file $(Build.SourcesDirectory)/$(templateFile) \
            --parameters '$(Build.SourcesDirectory)/$(templateParameterFile)' \
            --location '$(location)'

- stage: Deploy
  jobs:
    - deployment: Deploy
      environment: BicepEnvironment
      strategy:
        runOnce:
          deploy:
            steps:
            - checkout: self

            - task: AzureResourceManagerTemplateDeployment@3
              name: DeployBicepTemplate
              inputs:
                deploymentScope: 'Subscription'
                azureResourceManagerConnection: '$(azureServiceConnection)'
                action: 'Create Or Update Resource Group'
                location: '$(location)'
                templateLocation: 'Linked artifact'
                csmFile: '$(Build.SourcesDirectory)/$(templateFile)'
                csmParametersFile: '$(Build.SourcesDirectory)/$(templateParameterFile)'
                deploymentMode: 'Incremental'
                deploymentName: 'DeployPipelineTemplate'

```

In a production environment you would also want to add some smoke testing or pester testing which can be easily achieved with PowerShell. 

Hope you enjoyed the read.