Skip to main content

Automated infrastructure (pre) deployment verification tests

I develop C# based web apps and integration solutions (APIs) for years and nowadays they are hosted in Azure. Most of the time I also create the infrastructure to deploy these kind of applications. In accordance with the Infrastructure as Code principles that I use, I do this using ARM templates supplemented with PowerShell (DSC) scripts. As a developer, I am used to writing unit tests to test my applications. Testing infrastructure is a new area for me. The traditional unit test approach does not work here because you cannot test an infrastructure until it has been rolled out.

Recently I have gained experience with the use of the Pester framework for performing (pre) deployment verification testing during the rollout of an IIS website environment. In this blog article I share some experiences I have gained during this project.

Case

The case in this case is the rollout of a WCF API hosted on an Azure VM on which no IIS web server is installed yet. This web service will run under a service account. The creation of the Azure VM, the DNS name and the service account are set up by another Dev/Ops team and therefore fall outside the responsibility of my CI/CD pipeline.

In terms of rollout, we have various pain points. Does the Azure VM meet the installation requirements? Is there enough disk space and internal memory available. Has the DNS name been created by the other team and is the required service account present and not disabled? In terms of connectivity, it can also be important that your API can call other APIs and databases. These are all things that can make the deployment fail or cause our own API not to work. These points must be verified beforehand, otherwise it makes no sense to allow the deployment to continue. All these checks fall into the category of pre deployment verification testing.

After installation, it is also important to check that our own API is installed properly. We must therefore check whether our API is running and whether it is responding. These kind of checks fall into the category of deployment verification testing.

Approach

Since we receive a bare Azure VM, we first have to apply our prerequisites. In this case, that is the configuration of the IIS server with a website that meets the application requirements and the installation of some PowerShell modules. Once the requirements are present, we can install the WCF API. For the installation we use PowerShell DSC which is executed by an Azure DevOps CD pipeline. The results of our (pre) depoyment verification tests will be published to our pipeline in the form of NUnit (or JUnit) reports.

The deployment order will be:
  • General phase
    • Pre-deployment verification
      • Check available storage
      • Check memory requirements
      • Check connectivity
    • Configure IIS and deploy PowerShell modules
    • Deployment verification
      • Check IIS settings
      • Check presence of PowerShell modules
  • Application specific phase
    • Pre-deployment verification
      • Check presence of DNS name
      • Check presence and state of Windows service account
    • Configure IIS website
    • Deployment verification
      • Check IIS website settings
    • Install website
    • Deployment verification
      • Check whether our API is running and whether it is responding.
Besides PowerShell DSC we use also the Pester framework. To use a current version on your computer execute the following command.
Install-Module -Name Pester -Force -SkipPublisherCheck
For general information about Pester read their Wiki page.

General phase

Pre-deployment verification

To execute these tests we create a PowerShell file with the name General.PreDeploymentVerification.Tests.ps1. The .Tests.ps1 part is manditory by the Pester framework. In this file we add our three tests in the Behavior-Driven Development (BDD) based syntax of Pester.
Describe -Name "General pre-deployment verification checks" -Fixture {
    Context -Name "Hardware" -Fixture {
        It -Name "Has at least 4 GB of total RAM" -Test {
            (Get-CimInstance Win32_PhysicalMemory | Measure-Object -Property Capacity -Sum).Sum /1GB |
            Should -Not -BeLessThan 4
        }

        It -Name "Has at least 5 GB of free disk space on the C: drive" -Test {
            $disk = Get-WMIObject -Class Win32_LogicalDisk |
                Where-Object DeviceID -eq "C:" |
                Select-Object DeviceID, @{'Name'='Freespace'; 'Expression'={ [Math]::Truncate($_.Freespace/1GB) }}

            $disk.Freespace |
            Should -Not -BeLessThan 5
        }
    }

    Context -Name "Networking" -Fixture {        
        It -Name "Has internet connectivity" -Test {
            (Test-NetConnection -CommonTCPPort HTTP).TcpTestSucceeded |
            Should -BeTrue
        }

        # Add other more specific checks below using commands like 'Test-NetConnection -ComputerName www.google.com -Port 443'.
    }
}

