Azure DevOps: Deploying to IIS with Windows VM Environments

Published: Jan 15, 2021 by Isaac Johnson

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

/content/images/2021/01/image-46.png

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

/content/images/2021/01/image-48.png

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>

/content/images/2021/01/image-49.png

AzDO Creating Environment

Then lets create an environment

/content/images/2021/01/image-50.png

Next we can pick windows and get the registration powershell

/content/images/2021/01/image-51.png

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_

/content/images/2021/01/image-52.png

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

/content/images/2021/01/image-53.png

In my second session, I’ll add some tags

/content/images/2021/01/image-54.png

We can now see them listed in our “Environment”

/content/images/2021/01/image-55.png

We can see the tags

/content/images/2021/01/image-56.png

Pipeline Setup

Let’s now create a GIT repo

/content/images/2021/01/image-57.png

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.

/content/images/2021/01/image-60.png

/content/images/2021/01/image-59.png

We can see that powershell ran on vm2:

/content/images/2021/01/image-62.png

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

/content/images/2021/01/image-63.png

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:

/content/images/2021/01/image-64.png

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

/content/images/2021/01/image-65.png

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

/content/images/2021/01/image-66.png

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

/content/images/2021/01/image-67.png

What if we want to modify IIS on the fly?

/content/images/2021/01/image-68.png

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.

/content/images/2021/01/image-69.png

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'

/content/images/2021/01/image-70.png

Cleanup

/content/images/2021/01/image-71.png

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.

iis azure-devops

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes