Blogs

Share on facebook
Share on twitter
Share on linkedin
Share on email

Azure Done Right Series: Azure DevOps ARM Test Toolkit and Artifacts between Stages

In my previous post we went through how to deploy ARM templates with Azure DevOps using GitHub Flow. Today we are expanding on that and going through some tips and gotchas around adding the ARM Test Toolkit for testing and passing artifacts between stages.

ARM Test Toolkit

When we setup our Azure Pipeline Template we added a simple ARM template validation test. While this is a good start we should be expanding on this and adding some more comprehensive tests. A good place to start for testing ARM templates is the ARM Test Toolkit. It is a PowerShell module that provides a series of test cases to run against your ARM templates and also allows you to add your own.

The only caveat is the PowerShell module is not currently available from the PowerShellGallery and you will need to download the module and manage it yourself.

Below is the changes i made to incorporate the ARM Test Toolkit.

  • Download the module and stored it in my repo (in my case I chose to store the module in a directory called “modules”)

  • Updated my Azure Pipeline Template to run all tests from the ARM Test Toolkit
parameters:
  serviceConnection: ""
  resourceGroup: ""
  location: ""
  templateFile: ""
  templateParametersFile: ""
  overrideParameters: ""
  deploymentMode: Validation

stages:
- stage: build
  displayName: Build  
  jobs:
  - job: build
    displayName: Build
    pool:
      vmimage: windows-2019
    steps:      
    - task: AzureResourceGroupDeployment@2
      displayName: 'Validate ARM Template'
      inputs:
        azureSubscription:  '${{parameters.serviceConnection}}'
        resourceGroupName: '${{parameters.resourceGroup}}'
        location: '${{parameters.location}}'
        csmFile: '${{parameters.templateFile}}'
        csmParametersFile: '${{parameters.templateParametersFile}}'
        overrideParameters: '${{parameters.overrideParameters}}'
        deploymentMode: '${{parameters.deploymentMode}}'

    - task: PowerShell@1
      displayName: 'Run Integration Tests'
      inputs:
        scriptType: inlineScript
        failOnStderr: true
        arguments: '-template ${{parameters.templateFile}}'
        inlineScript: |          
          param(
          [string]$template
          )
          $ErrorActionPreference = 'Stop' 
          import-module .\modules\arm-ttk\arm-ttk.psd1
          Test-AzTemplate -TemplatePath .\$template

    - task: PowerShell@1
      displayName: 'Get ARM Template directory'
      inputs:
        scriptType: inlineScript
        arguments: '-directory ${{parameters.templateFile}}'
        inlineScript: |
          param(
          [string]$directory
          ) 
          $date = get-date -Format yyyy-MM-dd-hhmmss
          $templateDirectory = $directory.Substring(0,$directory.LastIndexOf("/"))
          write-output ("##vso[task.setvariable variable=templateDirectory;]$templateDirectory")
          write-output ("##vso[task.setvariable variable=artifactDate;]$date")
    
    - task: PublishPipelineArtifact@1
      displayName: 'Publish Pipeline Artifact'
      inputs:
        path: $(templateDirectory)
        artifact: drop_$(artifactDate)

Done! Now when my StorageAccounts-CI pipeline is kicked off a lot more tests are run.

Tips and Gotchas

One gotcha i ran into is that there could be a situation where a deployment of my template has been kicked off and is awaiting approval to deploy my production environment. In the meantime someone else has committed and merged a change, if i then approve the deployment to production it will use the latest artifact resulting in different code deployed to my environments.

So how do we ensure that our deployments to each environment use the same artifacts? The answer is simple, specify the artifact to download and use throughout each stage of the deployment.

While sounds easy its a bit more complex to execute, it will involve downloading the latest artifact, retrieving the artifact name and then storing the artifact name in a variable for each stage to reference. While this still may not seem overly complex to you here comes the next gotcha, you cannot share variables between stages in Azure DevOps (at the time of writing this article). Although we currently cannot share variables between stages there is a workaround, which is to store the variable in a file and download the file in each stage of your CD pipeline. Its not pretty but it works.

Below is the updates i made to my CD pipeline;

  • Added a “Build” stage to;Download the latest artifact
    • Get the latest artifact name and store the name of the artifact it in a file
    • Publish the file as an artifact so it will be available to download in other stages

NOTE I chose to store my file in the Build.ArtifactStagingDirectory as this directory is purged before each new build, so you don’t have to clean it up yourself.

stages:
  - stage: Build  
    jobs:
    - job: build
      displayName: Build
      pool:
        vmimage: windows-2019
      steps:
      - task: DownloadPipelineArtifact@2
        displayName: 'Download Pipeline Artifact'
        inputs:
          buildType: specific
          project: AzureFoundations
          definition: StorageAccounts-CI

      - powershell: |          
          $dir = '$(Build.ArtifactStagingDirectory)\variables\'
          $file = "sharedvariables.json"
          $artifactname = (get-item $(Pipeline.Workspace)\drop_*).name
          new-item $dir -itemType directory -force | out-null
          new-item ($dir + $file) -itemtype file -force | out-null
          set-content ($dir + $file) ('{ "latestArtifactName": "' + $artifactname + '" }') -force
        displayName: 'Create Shared Variables'
      
      - task: PublishPipelineArtifact@1
        displayName: 'Publish Shared Variables'
        inputs:
          path: $(Build.ArtifactStagingDirectory)/variables
          artifact: shared_variables
  • Updated each stage to;Download the file from Build.ArtifactStagingDirectory
    • Read the file and get the artifact name
    • Download the artifact using the name I got from my file
    • Deploy my ARM Template
  - stage: Development
    jobs:
      - deployment: storage_accounts_dev
        displayName: Deploy Storage Accounts
        pool:
          vmimage: windows-2019
        environment: Development
        strategy:
          runOnce:
            deploy:
              steps:
                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Shared Variables'
                  inputs:
                    artifactName: shared_variables
                    targetPath: $(Build.ArtifactStagingDirectory)

                - powershell: |                                                            
                    $vars = Get-Content '$(Build.ArtifactStagingDirectory)\*' | ConvertFrom-Json
                    $artifactname = $vars.latestArtifactName
                    write-output ("##vso[task.setvariable variable=artifactName;]$artifactname")                  
                  displayName: 'Define Deployment Artifact'

                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Latest Artifact'
                  inputs:
                    buildType: specific
                    project: AzureFoundations
                    definition: StorageAccounts-CI
                    artifactName: $(artifactName)
                    targetPath: $(Build.ArtifactStagingDirectory)/drop

                - task: AzureResourceGroupDeployment@2
                  displayName: Deploy Dev Diagnostic Storage Account
                  inputs:
                    azureSubscription: "$(serviceConnectionNpd)"
                    resourceGroupName: "$(rgDiagNpd)"
                    location: "$(primaryLoc)"
                    csmFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.json'
                    csmParametersFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.parameters.json'
                    overrideParameters: '-storageName "$(diagNpdStorageAccount)" -accountType "Standard_LRS" -accessTier "Hot" -kind "StorageV2" -softDeleteRetentionDays 7'        
                    deploymentMode: Validation

My entire CD pipeline now looks like this and will ensure deployments to each stage use the same artifacts.

