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.

Fixing HTTP Error 401.2 unauthorized on local IIS

Sometimes the Windows Authentication got broken on IIS servers so you cannot log in locally on the server. In that case you get the dreadfully error message HTTP Error 401.2 - Unauthorized You are not authorized to view this page due to invalid authentication headers. To fix this issue you can repair the Windows Authentication feature with the following PowerShell commands: Remove-WindowsFeature Web-Windows-Auth Add-WindowsFeature Web-Windows-Auth

Make steps conditional in multi-stage YAML pipelines

To make the switch from the graphical release pipelines in Azure DevOps I am missing two features. The first one is to be able to defer a deploy and the second one is to exclude certain deployment steps without the need for editing the YAML file.  The defer option is something Microsoft has to solve in their Azure DevOps proposition. It's a feature which you have in the graphical release pipeline but what they have not implemented yet in their YAML pipeline replacement. Approvals and certain gate conditions are implemented on the environment but the defer option is still missing .  Pipeline The conditional deployment option can be implemented with the help of runtime parameters and expressions . In the parameter section you define boolean parameters which will control the deploy behavior. With the expressions you can control which stage/job/task should be executed when the pipeline runs. In the below YAML sample I experimented with conditions in the azure-pipelines.yml  file