How do you create a Parameter File for your Azure Resource Manager template deployment? In this post we are going to create a Parameter File generator using PowerShell Classes. Furthermore we are looking into testing and validating your PowerShell Classes implementation.

Learn Classes

If you are not yet familiar with the concepts of PowerShell Classes I can highly recommend the PSConfEU 2019 Video: Learn Classes with Class {} from @Stephanevg go to Video

Parameter File Generator

I was looking for a way to create Azure Resource Manager template parameter files based on a given ARM template that can be used like this New-ParameterFile to jump to the code go to Implementation or download New-ParameterFile.ps1.

New-ParameterFile
<#
{
  "schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contenVersion": "1.0.0.0",
  "parameters": {
    "networkAcls": {
      "value": "Prompt"
    },
    "storageAccountAccessTier": {
      "value": "Prompt"
    },
    "storageAccountSku": {
      "value": "Prompt"
    },
    "location": {
      "value": "Prompt"
    },
    "resourceName": {
      "value": "Prompt"
    }
  }
}
#>

A parameter file contains sets of parameters that are used together with the Azure Resource Manager Template. However, a TemplateFile can specify parameters with a defaultValue, which is used for the deployment.

The New-AzResourceGroupDeployment can be executed with two parameters, e.g. -ResourceGroupName and -TemplateFile. Additionally you can overwrite default values by passing -TemplateParameterFile and a parameter file.

$Deployment = @{
    ResourceGroupName = "MyResourceGroup"
    TemplateFile = (Join-Path $PSScriptRoot "azuredeploy.json").FullName
    TemplateParameterFile = (Join-Path $PSScriptRoot "azuredeploy.parameters.json").FullName
}
New-AzResourceGroupDeployment @Deployment

Also you cann add dynamic parameters to the deployment. This object will set the parameters dynamically at execution time. Object passed to the function will add dynamic parameters to the deployment.

# Object will set the Parameter1 value to $Value using dynamic created parameter
# This object will override the Parameter1 Value if it is set in TemplateParameterFile too.

$Object = @{
    Parameter1 = $Value # Set Parameter1
}

$Deployment = @{
    ResourceGroupName = "MyResourceGroup"
    TemplateFile = (Join-Path $PSScriptRoot "azuredeploy.json").FullName
    TemplateParameterFile = (Join-Path $PSScriptRoot "azuredeploy.parameters.json").FullName
    # Will override duplicates in TemplateParamterFile
}
New-AzResourceGroupDeployment @Deployment @Object

Implementation Considerations

With all that being said, we can consider not all parameters specified in an ARM template are needed for parameter file generation. To implement these requirements the code needed some flexibility and extensibility, so I opted for a class first code approach.

I created a Plain PowerShell Object based on the schema of the Azure Resource Manager Parameter template file. (The reference feature is not implemented to reduce complexity).

The ParamterObject class therefor implements the properties schema, contenVersion and parameters. As the file expects to have the parameters name as a key a PowerShell hashtable for each parameter is used. The hashtables key is the parameter name, this implementation converts nicely into the expected json when using ConverTo-Json.

A parameter file could contain all, none, a subset or only the mandatory parameters specified in a given ARM template. Having multiple different options for ParameterFiles the ParameterObject could be used as the abstract base class and concrete implementations could inherit the properties.

The ParameterFileGenerator takes a given template and exposes the builder method GenerateParameterFile. The method will create a parameter file string as json based on the template passed. In this implementation you can specify to create a file with mandatory parameters only or all parameters by specifying a flag.

Mandatory parameters are defined as parameters that do not implement the defaultValues property in the given ARM template.

Implementation

Usage

Now we created a function that can create Parameter Files of a given template. The usage is pretty straight forward.

# Functionality needs to dot source
. .\New-ParameterFile.ps1

New-ParameterFile
<#
{
  "schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contenVersion": "1.0.0.0",
  "parameters": {
    "networkAcls": {
      "value": "Prompt"
    },
    "storageAccountAccessTier": {
      "value": "Prompt"
    },
    "storageAccountSku": {
      "value": "Prompt"
    },
    "location": {
      "value": "Prompt"
    },
    "resourceName": {
      "value": "Prompt"
    }
  }
}
#>

New-ParameterFile -OnlyMandatoryParameter
<#
{
  "schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#",
  "contenVersion": "1.0.0.0",
  "parameters": {
    "resourceName": {
      "value": "Prompt"
    },
    "networkAcls": {
      "value": "Prompt"
    }
  }
}
#>