Configure IIS and deploy PowerShell modules

First we install some necessary PowerShell modules. One of the modules that we will soon need is the xWebAdministration module for configuring our IIS website. For the real project I created a PowerShell module which contains commandlets for deploying all of the individual modules this project needed. Below I will give a sample for the installation of the xWebAdministration PowerShell DSC module.
function Install-xWebAdministration {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory=$true)]
        [string] $Version
    )

    if (-not (Get-Module -Name "xWebAdministration" -ListAvailable -ErrorAction SilentlyContinue | Where-Object { $_.Version -eq $Version })) {
        $Versions = (Get-InstalledModule "xWebAdministration" -AllVersions -ErrorAction SilentlyContinue | Select-Object Version)
        $Versions | ForEach { 
            Uninstall-AllModules -TargetModule "xWebAdministration" -Version ($_.Version) -Force
        }

        Install-Module -Name "xWebAdministration" -RequiredVersion $Version -Scope AllUsers -Force -AllowClobber
        Write-Verbose "xWebAdministration module version '$Version' installed on machine."
    }
}
This function checks if the supplied version of xWebAdministration is installed on the computer. If not it will install it, if an older version is found it will remove that version and install the requested version. The code of the Uninstall-AllModules commandlet you can find on the Uninstall Azure PowerShell page of Microsoft. With the command Install-xWebAdministration -Version "3.0.0.0" we install the requested version we wil use in the rest of this blog post.

Next we will create a PowerShell DSC file called General.IISConfigurationDsc.ps1. In this file we define the desired state of our basic IIS configuration.
Configuration WebConfiguration
{
    Import-DscResource -ModuleName "PSDesiredStateConfiguration"

    Node $AllNodes.NodeName
    {
        WindowsFeature "Web-Server"
        {
            Ensure = "Present"
            Name   = "Web-Server"
        }

        WindowsFeature "Web-Dyn-Compression"
        {
            Ensure = "Present"
            Name   = "Web-Dyn-Compression"
        }

        WindowsFeature "Web-Windows-Auth"
        {
            Ensure = "Present"
            Name   = "Web-Windows-Auth"
        }

        # Disable ASP.NET 3.5.
        WindowsFeature "Web-Asp-Net"
        {
            Ensure = "Absent"
            Name   = "Web-Asp-Net"
        }

        WindowsFeature "NET-HTTP-Activation"
        {
            Ensure = "Absent"
            Name   = "NET-HTTP-Activation"
        }

        # Enable ASP.Net 4.x.
        WindowsFeature "Web-Asp-Net45"
        {
            Ensure = "Present"
            Name   = "Web-Asp-Net45"
        }

        WindowsFeature "NET-WCF-HTTP-Activation45"
        {
            Ensure = "Present"
            Name   = "NET-WCF-HTTP-Activation45"
        }
     }
}

$exportPath = Join-Path $PSScriptRoot "IISConfigurationDsc"

WebConfiguration -OutputPath $exportPath -ConfigurationData @{ AllNodes = @( @{ NodeName = $env:COMPUTERNAME; PSDscAllowPlainTextPassword = $true ; PSDscAllowDomainUser = $true } ) }
Start-DscConfiguration -Path $exportPath -Wait -Verbose:$VerbosePreference -Force

# Cleanup mof files
Remove-Item -Path $exportPath -Recurse

Deployment verification

