In Azure DevOps we’ve covered plenty with linux deployments and containerized workloads.  But at times we may need to deploy to a set of Windows hosts, especially if we need to support IIS workloads.  Let’s explore using Virtual Machines with tags in Environments and Azure YAML Pipelines.

Setup

Lets create a RG and some windows VMs

$ az group create --name winVMrg --location eastus
{
  "id": "/subscriptions/70b4asdf-asdf-asdf-asdf-asdf95b1aca8/resourceGroups/winVMrg",
  "location": "eastus",
  "managedBy": null,
  "name": "winVMrg",
  "properties": {
    "provisioningState": "Succeeded"
  },
  "tags": null,
  "type": "Microsoft.Resources/resourceGroups"
}

Then lets create a VM and open port 80

$ az vm create --resource-group winVMrg --name winVm1 --image win2016datacenter --admin-username azureuser
Admin Password: 
Confirm Admin Password: 
{- Finished ..
  "fqdns": "",
  "id": "/subscriptions/70b4asdf-asdf-asdf-asdf-asdf95b1aca8/resourceGroups/winVMrg/providers/Microsoft.Compute/virtualMachines/winVm1",
  "location": "eastus",
  "macAddress": "00-22-48-20-26-29",
  "powerState": "VM running",
  "privateIpAddress": "10.0.0.4",
  "publicIpAddress": "40.117.101.109",
  "resourceGroup": "winVMrg",
  "zones": ""
}

$ az vm create --resource-group winVMrg --name winVm2 --image win2016datacenter --admin-username azureuser
Admin Password: 
Confirm Admin Password: 
{- Finished ..
  "fqdns": "",
  "id": "/subscriptions/70b4asdf-asdf-asdf-asdf-asdf95b1aca8/resourceGroups/winVMrg/providers/Microsoft.Compute/virtualMachines/winVm2",
  "location": "eastus",
  "macAddress": "00-0D-3A-9B-24-D9",
  "powerState": "VM running",
  "privateIpAddress": "10.0.0.5",
  "publicIpAddress": "168.61.36.121",
  "resourceGroup": "winVMrg",
  "zones": ""
}

Enabling IIS

