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.