To execute these tests we create a PowerShell file with the name General.DeploymentVerification.Tests.ps1. We are going to check that the PowerShell modules we need are available and that IIS is available and configured correctly.
Describe -Name "General deployment verification checks" -Fixture {
    Context -Name "PowerShell" -Fixture {
        It -Name "Has PowerShell version 4.0 or later" -Test {
            $PSVersionTable.PSVersion -ge [System.Version] "4.0" |
            Should -BeTrue
        }

        It -Name "Has the xWebAdministration module installed" -Test {
            Get-Module -Name "xWebAdministration" -ListAvailable |
            Should -Not -BeNullOrEmpty
        }
    }

    Context -Name "IIS" -Fixture {
        It -Name "World Wide Web Publishing Service (W3SVC) is running" -Test {
            (Get-Service -Name W3SVC).Status |
            Should -Be Running
        }

        It -Name "ASP.NET 3.5 is not installed" -Test {
            (Get-WindowsFeature -Name "Web-Asp-Net").InstallState |
            Should -Be Available
        }

        It -Name "ASP.NET 4.x is installed" -Test {
            (Get-WindowsFeature -Name "Web-Asp-Net45").InstallState |
            Should -Be Installed
        }
    }
}
If none of these tests fail we can continue the installation of our application.

Application specific phase

Pre-deployment verification

To execute these tests we create a PowerShell file with the name Application.PreDeploymentVerification.Tests.ps1. With these tests we are going to test if the DNS name is configured by the other team and that the service account we need is created and enabled.

This Pester file will use the PowerShell data file we will use to create the website of the WCF API as its input source.
[CmdletBinding(SupportsShouldProcess)]
param (
    [Parameter(Mandatory = $true)]
    [string] $ConfigurationFile
)

$ConfigurationFileFolder = Split-Path -Path (Resolve-Path $ConfigurationFile)

# Import configuration information
$node = (Import-PowerShellDataFile -Path $ConfigurationFile).AllNodes

Describe -Name "IIS prerequisites" -Fixture {

    $testCases = @()

    ForEach ($Site in $node.Sites) {
        $testCase = @{}
        $testCase.Add("AccountName", $Site.Pool.AccountName)
        $testCase.Add("HostName", $Site.BindingInformation.HostName)
 
        $testCases += $testCase
    }

    It -Name "The AD account '<AccountName>' exist in the AD domain." -TestCases $testCases {
        param ($AccountName)

        Get-ADUser -Filter { UserPrincipalName -eq $AccountName } |
        Should -Not -BeNullOrEmpty
    }

    It -Name "The AD account '<AccountName>' is enabled." -TestCases $testCases {
        param ($AccountName)

        (Get-ADUser -Filter { UserPrincipalName -eq $AccountName }).Enabled |
        Should -BeTrue
    }

    It -Name "The DNS name '<HostName>' can be resolved." -TestCases $testCases {
        param ($HostName)

        Resolve-DnsName $HostName |
        Should -Not -BeNullOrEmpty
    }
}
If none of these tests fail the creation of the website will succeed.

Configure IIS website

I use a PowerShell data file to store the definition of the IIS website. Create a PowerShell file with the name Application.IISWebsite.psd1. Below a sample of the definition of the IIS configuration of our sample WCF API.
@{
    AllNodes =  @(
        @{
            # Node name
            NodeName = "localhost"

            # See: https://blogs.technet.microsoft.com/ashleymcglone/2015/12/18/using-credentials-with-psdscallowplaintextpassword-and-psdscallowdomainuser-in-powershell-dsc-configuration-data/
            PSDscAllowPlainTextPassword = $true 
            PSDscAllowDomainUser = $true

            # Define root path for IIS Sites
            RootPath = "C:\inetpub\wwwroot"

            # Define IIS Sites
            Sites = @(
                @{
                    Ensure                       = "Present"
                    Name                         = "WCFSample"
                    State                        = "Started"
                    BindingInformation = @(
                        @{
                            Port                 = "443"
                            Protocol             = "HTTPS"
                            HostName             = "wcfsample.acme.org"
                            CertificateSubject   = "wcfsample.acme.org"
                            CertificateStoreName = "My"
                            SslFlags             = 1
                        }
                    )
                    Pool = @{
                        Name                     = "WCFSample"
                        Pipeline                 = "Integrated"
                        RuntimeVersion           = "v4.0"
                        State                    = "Started"
                        IdentityType             = "SpecificUser"
                        AccountName              = "WCFSample@acme.org"
                    }
                    Authentication = @{
                        Windows                  = $true
                        Anonymous                = $false
                    }
                    TestUris = @(
                        @{
                            Uri                  = "https://wcfsample.acme.org/CalculatorService.svc"
                            StatusCode           = 200
                        }
                    )
                }
             )
        }
    )
}
For the actual configuration we create a PowerShell DSC file called Application.IISWebsiteDsc.ps1. The sample below is a stripped down version of the one I used for the project. The script is expecting that the HTTPS certificate is present on the Azure VM. Install it scripted (from Azure Key Vault for example) within the general phase.
[CmdletBinding(SupportsShouldProcess)]
param
(
    [Parameter(Mandatory = $true)]
    [string] $ConfigurationDataFileName
)