$ az vm open-port --port 80 -g winVMrg -n winVm1
{- Finished ..
  "defaultSecurityRules": [
...

$ az vm open-port --port 80 -g winVMrg -n winVm2
{- Finished ..
  "defaultSecurityRules": [
...

We now have VM1 at 40.117.101.109 and VM2 at 168.61.36.121

We can now RDC with the mstsc command mstsc /v:$IP

In an admin command prompt, install IIS Install-WindowsFeature -name Web-Server -IncludeManagementTools

which is

Windows PowerShell
Copyright (C) 2016 Microsoft Corporation. All rights reserved.

PS C:\Users\azureuser> Install-WindowsFeature -name Web-Server -IncludeManagementTools

Success Restart Needed Exit Code      Feature Result
------- -------------- ---------      --------------
True    No             Success        {Common HTTP Features, Default Document, D...


PS C:\Users\azureuser>

AzDO Creating Environment

Then lets create an environment

Next we can pick windows and get the registration powershell

when copied it looks like this:

$ErrorActionPreference="Stop";If(-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() ).IsInRole( [Security.Principal.WindowsBuiltInRole] "Administrator")){ throw "Run command in an administrator PowerShell prompt"};If($PSVersionTable.PSVersion -lt (New-Object System.Version("3.0"))){ throw "The minimum version of Windows PowerShell that is required by the script (3.0) does not match the currently running version of Windows PowerShell." };If(-NOT (Test-Path $env:SystemDrive\'azagent')){mkdir $env:SystemDrive\'azagent'}; cd $env:SystemDrive\'azagent'; for($i=1; $i -lt 100; $i++){$destFolder="A"+$i.ToString();if(-NOT (Test-Path ($destFolder))){mkdir $destFolder;cd $destFolder;break;}}; $agentZip="$PWD\agent.zip";$DefaultProxy=[System.Net.WebRequest]::DefaultWebProxy;$securityProtocol=@();$securityProtocol+=[Net.ServicePointManager]::SecurityProtocol;$securityProtocol+=[Net.SecurityProtocolType]::Tls12;[Net.ServicePointManager]::SecurityProtocol=$securityProtocol;$WebClient=New-Object Net.WebClient; $Uri='https://vstsagentpackage.azureedge.net/agent/2.179.0/vsts-agent-win-x64-2.179.0.zip';if($DefaultProxy -and (-not $DefaultProxy.IsBypassed($Uri))){$WebClient.Proxy= New-Object Net.WebProxy($DefaultProxy.GetProxy($Uri).OriginalString, $True);}; $WebClient.DownloadFile($Uri, $agentZip);Add-Type -AssemblyName System.IO.Compression.FileSystem;[System.IO.Compression.ZipFile]::ExtractToDirectory( $agentZip, "$PWD");.\config.cmd --environment --environmentname "AzureWinVMSet" --agent $env:COMPUTERNAME --runasservice --work '_work' --url 'https://princessking.visualstudio.com/' --projectname 'StandupTime' --auth PAT --token qugkvy67hrd2ip43z4yp3a5nfmsf6blsxzu43nqvbfxbpe47yhka; Remove-Item $agentZip;

Note: the token is short lived - just 3h

We can set the runAs user to our admin user (so we can do administrative work)

In my second session, I’ll add some tags

We can now see them listed in our “Environment”

We can see the tags

Pipeline Setup

Let’s now create a GIT repo

We can setup a starter pipeline

# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
 
trigger:
- main
 
pool:
  vmImage: 'ubuntu-latest'
 
jobs:
- job: Build
  displayName: buildproject
  steps:
  - script: echo Hello, world!
    displayName: 'Run a one-line script'
 
  - script: |
      echo Add other tasks to build, test, and deploy your project.
      echo See https://aka.ms/yaml
    displayName: 'Run a multi-line script'
 
  - task: CopyFiles@2
    displayName: 'Copy Files to artifact staging directory'
    inputs:
      SourceFolder: '$(System.DefaultWorkingDirectory)'
      Contents: '**/*'
      TargetFolder: $(Build.ArtifactStagingDirectory)
 
  - upload: $(Build.ArtifactStagingDirectory)
    artifact: drop
 
- deployment: VMDeploy
  dependsOn: Build
  displayName: web
  environment:
    name: AzureWinVMSet
    resourceType: VirtualMachine
    tags: web
  strategy:
      rolling:
        maxParallel: 5  #for percentages, mention as x%
        preDeploy:
          steps:
          - download: current
            artifact: drop
          - script: dir
        deploy:
          steps:
          - task: PowerShell@2
            inputs:
              targetType: 'inline'
              script: |
                # Write your PowerShell commands here.
                
                Write-Host "Hello World"
              errorActionPreference: 'silentlyContinue'
        routeTraffic:
          steps:
          - script: echo routing traffic
        postRouteTraffic:
          steps:
          - script: echo health check post-route traffic
        on:
          failure:
            steps:
            - script: echo Restore from backup! This is on failure
          success:
            steps:
            - script: echo Notify! This is on success

What you’ll see is we zip up the build dir (arguably we likely would zip up the build contents of a .NET build) and then we deploy just to web tagged servers.

...

We can see that powershell ran on vm2:

And our post-traffic should it enabled IIS after deploy

2021-01-15T18:03:06.4076548Z ##[section]Starting: CmdLine
2021-01-15T18:03:07.1566823Z ==============================================================================
2021-01-15T18:03:07.1567269Z Task         : Command line
2021-01-15T18:03:07.1567602Z Description  : Run a command line script using Bash on Linux and macOS and cmd.exe on Windows
2021-01-15T18:03:07.1567818Z Version      : 2.178.0
2021-01-15T18:03:07.1568081Z Author       : Microsoft Corporation
2021-01-15T18:03:07.1568448Z Help         : https://docs.microsoft.com/azure/devops/pipelines/tasks/utility/command-line
2021-01-15T18:03:07.1568680Z ==============================================================================
2021-01-15T18:03:08.0338350Z Generating script.
2021-01-15T18:03:08.0339641Z Script contents:
2021-01-15T18:03:08.0439985Z echo health check post-route traffic
2021-01-15T18:03:08.0443367Z ========================== Starting Command Output ===========================
2021-01-15T18:03:08.0444891Z ##[command]"C:\windows\system32\cmd.exe" /D /E:ON /V:OFF /S /C "CALL "C:\azagent\A1\_work\_temp\cc18b726-200c-46b8-aead-bc26bea10bc1.cmd""
2021-01-15T18:03:08.0445264Z health check post-route traffic
2021-01-15T18:03:08.0462393Z ##[section]Finishing: CmdLine

You can use the IIS WebApp deploy task (https://docs.microsoft.com/en-us/azure/devops/pipelines/tasks/deploy/iis-web-app-deployment-on-machine-group?view=azure-devops)

# IIS web app deploy
# Deploy a website or web application using Web Deploy
- task: IISWebAppDeploymentOnMachineGroup@0
  inputs:
    webSiteName: 
    #virtualApplication: # Optional
    #package: '$(System.DefaultWorkingDirectory)\**\*.zip' 
    #setParametersFile: # Optional
    #removeAdditionalFilesFlag: false # Optional
    #excludeFilesFromAppDataFlag: false # Optional
    #takeAppOfflineFlag: false # Optional
    #additionalArguments: # Optional
    #xmlTransformation: # Optional
    #xmlVariableSubstitution: # Optional
    #jSONFiles: # Optional

But we can deploy with just powershell

- deployment: VMDeploy
  dependsOn: Build
  displayName: web
  environment:
    name: AzureWinVMSet
    resourceType: VirtualMachine
    tags: web
  strategy:
      rolling:
        maxParallel: 5  #for percentages, mention as x%
        preDeploy:
          steps:
          - download: current
            artifact: drop
          - script: dir
        deploy:
          steps:
          - task: PowerShell@2
            inputs:
              targetType: 'inline'
              script: |
                # Write your PowerShell commands here.
                Set-PSDebug -Trace 1
 
                Write-Host "Hello World 1b"
                Copy-Item -Path "$(Agent.BuildDirectory)/drop/*" -Destination "C:/inetpub/wwwroot" -Recurse
              errorActionPreference: 'silentlyContinue'
 
        routeTraffic:
          steps:
          - script: echo routing traffic
        postRouteTraffic:
          steps:
          - script: echo health check post-route traffic
        on:
          failure:
            steps:
            - script: echo Restore from backup! This is on failure
          success:
            steps:
            - script: echo Notify! This is on success

We can see the files are copied:

to view the MD file, you’ll need to set it as text

So if you haven't set the mimetype, you’ll see an error;

However, once set, we can see the MD file via IIS

What if we want to modify IIS on the fly?

We could set it with powershell:

set-webconfigurationproperty //staticContent -name collection -value @{fileExtension='.md'; mimeType='text/plain'} 

However, this can override all the mimetypes in IIS if we do it that way.  The better approach is to use:

Add-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter "system.webServer/staticContent" -Name "." -Value @{ fileExtension='.md'; mimeType='text/plain' }

Which will add it if it doesn't exist.

So we can do this one:

- deployment: VMDeploy
  dependsOn: Build
  displayName: web
  environment:
    name: AzureWinVMSet
    resourceType: VirtualMachine
    tags: web
  strategy:
      rolling:
        maxParallel: 5  #for percentages, mention as x%
        preDeploy:
          steps:
          - download: current
            artifact: drop
          - script: dir
        deploy:
          steps:
          - task: PowerShell@2
            inputs:
              targetType: 'inline'
              script: |
                # Write your PowerShell commands here.
                Set-PSDebug -Trace 1
 
                Add-WebConfigurationProperty -PSPath 'MACHINE/WEBROOT/APPHOST' -Filter "system.webServer/staticContent" -Name "." -Value @{ fileExtension='.md'; mimeType='text/plain' }
              errorActionPreference: 'silentlyContinue'
          - task: PowerShell@2
            inputs:
              targetType: 'inline'
              script: |
                # Write your PowerShell commands here.
                Set-PSDebug -Trace 1
 
                Write-Host "Hello World 1b"
 
                Copy-Item -Path "$(Agent.BuildDirectory)/drop/*" -Destination "C:/inetpub/wwwroot" -Recurse
              errorActionPreference: 'silentlyContinue'

Cleanup

Summary

We can use VMs with Tags to join to an Environment.  We could use a longer lived PAT to join with powershell.  However, the scope here is for those supporting legacy environments with Windows Servers.  Since the agent reaches out, this could work onPrem or in other clouds.