# Or specify a path to the ARM template manually
$AzureDeployPath = "azuredeploy.json"
New-ParameterFile -Path $AzureDeployPath
New-ParameterFile -Path $AzureDeployPath -OnlyMandatoryParameter

Test PowerShell classes

In order to test the functionality the whole implementation needs to be available in memory. So PowerShell is aware of the classes and the functions.

Using New-Fixture command from the Pester module the whole script will be dot sourced.

After the classes are available in memory we can instantiate the class and execute its functionality. As classes expect to implement the return, when not void, we can assert the functionality is executed as expected by asserting the returned value. Also, we can check the inner state by asserting the properties of the object.

Pester Output

The end user facing function should be tested thoroughly.

Get the New-ParameterFile.Tests.ps1

# New-ParameterFile.Tests.ps1

# this is created when running `New-Fixture`
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
. "$here\$sut"

Describe "Class ParameterFile" {

    [array]$Parameters = @(
        @{
            Name = "Test1"
        },
        @{
            Name = "Test2"
        },
        @{
            Name = "Test3"
        }
    )
    it "should create a ParameterFile object" {
        [ParameterFile]::new($Parameters).GetType() | Should -Be "ParameterFile"
    }

    it "should have schema" {
        [ParameterFile]::new($Parameters).Schema | Should -Be "https://schema.management.azure.com/schemas/2015-01-01/deploymentParameters.json#"
    }

    it "should have contenVersion" {
        [ParameterFile]::new($Parameters).contenVersion | Should -Be "1.0.0.0"
    }

    it "should have parameters" {
        [ParameterFile]::new($Parameters).parameters | Should -Not -BeNullOrEmpty
    }
}


Describe "Class ParameterFileGenerator" {

    it "should create a ParameterFileGenerator object" {
        [ParameterFileGenerator]::new("$here\azuredeploy.json").GetType() | Should -Be "ParameterFileFactory"
    }

    it "should have template" {
        [ParameterFileGenerator]::new("$here\azuredeploy.json").template | Should -Not -BeNullOrEmpty
    }

    it "should have Parameter" {
        [ParameterFileGenerator]::new("$here\azuredeploy.json").Parameter | Should -Not -BeNullOrEmpty
        [ParameterFileGenerator]::new("$here\azuredeploy.json").Parameter.Count | Should -BeGreaterOrEqual 5
    }

    it "should have MandatoryParameter" {
        [ParameterFileGenerator]::new("$here\azuredeploy.json").MandatoryParameter | Should -Not -BeNullOrEmpty
        [ParameterFileGenerator]::new("$here\azuredeploy.json").MandatoryParameter.Count | Should -Be 2
    }

    it "should create ParameterFile" {
        $ParameterFile = [ParameterFileGenerator]::new("$here\azuredeploy.json").GenerateParameterFile($false)
        $ParameterFile.GetType() | Should -Be "ParameterFile"
        $ParameterFile | Should -Not -BeNullOrEmpty
    }
}


Describe ".\New-ParameterFile" {
    context "Valid Public Function Tests" {

        # Execute the user facing command first, as we want to make sure the user can run it
        $ParameterFile = New-ParameterFile

        # The command will return a JSON, so we convert it to assert on it
        $Json = $ParameterFile | ConvertFrom-Json -ErrorAction Stop -ErrorVariable JsonException

        # Basic sanity assertion to check if valid json is returned
        It "should create valid json" {
            $JsonException | Should -BeNullOrEmpty
        }

        $TestCases = @(
            @{
                Property = "Schema"
            },
            @{
                Property = "contenVersion"
            },
            @{
                Property = "parameters"
            }
        )
        it "should have <Property> " -TestCases $TestCases {
            Param(
                $Property
            )
            $Json.$Property | Should -Not -BeNullOrEmpty
        }


        $TestCases = @(
            @{
                Parameter = "networkAcls"
            },
            @{
                Parameter = "resourceName"
            },
            @{
                Parameter = "storageAccountSku"
            },
            @{
                Parameter = "location"
            }
        )
        it "should have <Parameter> with value Prompt" -TestCases $TestCases {
            Param(
                $Parameter
            )
            $Json.Parameters.$Parameter | Should -Not -BeNullOrEmpty
            $Json.Parameters.$Parameter.Value | Should -Be "Prompt"
        }
    }
}

In order to validate the code I used the example ADLS Gen2 azuredeploy.json.

Video

Remarks

Table of contents