Configuration WebConfigurationWebsite
{
    # Import the module that contains the resources we're using.
    Import-DscResource -ModuleName "PSDesiredStateConfiguration"
    Import-DscResource -ModuleName "xWebAdministration"

    Node $AllNodes.NodeName
    {
        ForEach ($Site in $Node.Sites)
        {
            # Create the folder.
            File $Site.Name
            {
                Ensure                 = $(if ($Site.Ensure) { $Site.Ensure } else { "Present" })
                Type                   = "Directory"
                Force                  = $true
                DestinationPath        = "$($Node.RootPath)\$($Site.Name)"
            }

            if ($Site.Pool.AccountName) {
                $password = ... Retrieve password from Azure Key Vault here based on the account name ...
                $credential = New-Object System.Management.Automation.PSCredential -ArgumentList $Site.Pool.AccountName, $password.SecretValue
            }

            xWebAppPool $Site.Name
            {
                Ensure                = $(if ($Site.Ensure) { $Site.Ensure } else { "Present" })
                Name                  = $(if ($Site.Pool.Name) { $Site.Pool.Name } else { $Site.Name })
                State                 = $Site.Pool.State

                # General
                managedPipelineMode   = "$($Site.Pool.Pipeline)"
                managedRuntimeVersion = "$($Site.Pool.RuntimeVersion)"

                # Process Model
                identityType          = $(if ($Site.Pool.IdentityType) { $Site.Pool.IdentityType } else { "ApplicationPoolIdentity" })
                Credential            = $(if ($Site.Pool.AccountName) { $credential } else { $null } )
            }

            # Create the IIS site.
            xWebSite $Site.Name
            {
                Ensure          = $(if ($Site.Ensure) { $Site.Ensure } else { "Present" })
                Name            = $Site.Name
                PhysicalPath    = Join-Path $Node.Rootpath $Site.Name
                State           = $Site.State

                # Configure the site bindings.
                BindingInfo     = @(
                    ForEach ($BindingInfo in $Site.BindingInformation)
                    {
                        if ($BindingInfo.CertificateSubject) {
                            MSFT_xWebBindingInformation
                            {
                                Protocol             = $BindingInfo.Protocol
                                IPAddress            = $BindingInfo.IPAddress
                                Port                 = $BindingInfo.Port
                                HostName             = $BindingInfo.HostName
                                CertificateSubject   = $BindingInfo.CertificateSubject
                                CertificateStoreName = $BindingInfo.CertificateStoreName
                                SslFlags             = $BindingInfo.SslFlags
                            }
                        }
                        else {
                            MSFT_xWebBindingInformation
                            {
                                Protocol  = $BindingInfo.Protocol
                                IPAddress = $BindingInfo.IPAddress
                                Port      = $BindingInfo.Port
                                HostName  = $BindingInfo.HostName
                            }
                        }
                    }
                )

                ApplicationPool = $(if ($Site.Pool.Name) { $Site.Pool.Name } else { $Site.Name })

                # Set Authentication mechanisms.
                AuthenticationInfo = MSFT_xWebAuthenticationInformation {
                    Anonymous = $Site.Authentication.Anonymous
                    Windows   = $Site.Authentication.Windows
                }
            }
        }
    }
}

