Blogs

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  
[mailpoet_form id="1"]

Other Recent Blogs

Microsoft Teams IP Phones and Intune Enrollment

Microsoft Teams provides a growing portfolio of devices that can be used as desk and conference room phones. These IP phones run on Android 8.x or 9.x and are required to be enrolled in Intune. By default, these devices are enrolled as personal devices, which is not ideal as users should not be able to enrol their own personal Android devices.

Read More »

Level 9, 360 Collins Street, 
Melbourne VIC 3000

Level 2, 24 Campbell St,
Sydney NSW 2000

200 Adelaide St,
Brisbane QLD 4000

191 St Georges Terrace
Perth WA 6000

Level 10, 41 Shortland Street
Auckland

Part of

Arinco trades as Arinco (VIC) Pty Ltd and Arinco (NSW) Pty Ltd. © 2023 All Rights Reserved Arinco™ | Privacy Policy | Sustainability and Our Community
Arinco acknowledges the Traditional Owners of the land on which our offices are situated, and pay our respects to their Elders past, present and emerging.

Get started on the right path to cloud success today. Our Crew are standing by to answer your questions and get you up and running.