This toolkit originated from the PSAIBDeploymentToolkit. I am also developing that, however I needed to develop a way to manage applications in an "offline" manor. This Toolkit does not use AIB instead it uses scripts that build images using the remote Powershell command invoked in a Azure VM. This can support both Azure IL5 and IL6.
The structure is similar to MDT's and each defined "sequenced" process is within the Control folder and each "sequence" contains a sequence.json file. This file is not a schema that follows the Azure Image builder schema, however with this file in conjunction with a basic template file (within the Template folder), the Applications\applications.json will build and capture a reference image for AVD consumption.
NOTE: I am working to merge this toolkit with my AIB toolkit allowing it to support both methods.
- Azure Subscriptions
- Virtual network for reference image
- The rest can be built using 1_prep_azureenv.ps1 script
To support multiple environment and applications offline, these applications must be downloaded and staged in blob prior to running the image process. This process is not 100% automated at the moment and does require PowerShell scripts to run each month.
AVDDeploymentToolkit
|-Applications
|--fslogix
|--lgpo
|--office365
|--onedrive
|--teams
|--etc...
|-Control
|--Win10AvdImage
|---sequence.json
|--Win11AvdImage
|---sequence.json
|-Scripts
|--supporting scripts
|--VM
|--sequence scripts
|-Templates
|--json files
|--template scripts*
|-Tools
|--7za.exe
|--7za.dll
|-Logs
|--transaction logs for each script ran
| Filename | Explanation | Access Requirements | Run Example | Recommended Cadence | Notes |
|---|---|---|---|---|---|
| 1_prep_azureenv.ps1 | Sets up azure environment to support this toolkit and AIB | must have tenant access and Global Admin | PS .\1_prep_azureenv.ps1 -ControlSettings setting.gov.json |
Monthly for sastoken renewal. | Sastoken can be generated manually if preferred (paste token in settings.json) |
| 2_download_applications.ps1 | Downloads applications and zips them up | must have internet access | PS .\2_download_applications.ps1 -ControlSettings setting.gov.json -CompressForUpload |
Monthly | Can be ran on a internet device and files transferred to a tenant connect device from a media |
| 3_upload_to_azureblob.ps1 | Uploads archived applications to blob using sastoken | must have network access to blob storage | PS .\3_upload_to_azureblob.ps1 -ControlSettings setting.gov.json |
Monthly | |
| 4A_create_avd_ref_vm.ps1 | Create Azure VM and runs prep script to install applications | must have tenant access and compute contributor role | PS .\4A_create_avd_ref_vm.ps1 -ControlSettings setting.gov.json -Sequence Win11AvdGFEImage |
Monthly | |
| 5A_capture_vm_image_invokeposh.ps1 | Sets up azure environment to support this toolkit and AIB | must have tenant access and compute contributor role | PS .\5A_capture_vm_image_invokeposh.ps1 -ControlSettings setting.gov.test.json -ForceNewSasToken -Sequence Win11AvdGFEImage -VMName TEST-2306-REF -CleanUpVMOnCaptureSuccess |
Monthly |
TIP: Each of these script has a dependency on at least one json file included in the toolkit.
- Download repo
- Edit the applications.json (or leave it be). See application.json breakdown below
- Copy TemplateImage folders in Control folder and name it to your image needs (or edit the existing ones)
- Edit the sequence.json for the applications,scripts you want to install
- See sequence.json breakdown below
- Edit all entries with arrows '<>' and choose an option with the pipe '|'
- Copy the settings.example.json and make new file.
- Edit all entries with arrows '<>' and choose an option with the pipe '|'
- Run each script in order using the params (like in the examples)
NOTE: Images may not reflect script names
This is file contains a list applications and the method for downloading them and installing them
Supported parameters are:
- enabled – boolean. enables or disables this step entirely
- download – boolean. enables or disables the download step
- appId – guid. Use New-Guid to get a guid,
- productName – string. Name of product (use what shows up in appwiz.cpl)
- version – string. Version of product (set to "latest") for latest download
- localPath – string. Path of where application will downloaded to
- fileName – string. The name of the file to be downloaded or executed
- downloadURI – url. the official url where files can be downloaded from
- downloadUriType – string. can be either webrequest, shortlink, shortlinkextract, linkId, or linkIdExtract. Used to determine the method of download
- preDownloadScript – string or array of strings. This is sequential. Each line will run in powershell before download starts. Typically used to get versions or release url
- postDownloadScript – string or array of strings. This is sequential. Each line will run in powershell after download is complete. Typically used to cleanup additional files or extract archive
- installArguments – string. the arguments used to install the application silently
- preInstallScript – string or array of strings. This is sequential. Each line will run in powershell before install starts. Typically used to setup dependencies.
- postInstallScript – string or array of strings. This is sequential. Each line will run in powershell after application is installed. Typically used to configure post settings for applications
[
{
"download": "false",
"appId": "4f86a38b-0a06-4d08-94a0-aaeecb9c359f",
"productName" : "Git For Windows",
"version" : "[version]",
"localPath" : "[ApplicationsPath]\\Git",
"fileName": "Git-installer-x64.exe",
"preDownloadScript": [
"$releaseURI = Invoke-WebRequest \"https://github.com/git-for-windows/git/releases/latest\" -Headers @{\"Accept\" = \"application/json\" } -UseBasicParsing",
"$json = $releaseURI.Content | ConvertFrom-Json",
"$release = $json.tag_name",
"$versionURI = Invoke-WebRequest \"https://github.com/git-for-windows/git/releases/tag/[release]\" -UseBasicParsing",
"[xml]$xml = $versionURI | Select-String '(?s)(<table>.+?</table>)' | ForEach-Object { $_.Matches[0].Groups[1].Value }",
"$hashtable = $xml.table.tbody.tr | ForEach-Object { [PSCustomObject]@{File = $_.td[0];Hash = $_.td[1] }}",
"$version = $hashtable | Where file -like \"*64-bit.exe\" | Select -ExpandProperty file"
],
"downloadURI" : "https://github.com/git-for-windows/git/releases/download/[release]/[version]",
"downloadUriType" : "webrequest",
"installArguments": "/VERYSILENT /NORESTART /COMPONENTS=\"ext,ext\\shellhere,ext\\guihere,gitlfs,assoc,assoc_sh\" /LOG"
},
] [
{
"download": "true",
"appId": "73d9d3c6-0041-48dc-9866-55b6c1f2af33",
"productName" : "Microsoft 365 Apps for enterprise - en-us",
"version" : "latest",
"localPath" : "[ApplicationsPath]\\M365",
"fileName": "setup.exe",
"downloadURI" : "https://www.microsoft.com/en-us/download/details.aspx?id=49117",
"downloadUriType" : "linkIdExtract",
"postDownloadScript": [
"Remove-Item [localPath] -Recurse -Include *.xml -Force -ErrorAction SilentlyContinue | Out-Null",
"Push-Location [localPath]",
"$xml = @()",
"$xml += '<Configuration>'",
"$xml += '<Add OfficeClientEdition=\"64\" Channel=\"MonthlyEnterprise\">'",
"$xml += '<Product ID=\"O365ProPlusRetail\">'",
"$xml += '<Language ID=\"en-US\" />'",
"$xml += '<Language ID=\"MatchOS\" />'",
"$xml += '<ExcludeApp ID=\"Groove\" />'",
"$xml += '<ExcludeApp ID=\"Lync\" />'",
"$xml += '<ExcludeApp ID=\"OneDrive\" />'",
"$xml += '<ExcludeApp ID=\"Teams\" />'",
"$xml += '</Product>'",
"$xml += '</Add>'",
"$xml += '<Updates Enabled=\"FALSE\"/>'",
"$xml += '<Display Level=\"None\" AcceptEULA=\"TRUE\" />'",
"$xml += '<Property Name=\"FORCEAPPSHUTDOWN\" Value=\"TRUE\"/>'",
"$xml += '<Property Name=\"SharedComputerLicensing\" Value=\"1\"/>'",
"$xml += '</Configuration>'",
"$xml | Out-file -FilePath \"[localPath]\\configuration.xml\"",
"[outputPath] /download \"[localPath]\\configuration.xml\"",
"$version = (Get-ChildItem -Path \"[localPath]\" -Recurse -Directory | Where BaseName -match \"\\d+(\\.\\d+){1,3}\").BaseName",
"Pop-Location"
],
"installArguments": "/configure \"[localPath]\\configuration.xml\""
},
]This file should be located under the Control folder.
- Settings – Specify paths and modules needed for toolkit to work
- TenantEnvironment – Used for tenant connection with Azure modules
- AzureResources – Resources need to manage the image build process. Some key ones to focus on
- storageAccount – used during the application upload and download steps. Specify the storage account used
- storageContainer – used during the application upload and download steps. Specify the container used
- containerSasToken – used during the application upload and download steps. Can be autogenerated using script A1_prep_azureenv.ps1. Can use stored in keyvault
- keyVault – Specify the keyvault to use or create
Note Some value can use [Keyvault]; this will securely store the values in keyvault during the process and use it throughout the process
- storageAccount – used during the application upload and download steps. Specify the storage account used
- AvdResources – NOT USED YET
- ManagedIdentity – specified to appropiate assign roles to AIB
- LogAnalytics – Not used at the moment. Intended for sending build status to log analytics for viewing
This file should exist in each type of sequence folder under Control. It determines what actions are done on the VM.
- customSettings – section is where the global settings will be.
- customSequence – section is used to specify each step the script will run through. Once the customSequence is complete the cleanup action and final action (from customSettings section) are ran
- Template – section is used for AIB process
- imageDefinition – section is used to build the reference image and provide the name of the image image in the gallery
There are three types of steps that can be ran during the customSequence: Applications, Scripts, and Windows Updates:
Supported parameters are:
- enabled – boolean. enables or disables the step in the csutomizations
- type – string. Set to "Application"
- name – string. Name of step
- id – guid. Must match that of the application.json corresponding list,
- workingDirectory – string. Path of where application will installed from
- validExitCodes – array of integers. typically set to [0,3010]
- continueOnError – boolean. enables allows script to run even if do does not match the validExitCode
- validateInstalled – boolean. enables validates the application is installed using the application name
- rebootOnSuccess – boolean. Reboots the system after install. this will break the script from continuing. DON'T USE YET
"customSequence": [
{
"enabled": "true",
"type": "Application",
"name" : "Install FSLogix",
"id": "5c97799b-78a8-466f-82e3-99bb04797fb1",
"workingDirectory": "[localPath]\\FSlogix",
"validExitCodes": [0,3010],
"continueOnError": "true",
"validateInstalled": "true",
"rebootOnSuccess": "false"
}
]Supported parameters are:
- enabled – boolean. enables or disables the step in the customizations
- type – string. Set to "Script"
- name – string. Name of step
- id – guid. can be anything. Not used
- inlineScript – string or array of strings. This is sequential. Each line will run in powershell
- validExitCodes – array of integers. typically set to [0,3010]
- continueOnError – boolean. enables allows script to run even if do does not match the validExitCode
- rebootOnSuccess – boolean. Reboots the system after script is ran. this will break the script from continuing. DON'T USE YET
"customize": [
{
"enabled": "true",
"type": "Script",
"name" : "Setup CMtrace",
"id": "693c894c-58c4-4572-b5f0-fc86e40186f3",
"inlineScript": [
"Copy-Item -Path \"`[ToolsPath]\\CMTrace.exe\" -Destination \"$env:Windir\\System32\" -Force -ErrorAction Stop",
"New-Item -Path 'HKLM:\\Software\\Classes\\.lo_' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Classes\\.log' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Classes\\.log.File' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Classes\\.Log.File\\shell' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Classes\\Log.File\\shell\\Open' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Classes\\Log.File\\shell\\Open\\Command' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\Software\\Microsoft\\Trace32' -type Directory -Force -ErrorAction SilentlyContinue | Out-Null",
"New-ItemProperty -LiteralPath 'HKLM:\\Software\\Classes\\.lo_' -Name '(default)' -Value 'Log.File' -PropertyType String -Force -ea SilentlyContinue | Out-Null",
"New-ItemProperty -LiteralPath 'HKLM:\\Software\\Classes\\.log' -Name '(default)' -Value 'Log.File' -PropertyType String -Force -ea SilentlyContinue | Out-Null",
"New-ItemProperty -LiteralPath 'HKLM:\\Software\\Classes\\Log.File\\shell\\open\\command' -Name '(default)' -Value '$env:Windir\\System32\\CMTrace.exe \"\"%1\"\"' -PropertyType String -Force -ea SilentlyContinue | Out-Null",
"New-Item -Path 'HKLM:\\SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\CMtrace' -type Directory -Force | Out-Null",
"New-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\CMtrace' -Name 'Version' -Value 1 -PropertyType String -Force | Out-Null",
"New-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Active Setup\\Installed Components\\CMtrace' -Name 'StubPath' -Value \"reg.exe add HKCU\\Software\\Microsoft\\Trace32 /v 'Register File Types' /d 0 /f\" -PropertyType ExpandString -Force | Out-Null"
],
"continueOnError": "true",
"rebootOnSuccess": "false"
}
],Supported parameters are:
- enabled – boolean. enables or disables the step in the customizations
- type – string. Set to "WindowsUpdate"
- name – string. Name of step
- id – guid. can be anything. Not used
- preUpdateScript – string or array of strings. This is sequential. Each line will run in powershell before updates start
- postUpdateScript – string or array of strings. This is sequential. Each line will run in powershell after updates are installed
- restartTimeout – integer. typically set to 0
- continueOnError – boolean. enables allows script to run even if do does not match the validExitCode
- rebootOnSuccess – boolean. Reboots the system after script is ran. this will break the script from continuing. DON'T USE YET
"customize": [
{
"enabled": "true",
"type": "WindowsUpdate",
"name" : "Install Windows Update",
"id": "03fc164d-a1cd-4ba3-aa60-249f39a5fff7",
"restartTimeout": "0",
"continueOnError": "true",
"rebootOnSuccess": "false"
}
],As each json object is processed, the scripts are looking for bracketed values to convert to variables. This allows to the script to be more dynamic.
If the script already has a variable $localpath = "c:\windows\temp\apps" the script will look for any property using [localPath] and replace it with "c:\windows\temp\apps".
Since the json has key:value properties in it such as: "filename":"setup.exe"; during the process, if the script sees a bracketed value of [filename] it will be replaced with "setup.exe"
- Storage account has public access but to certain virtual networks
- Container must be anonymous access with SASTokens
- Build process to use Azure Key vault with rotating storage keys
- Use Azure Automation with Managed Identities
- Develop a MDT-like User Interface to allow easier configurations or use MDT then convert for AIB to consume
- Build language pack support using the Packages folder (https://docs.microsoft.com/en-us/azure/virtual-desktop/language-packs)
- Develop a method to document definition version (eg after each build using custom table in log analytics to store output)
- Azure Image Version cleanup
- Azure Virtual Machine host cycle
If you are contributing, testing or using the code. Please create a copy of the Settings.json file in control folder and name it something like Settings-<user>.json. (keep the Settings- in the filename); this file will be ignored during pull request.
You don't want your secrets to be public.
There is a Logs folder that will contain a dated transcript of the AIB sequence called and the json arm template is generated there for reference.
- Please submit issues for me to track
- https://github.com/danielsollondon/azvmimagebuilder/tree/master/quickquickstarts/0_Creating_a_Custom_Windows_Managed_Image
- https://docs.microsoft.com/en-us/azure/virtual-machines/linux/image-builder-json?tabs=azure-powershell
- https://docs.microsoft.com/en-us/azure/virtual-desktop/set-up-customize-master-image
- https://docs.microsoft.com/en-us/azure/virtual-desktop/set-up-golden-image
- https://docs.microsoft.com/en-us/azure/virtual-machines/windows/image-builder-powershell
Even though I have tested this to the extend that I could, I want to ensure your aware of Microsoft’s position on developing custom scripts.
This Sample Code is provided for the purpose of illustration only and is not intended to be used in a production environment. THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to reproduce and distribute the object code form of the Sample Code, provided that You agree: (i) to not use Our name, logo, or trademarks to market Your software product in which the Sample Code is embedded; (ii) to include a valid copyright notice on Your software product in which the Sample Code is embedded; and (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against any claims or lawsuits, including attorneys’ fees, that arise or result from the use or distribution of the Sample Code.
This posting is provided "AS IS" with no warranties, and confers no rights. Use of included script samples are subject to the terms specified at https://www.microsoft.com/en-us/legal/copyright.