$configurationDataPath = Join-Path $PSScriptRoot $ConfigurationDataFileName
$exportPath = Join-Path $PSScriptRoot (Get-Item $configurationDataPath ).Basename

WebConfigurationWebsite -OutputPath $exportPath -ConfigurationData $PSScriptRoot/$ConfigurationDataFileName
Start-DscConfiguration -Path $exportPath -Wait -Verbose:$VerbosePreference -Force

# Cleanup mof files
Remove-Item -Path $exportPath -Recurse

Deployment verification

To execute these tests we create a PowerShell file with the name Application.IISWebsiteDsc.DeploymentVerification.Tests.ps1. We are going to check if the application pool and the website are configured correctly.
[CmdletBinding(SupportsShouldProcess)]
param (
    [Parameter(Mandatory = $true)]
    [string] $ConfigurationFile
)

$ConfigurationFileFolder = Split-Path -Path (Resolve-Path $ConfigurationFile)

# Import module(s)
Import-Module (Join-Path -Path $ConfigurationFileFolder -ChildPath "Functions-WebDeploy.psm1") -Verbose:$VerbosePreference

# Import configuration information
$node = (Import-PowerShellDataFile -Path $ConfigurationFile).AllNodes

Describe -Name "IIS" -Fixture {

    Context -Name "Application pool" -Fixture {
        $testCases = @()

        ForEach ($Site in $node.Sites) {
            $testCase = @{}
            $testCase.Add("Name", $Site.Pool.Name)
            $testCase.Add("ManagedPipelineMode", $Site.Pool.Pipeline)
            $testCase.Add("State", $Site.Pool.State)

            $testCases += $testCase
        }

        It -Name "The application pool '<name>' should exist." -TestCases $testCases {
            param ($Name, $State)

            Get-IISAppPool -Name $Name |
            Should -Not -BeNullOrEmpty
        }

        It -Name "The application pool '<name>' managed pipeline mode is configured correctly." -TestCases $testCases {
            param ($Name, $ManagedPipelineMode)

            (Get-IISAppPool -Name $Name).ManagedPipelineMode |
            Should -Be $ManagedPipelineMode
        }
    }

    Context -Name "Sites" -Fixture {
        $testCases = @()

        ForEach ($Site in $node.Sites) {
            $testCase = @{}
            $testCase.Add("Name", $Site.Name)
            $testCase.Add("PhysicalPath", Join-Path $node.Rootpath $Site.Name)
            $testCase.Add("AuthenticationWindows", $(if ($Site.Authentication.Windows) { $Site.Authentication.Windows } else { $false } ))

            $testCases += $testCase
        }

        It -Name "The site '<name>' should exist." -TestCases $testCases {
            param ($Name, $State)

            Get-IISSite -Name $Name |
            Should -Not -BeNullOrEmpty
        }

        It -Name "The physical path of site '<name>' is configured correctly." -TestCases $testCases {
            param ($Name, $PhysicalPath)

            (Get-IISSite -Name $Name).Applications["/"].VirtualDirectories["/"].PhysicalPath |
            Should -Be $PhysicalPath
        }

        It -Name "The authentication setting of site '<name>' for windows authentication is configured correctly." -TestCases $testCases {
            param ($Name, $AuthenticationWindows)

            (Get-WebConfigurationProperty -Filter system.webServer/security/authentication/windowsAuthentication -PSPath IIS:\Sites\$Name -Name Enabled).Value |
            Should -Be $AuthenticationWindows
        }
    }

    Context -Name "Bindings" -Fixture {
        $testCases = @()

        ForEach ($Site in $node.Sites) {
            ForEach ($Binding in $Site.BindingInformation) {
                $testCase = @{}
                $testCase.Add("Name", $Site.Name)
                $testCase.Add("BindingInformation", "*:" + $Binding.Port + ":" + $Binding.HostName)
                $testCase.Add("CertificateSubject", $Binding.CertificateSubject)

                $flag = switch ($Binding.SslFlags) {
                    0 { "None"; break}
                    1  { "Sni"; break}
                    2 { "CentralCertStore"; break}
                    3 { "Sni, CentralCertStore"; break}
                    default { "Unknown"; break}
                }

                $testCase.Add("SslFlags", $flag)

                $testCases += $testCase
            }
        }

        It -Name "The binding '<bindinginformation>' exists for site '<name>'." -TestCases $testCases {
            param ($Name, $BindingInformation)

            Get-IISSiteBinding -Name $Name -BindingInformation $BindingInformation |
            Should -Not -BeNullOrEmpty
        }

        It -Name "The host name for binding '<bindinginformation>' of site '<name>' is configured correctly." -TestCases $testCases {
            param ($Name, $BindingInformation, $HostName)

            (Get-IISSiteBinding -Name $Name -BindingInformation $BindingInformation).Host |
            Should -Be $HostName
        }

        It -Name "The correct client certificate for binding '<bindinginformation>' of site '<name>' is used." -TestCases $testCases {
            param ($Name, $BindingInformation, $CertificateSubject)

            if ($CertificateSubject) {
                $Certifcate = Get-ClientCertificate -CommonName $CertificateSubject

                (Get-IISSiteBinding -Name $Name -BindingInformation $BindingInformation).CertificateHash |
                Should -Be $Certifcate.GetCertHash()
            }
            else {
                Set-ItResult -Skipped -Because "it's no HTTPS binding"
            }
        }
    }
}
If these tests of our settings are not failing we can start the deployment of our WCF API.