trigger:
  branches:
    include:
      - master
  paths:
    include:
      - New-StorageAccount/*

name: StorageAccounts-CD

variables:
  - group: Variables - Storage

stages:
  - stage: Build  
    jobs:
    - job: build
      displayName: Build
      pool:
        vmimage: windows-2019
      steps:
      - task: DownloadPipelineArtifact@2
        displayName: 'Download Pipeline Artifact'
        inputs:
          buildType: specific
          project: AzureFoundations
          definition: StorageAccounts-CI

      - powershell: |          
          $dir = '$(Build.ArtifactStagingDirectory)\variables\'
          $file = "sharedvariables.json"
          $artifactname = (get-item $(Pipeline.Workspace)\drop_*).name
          new-item $dir -itemType directory -force | out-null
          new-item ($dir + $file) -itemtype file -force | out-null
          set-content ($dir + $file) ('{ "latestArtifactName": "' + $artifactname + '" }') -force
        displayName: 'Create Shared Variables'
      
      - task: PublishPipelineArtifact@1
        displayName: 'Publish Shared Variables'
        inputs:
          path: $(Build.ArtifactStagingDirectory)/variables
          artifact: shared_variables

  - stage: Development
    jobs:
      - deployment: storage_accounts_dev
        displayName: Deploy Storage Accounts
        pool:
          vmimage: windows-2019
        environment: Development
        strategy:
          runOnce:
            deploy:
              steps:
                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Shared Variables'
                  inputs:
                    artifactName: shared_variables
                    targetPath: $(Build.ArtifactStagingDirectory)

                - powershell: |                                                            
                    $vars = Get-Content '$(Build.ArtifactStagingDirectory)\*' | ConvertFrom-Json
                    $artifactname = $vars.latestArtifactName
                    write-output ("##vso[task.setvariable variable=artifactName;]$artifactname")                  
                  displayName: 'Define Deployment Artifact'

                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Latest Artifact'
                  inputs:
                    buildType: specific
                    project: AzureFoundations
                    definition: StorageAccounts-CI
                    artifactName: $(artifactName)
                    targetPath: $(Build.ArtifactStagingDirectory)/drop

                - task: AzureResourceGroupDeployment@2
                  displayName: Deploy Dev Diagnostic Storage Account
                  inputs:
                    azureSubscription: "$(serviceConnectionNpd)"
                    resourceGroupName: "$(rgDiagNpd)"
                    location: "$(primaryLoc)"
                    csmFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.json'
                    csmParametersFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.parameters.json'
                    overrideParameters: '-storageName "$(diagNpdStorageAccount)" -accountType "Standard_LRS" -accessTier "Hot" -kind "StorageV2" -softDeleteRetentionDays 7'        
                    deploymentMode: Validation

  - stage: NonProduction
    jobs:
      - deployment: storage_accounts_npd
        displayName: Deploy Storage Accounts
        pool:
          vmimage: windows-2019
        environment: Non-Production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Shared Variables'
                  inputs:
                    artifactName: shared_variables
                    targetPath: $(Build.ArtifactStagingDirectory)

                - powershell: |                                                            
                    $vars = Get-Content '$(Build.ArtifactStagingDirectory)\*' | ConvertFrom-Json
                    $artifactname = $vars.latestArtifactName
                    write-output ("##vso[task.setvariable variable=artifactName;]$artifactname")                  
                  displayName: 'Define Deployment Artifact'

                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Latest Artifact'
                  inputs:
                    buildType: specific
                    project: AzureFoundations
                    definition: StorageAccounts-CI
                    artifactName: $(artifactName)
                    targetPath: $(Build.ArtifactStagingDirectory)/drop

                - task: AzureResourceGroupDeployment@2
                  displayName: Deploy NonProd Diagnostic Storage Account
                  inputs:
                    azureSubscription: "$(serviceConnectionNpd)"
                    resourceGroupName: "$(rgDiagNpd)"
                    location: "$(primaryLoc)"
                    csmFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.json'
                    csmParametersFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.parameters.json'
                    overrideParameters: '-storageName "$(diagNpdStorageAccount)" -accountType "Standard_LRS" -accessTier "Hot" -kind "StorageV2" -softDeleteRetentionDays 7'        
                    deploymentMode: Incremental

  - stage: Production
    jobs:
      - deployment: storage_accounts_prd
        displayName: Deploy Storage Accounts
        pool:
          vmimage: windows-2019
        environment: Production
        strategy:
          runOnce:
            deploy:
              steps:
                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Shared Variables'
                  inputs:
                    artifactName: shared_variables
                    targetPath: $(Build.ArtifactStagingDirectory)

                - powershell: |                                                            
                    $vars = Get-Content '$(Build.ArtifactStagingDirectory)\*' | ConvertFrom-Json
                    $artifactname = $vars.latestArtifactName
                    write-output ("##vso[task.setvariable variable=artifactName;]$artifactname")                  
                  displayName: 'Define Deployment Artifact'

                - task: DownloadPipelineArtifact@2
                  displayName: 'Download Latest Artifact'
                  inputs:
                    buildType: specific
                    project: AzureFoundations
                    definition: StorageAccounts-CI
                    artifactName: $(artifactName)
                    targetPath: $(Build.ArtifactStagingDirectory)/drop

                - task: AzureResourceGroupDeployment@2
                  displayName: Deploy Prod Diagnostic Storage Account
                  inputs:
                    azureSubscription: "$(serviceConnectionPrd)"
                    resourceGroupName: "$(rgDiagPrd)"
                    location: "$(primaryLoc)"
                    csmFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.json'
                    csmParametersFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.parameters.json'
                    overrideParameters: '-storageName "$(diagPrdStorageAccount)" -accountType "Standard_LRS" -accessTier "Hot" -kind "StorageV2" -softDeleteRetentionDays 7'        
                    deploymentMode: Incremental

                - task: AzureResourceGroupDeployment@2
                  displayName: Deploy Shared Diagnostic Storage Account
                  inputs:
                    azureSubscription: "$(serviceConnectionPrd)"
                    resourceGroupName: "$(rgDiagShd)"
                    location: "$(primaryLoc)"
                    csmFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.json'
                    csmParametersFile: '$(Build.ArtifactStagingDirectory)/drop/New.StorageAccount.parameters.json'
                    overrideParameters: '-storageName "$(diagShdStorageAccount)" -accountType "Standard_LRS" -accessTier "Hot" -kind "StorageV2" -softDeleteRetentionDays 7'        
                    deploymentMode: Incremental  

Subscribe

Other Recent Blogs

Using Helm as a configuration manager for AKS

Helm is a package manager for Kubernetes that allows easier packaging, configuring and deployment of applications and services on your Kubernetes cluster, however it is not “only” a tool for application deployment. In this post I will discuss using it as a configuration and compliance management tool for AKS.

Read More »

Our Award Winning AI-Infused Solutions

Learn about the AI-infused solution we developed for Maurice Blackburn Lawyers, Australia’s leading social justice law firm. Leveraging Azure Cognitive Search and Services, the solution uses AI to review life insurance policies to assess rapidly whether a claim has merit and should be pursued. Our solution has helped the firm cut the time taken to assess a client’s entitlement to a superannuation disability insurance claim from months down to the span of the client’s initial phone. Our work with Maurice Blackburn Lawyers was recently featured across a range of Australian publications.

Read More »

Arinco PTY LTD
All Rights Reserved

Level 17, 303 Collins Street
Melbourne VIC 3000