Install website

We can now deploy the WCF API with a web deployment ZIP file in our Azure DevOps CD pipeline.

Deployment verification

To execute these tests we create a PowerShell file with the name Application.DeploymentVerification.Tests.ps1. We are going to check if the endpoint of our WCF API is responding.
[CmdletBinding(SupportsShouldProcess)]
param (
    [Parameter(Mandatory = $true)]
    [string] $ConfigurationFile
)

# Import configuration information
$node = (Import-PowerShellDataFile -Path $ConfigurationFile).AllNodes

Describe -Name "IIS" -Fixture {

    Context -Name "Ports" -Fixture {
        $testCases = @()

        ForEach ($Site in $node.Sites) {
            ForEach ($Binding in $Site.BindingInformation) {
                $testCase = @{}
                $testCase.Add("Protocol", $Binding.Protocol)
                $testCase.Add("Port", $Binding.Port)
                $testCase.Add("HostName", $(if ($Binding.HostName) { $Binding.HostName } else { "localhost" }))

                $testCases += $testCase
            }
        }

        if ($testCases.Count -gt 0) {
            It -Name "Given the hostname '<HostName>' the port <Port> (<Protocol>) should be active" -TestCases $testCases {
                param ($HostName, $Protocol, $Port)

                (Test-NetConnection -ComputerName $HostName -Port $Port).TcpTestSucceeded | Should -BeTrue
            }
        }
    }

    Context -Name "Uris" -Fixture {
        $testCases = @()

        ForEach ($Site in $node.Sites) {
            ForEach ($TestUri in $Site.TestUris) {
                $testCase = @{}
                $testCase.Add("Uri", $TestUri.Uri)
                $testCase.Add("StatusCode", $TestUri.StatusCode)

                switch ($TestUri.StatusCode) {
                    200  { $description = "OK"; break}
                    401  { $description = "Unauthorized"; break}
                    403  { $description = "Forbidden"; break}
                    404  { $description = "Not Found"; break}
                    500  { $description = "Internal Server Error"; break}
                    default { $description = ""; break}
                }

                $testCase.Add("StatusCodeDescription", $description)

                $testCases += $testCase
            }
        }

        It -Name "Given the uri '&lt;Uri&gt;' the returned status code should be &lt;StatusCode&gt; (&lt;StatusCodeDescription&gt;)" -TestCases $testCases {
            param ($Uri, $StatusCode, $StatusCodeDescription)

            try {
                 $response = Invoke-WebRequest -Uri $Uri -Method Get -MaximumRedirection 1 -ErrorAction Stop -UseDefaultCredentials -TimeoutSec 120 -UseBasicParsing

                # This will only execute if the Invoke-WebRequest is successful.
                $actualStatusCode = $response.StatusCode
            }
            catch {
                $actualStatusCode = $_.Exception.Response.StatusCode.value__

                if ($actualStatusCode -eq $null) {
                    $actualStatusCode = $_.Exception
                }
            }

            $actualStatusCode | Should -Be $StatusCode
        }
    }
}
If our endpoint is responding we know that the installation of the WCF API is done succesfully.

CD pipeline

All these individual PowerShell (DSC) scripts can be executed from an Azure DevOps pipeline by using the (Azure) PowerShell tasks. Publishing from the Pester test results can be done by uploading the report which is generated by commands like Invoke-Pester -OutputFile "./TestResults.xml" -OutputFormat NUnitXml with the Publish Test Results task.

Comments

Popular posts from this blog

CS8357: The specified version string contains wildcards, which are not compatible with determinism.

Today I was busy with creating a WCF service solution in Visual Studio Enterprise 2017 (15.9.2). In this solution I use a few C# class libraries based on .NET 4.7.2. When I compiled the solution I got this error message:
Error CS8357: The specified version string contains wildcards, which are not compatible with determinism. Either remove wildcards from the version string, or disable determinism for this compilation The error message is linking to my AssemblyInfo.cs file of the Class library projects. In all the projects of this solution I use the wildcard notation for generating build and revision numbers.
// Version information for an assembly consists of the following four values: // // Major Version // Minor Version // Build Number // Revision // // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyVersion("1.0.*&qu…

Deploy with ARM templates an Azure DevTest Lab environment

Within the company I work for we use Azure DevTest Labs already for a year to host our personal development computer and the BizTalk CI machines we need. We are planning to host also our development and test environments in Lab environments. At the moment we configure the lab environments manually.

I invested some time to write ARM templates to deploy Azure DevTest Labs within 5 minutes based on Azure DevOps CI/CD pipelines. In the process of writing these scripts I learned a lot. For example that the Azure documentation is not always up to date and that includes also tooling like Azure Resource Explorer. Based on querying the REST API with Postman I found the missing pieces for deploying a DevTest Lab environment based on nested ARM templates.

In this post I will document the nested ARM template I created the last few days. Lab With the first template you can create the lab environment itself and configure the following parts of the environment
Lab settings; like the storage which th…

Assign an existing certificate to your IIS website with WiX

Recently I had to change the bindings of existing IIS hosted websites and APIs from HTTP to HTTPS. They are installed with a MSI file created with the WiX Toolset.

Because I have to use an already on the server installed certificate I cannot use the Certificate element from the IIS Extension because this element only supports installing and uninstalling certificates based on PFX files. After doing some research I found the blog article Assign Certificate (Set HTTPS Binding certificate) to IIS website from Wix Installer which described the usage of Custom Actions for this purpose. I adopted this approach and rewrote the code for my scenario.

With WiX I still create the website.
<iis:WebSite Id="WebSite" ConfigureIfExists="yes" AutoStart="yes" Description="MyWebsite" Directory="IISROOT" StartOnInstall="yes"> <iis:WebAddress Id="WebSite" …