├── LICENSE ├── 3a. sysprep.ps1 ├── 3. Invoke Sysprep.ps1 ├── 2. Invoke Updates.ps1 ├── SECURITY.md ├── 2a. WindowsUpdate.ps1 ├── 1. Build New Template to Update.ps1 ├── DeleteVM.ps1 ├── 4. Snapshot and Move to SAs.ps1 ├── .gitignore ├── CreateVM.ps1 ├── 5. Create all Images and Cleanup.ps1 └── readme.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Microsoft Corporation. All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE 22 | -------------------------------------------------------------------------------- /3a. sysprep.ps1: -------------------------------------------------------------------------------- 1 | ####################################################################### 2 | #You might get issues with the provisioned state of some windows store apps 3 | #The following show you how to remove the apps to enable sysprep to complete 4 | #https://www.askvg.com/guide-how-to-remove-all-built-in-apps-in-windows-10/ 5 | #https://support.microsoft.com/en-us/help/2769827/sysprep-fails-after-you-remove-or-update-windows-store-apps-that-inclu 6 | #https://blogs.technet.microsoft.com/mniehaus/2018/04/17/cleaning-up-apps-to-keep-windows-10-sysprep-happy/ 7 | 8 | #this will remove all windows store apps, 9 | #Get-AppxPackage | Remove-AppxPackage 10 | #######################################################################> 11 | 12 | ###################### Create or Set Log file ######################### 13 | $datetime = Get-Date -UFormat "%Y%m%d%H%M%S" 14 | $filename = "Sysprep$datetime.txt" 15 | $Location = "C:\UpdateLogs" 16 | $LocationFile = "C:\UpdateLogs\$filename" 17 | 18 | If((Test-Path $Location) -eq $False) 19 | { 20 | New-Item -Path "C:\" -name "UpdateLogs" -ItemType "directory" 21 | } # End of folder exists test 22 | If((Test-Path $LocationFile) -eq $False) 23 | { 24 | New-Item -Path $Location -Name $filename -ItemType File 25 | } # End of file exist test 26 | Else 27 | { 28 | "The $LocationFile is already there." 29 | } 30 | ####################################################################### 31 | 32 | ########################## Start Sysprep ############################## 33 | Write-Output "+ Starting Sysprep" | add-content $LocationFile 34 | 35 | try 36 | { 37 | $sysprep = 'C:\Windows\System32\Sysprep\Sysprep.exe' 38 | $arg = '/generalize /oobe /shutdown /quiet' 39 | $sysprep += " $arg" 40 | Invoke-Expression $sysprep 41 | } 42 | catch 43 | { 44 | Write-Output "Error Running Sysprep" | add-content $LocationFile 45 | Write-Output $_.Exception.Message | add-content $LocationFile 46 | } 47 | 48 | Write-Output "+ Sysprep Executing" | add-content $LocationFile 49 | ####################################################################### -------------------------------------------------------------------------------- /3. Invoke Sysprep.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subscription ID for the subscription containing the VM deployed to update 3 | $rgName = #Resource Group containing the VM deployed for upgrading 4 | $location = #Region in which the VM to update has been deployed 5 | $vmName = #Name of the VM to be updates 6 | $storageaccname = #Storage account within the Sub and Resource group, used to access PS1 files 7 | $containerName = #Storage Container containing the PS1 Scripts 8 | $FileName = #Name of the Script to call "3a. sysprep.ps1" 9 | $extentionName = #Name for the Custom Script extension, has to be the same in the variables for "2. Invoke Updates" 10 | ############################################################################################# 11 | 12 | ################################ Login As Automation Account ################################ 13 | try 14 | { 15 | $Conn = Get-AutomationConnection -Name AzureRunAsConnection 16 | Login-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID ` 17 | -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 18 | Write-Output "Successfully connected as Automation Account" 19 | } 20 | catch 21 | { 22 | Write-Output "Error connecting as Automation Account" 23 | Write-Output $_.Exception.Message 24 | } 25 | 26 | $null = Set-AzureRMContext -SubscriptionId $subid 27 | ############################################################################################# 28 | 29 | ################################ Login As Automation Account ################################ 30 | 31 | $storagekey = (Get-AzureRmStorageAccountKey -ResourceGroupName $rgName -Name $storageaccname| where-object{$Psitem.keyname -eq 'key1'}).value 32 | 33 | try 34 | { 35 | Set-AzureRmVMCustomScriptExtension ` 36 | -ResourceGroupName $rgName ` 37 | -Location $location ` 38 | -VMName $vmName ` 39 | -Name $extentionName ` 40 | -StorageAccountName $storageaccname ` 41 | -StorageAccountKey $storagekey ` 42 | -FileName $FileName ` 43 | -ContainerName $containerName ` 44 | -Run $FileName 45 | } 46 | catch 47 | { 48 | Write-Output "Failed to set custom script extension" 49 | Write-Output $_.Exception.Message 50 | } 51 | ############################################################################################# -------------------------------------------------------------------------------- /2. Invoke Updates.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subscription ID for the subscription containing the VM deployed to update 3 | $rgName = #Resource Group containing the VM deployed for upgrading 4 | $location = #Region in which the VM to update has been deployed 5 | $vmName = #Name of the VM to be updates 6 | $storageaccname = #Storage account within the Sub and Resource group, used to access PS1 files 7 | $containerName = #Storage Container containing the PS1 Scripts 8 | $FileName = #Name of the Script to call "2a. WindowsUpdate.ps1" 9 | $extentionName = #Name for the Custom Script extension, has to be the same in the variables for "3. Invoke Sysprep" 10 | ############################################################################################# 11 | 12 | ################################ Login As Automation Account ################################ 13 | try 14 | { 15 | $Conn = Get-AutomationConnection -Name AzureRunAsConnection 16 | Login-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID ` 17 | -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 18 | Write-Output "Successfully connected as Automation Account" 19 | } 20 | catch 21 | { 22 | Write-Output "Error connecting as Automation Account" 23 | Write-Output $_.Exception.Message 24 | } 25 | 26 | $null = Set-AzureRMContext -SubscriptionId $subid 27 | ############################################################################################# 28 | 29 | ################################ Set Custom Script Extension ################################ 30 | 31 | $storagekey = (Get-AzureRmStorageAccountKey -ResourceGroupName $rgName -Name $storageaccname| where-object{$Psitem.keyname -eq 'key1'}).value 32 | 33 | try 34 | { 35 | Set-AzureRmVMCustomScriptExtension ` 36 | -ResourceGroupName $rgName ` 37 | -Location $location ` 38 | -VMName $vmName ` 39 | -Name $extentionName ` 40 | -StorageAccountName $storageaccname ` 41 | -StorageAccountKey $storagekey ` 42 | -FileName $FileName ` 43 | -ContainerName $containerName ` 44 | -Run $FileName 45 | } 46 | catch 47 | { 48 | Write-Output "Failed to set custom script extension" 49 | Write-Output $_.Exception.Message 50 | } 51 | 52 | 53 | ############################################################################################# -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Security 4 | 5 | Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet), [Xamarin](https://github.com/xamarin), and [our GitHub organizations](https://opensource.microsoft.com/). 6 | 7 | If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/opensource/security/definition), please report it to us as described below. 8 | 9 | ## Reporting Security Issues 10 | 11 | **Please do not report security vulnerabilities through public GitHub issues.** 12 | 13 | Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/opensource/security/create-report). 14 | 15 | If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/opensource/security/pgpkey). 16 | 17 | You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://aka.ms/opensource/security/msrc). 18 | 19 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 20 | 21 | * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) 22 | * Full paths of source file(s) related to the manifestation of the issue 23 | * The location of the affected source code (tag/branch/commit or direct URL) 24 | * Any special configuration required to reproduce the issue 25 | * Step-by-step instructions to reproduce the issue 26 | * Proof-of-concept or exploit code (if possible) 27 | * Impact of the issue, including how an attacker might exploit the issue 28 | 29 | This information will help us triage your report more quickly. 30 | 31 | If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/opensource/security/bounty) page for more details about our active programs. 32 | 33 | ## Preferred Languages 34 | 35 | We prefer all communications to be in English. 36 | 37 | ## Policy 38 | 39 | Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/opensource/security/cvd). 40 | 41 | 42 | -------------------------------------------------------------------------------- /2a. WindowsUpdate.ps1: -------------------------------------------------------------------------------- 1 | function Get-WIAStatusValue($value) 2 | { 3 | switch -exact ($value) 4 | { 5 | 0 {"NotStarted"} 6 | 1 {"InProgress"} 7 | 2 {"Succeeded"} 8 | 3 {"SucceededWithErrors"} 9 | 4 {"Failed"} 10 | 5 {"Aborted"} 11 | } 12 | } 13 | 14 | $needsReboot = $false 15 | $UpdateSession = New-Object -ComObject Microsoft.Update.Session 16 | $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() 17 | 18 | ###################### Create or Set Log file ######################### 19 | $datetime = Get-Date -UFormat "%Y%m%d%H%M%S" 20 | $filename = "WinUpdate$datetime.txt" 21 | $Location = "C:\UpdateLogs" 22 | $LocationFile = "C:\UpdateLogs\$filename" 23 | 24 | If((Test-Path $Location) -eq $False) 25 | { 26 | New-Item -Path "C:\" -name "UpdateLogs" -ItemType "directory" 27 | } # End of folder exists test 28 | If((Test-Path $LocationFile) -eq $False) 29 | { 30 | New-Item -Path $Location -Name $filename -ItemType File 31 | } # End of file exist test 32 | Else 33 | { 34 | "The $LocationFile is already there." 35 | } 36 | ####################################################################### 37 | 38 | ######################### Perform Update ############################## 39 | Write-Output " - Searching for Updates" | add-content $LocationFile 40 | $SearchResult = $UpdateSearcher.Search("IsAssigned=1 and IsHidden=0 and IsInstalled=0") 41 | 42 | Write-Output " - Found [$($SearchResult.Updates.count)] Updates to Download and install" | add-content $LocationFile 43 | 44 | $needsReboot = @() 45 | 46 | foreach($Update in $SearchResult.Updates) 47 | { 48 | # Add Update to Collection 49 | $UpdatesCollection = New-Object -ComObject Microsoft.Update.UpdateColl 50 | 51 | if ( $Update.EulaAccepted -eq 0 ) 52 | { 53 | $Update.AcceptEula() 54 | } 55 | 56 | $UpdatesCollection.Add($Update) | out-null 57 | 58 | #Download 59 | Write-Output " + Downloading Update $($Update.Title)" | add-content $LocationFile 60 | $UpdatesDownloader = $UpdateSession.CreateUpdateDownloader() 61 | $UpdatesDownloader.Updates = $UpdatesCollection 62 | $DownloadResult = $UpdatesDownloader.Download() 63 | $Message = " - Download {0}" -f (Get-WIAStatusValue $DownloadResult.ResultCode) 64 | Write-Output $message | add-content $LocationFile 65 | 66 | #Install 67 | Write-Output " - Installing Update" | add-content $LocationFile 68 | $UpdatesInstaller = $UpdateSession.CreateUpdateInstaller() 69 | $UpdatesInstaller.Updates = $UpdatesCollection 70 | $InstallResult = $UpdatesInstaller.Install() 71 | $Message = " - Install {0}" -f (Get-WIAStatusValue $DownloadResult.ResultCode) 72 | Write-Output $message | add-content $LocationFile 73 | 74 | if ($installResult.rebootRequired){$needsReboot += "1"} 75 | 76 | } 77 | 78 | #Restart if needed 79 | if($needsReboot.Contains("1")) 80 | { 81 | Write-Output "Restart Required" | add-content $LocationFile 82 | restart-computer -Force 83 | } 84 | ####################################################################### -------------------------------------------------------------------------------- /1. Build New Template to Update.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subsctiption in which you want to deploy the VM 3 | $rgName = #Resource Group in which yo want to deploy the VM 4 | $subnets = #CIDR ranges you want to be able to access the VM, e.g. "10.1.0.0/16", "192.168.2.0/24" 5 | $location = #Region you want the VM deployed in 6 | $vmName = #Name of the virtual machine you are want to create 7 | $subnetName = #Name of the Subnet to be created with the VM 8 | $vnetName = #Name of the Virtual Network to be created with the VM 9 | $ipName = #Name of the Public IP address to be created with the VM 10 | $nsgName = #Name of the Network Security Group to be created with the VM 11 | $ImageName = #Name of the Image which the VM is to be deployed from 12 | $vmSize = #SKU size for the VM you want to deploy https://docs.microsoft.com/en-us/azure/cloud-services/cloud-services-sizes-specs 13 | $Username = #Username for the account to be created on the VM 14 | $KeyVault = #Keyvault name to store the password for the user account to be created on the VM 15 | $PasswordName = #Name/ID of the password for the user account, which will be stored in keyvault 16 | ############################################################################################# 17 | 18 | ################################ Login As Automation Account ################################ 19 | try 20 | { 21 | $Conn = Get-AutomationConnection -Name AzureRunAsConnection 22 | Login-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID ` 23 | -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 24 | Write-Output "Successfully connected as Automation Account" 25 | } 26 | catch 27 | { 28 | Write-Output "Error connecting as Automation Account" 29 | Write-Output $_.Exception.Message 30 | } 31 | 32 | 33 | $null = Set-AzureRMContext -SubscriptionId $subid 34 | ############################################################################################# 35 | 36 | ################################ Create User Account ######################################## 37 | try 38 | { 39 | Add-Type -AssemblyName System.Web 40 | $securePassword = convertto-securestring ([System.Web.Security.Membership]::GeneratePassword(32,8)) -asplaintext -force 41 | Set-AzureKeyVaultSecret -VaultName $KeyVault -Name $PasswordName -SecretValue $securePassword 42 | $password = Get-AzureKeyVaultSecret -VaultName $KeyVault -Name $PasswordName 43 | $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($Username, $password.SecretValue) 44 | Write-Output "Successfully created user and password pair. Password saved in KeyVault" 45 | } 46 | catch 47 | { 48 | Write-Output "Failed creating username and password pair." 49 | Write-Output $_.Exception.Message 50 | } 51 | ############################################################################################# 52 | 53 | ######################################## Create NSG ######################################### 54 | try 55 | { 56 | #Create the NSG 57 | $rdpRule = New-AzureRmNetworkSecurityRuleConfig -Name myRdpRule -Description "Allow RDP" ` 58 | -Access Allow -Protocol Tcp -Direction Inbound -Priority 110 ` 59 | -SourceAddressPrefix Internet -SourcePortRange * ` 60 | -DestinationAddressPrefix * -DestinationPortRange 3389 61 | 62 | $nsg = New-AzureRmNetworkSecurityGroup -ResourceGroupName $rgName -Location $location ` 63 | -Name $nsgName -SecurityRules $rdpRule -Force 64 | Write-Output "Created NSG and added custom RDP rule" 65 | } 66 | catch 67 | { 68 | Write-Output "Failed to Create NSG with custom RDP rule" 69 | Write-Output $_.Exception.Message 70 | } 71 | 72 | try 73 | { 74 | # Add our IPs to the Nsg 75 | # Remove the open rdp rule 76 | $nsg | Remove-AzureRmNetworkSecurityRuleConfig -Name "myRdpRule" > $null 77 | $index=100 78 | foreach ($cidr in $subnets) { 79 | $name = "rdp-rule-$index"; 80 | $nsg | Add-AzureRmNetworkSecurityRuleConfig -Name $name -Description "Allow RDP Access for Defined IPs" -Access Allow -Protocol * -Direction Inbound -Priority $index -SourceAddressPrefix $cidr -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 3389 > $null 81 | $index++ 82 | } 83 | $nsg | Set-AzureRmNetworkSecurityGroup > $null 84 | Write-Output "IPs added to RDP NSG" 85 | } 86 | catch 87 | { 88 | Write-Output "IPs failed to be added to RDP NSG" 89 | Write-Output $_.Exception.Message 90 | } 91 | ############################################################################################ 92 | 93 | ####################################### Create the VM ###################################### 94 | New-AzureRmVm ` 95 | -ResourceGroupName $rgName ` 96 | -Name $vmName ` 97 | -ImageName $ImageName ` 98 | -Location $location ` 99 | -VirtualNetworkName $vnetName ` 100 | -SubnetName $subnetName ` 101 | -SecurityGroupName $nsgName ` 102 | -PublicIpAddressName $ipName ` 103 | -Credential $cred ` 104 | -Size $vmSize 105 | ############################################################################################ -------------------------------------------------------------------------------- /DeleteVM.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subsctiption ID in which your VM is deployed 3 | $subName = #Subsctiption Name iin which your VM is deployed 4 | $rgName = #Resource Group in which your VM is deployed 5 | $keyVaultName = #Name of the Keyvault containing the secret for this Vm to be deleted 6 | ############################################################################################# 7 | 8 | ############################################################################################# 9 | Write-Host " _______" 10 | Write-Host " |.-----.|" 11 | Write-Host " ||x . x||" 12 | Write-Host " ||_.-._||" 13 | Write-Host " ---)-(--- " 14 | Write-Host " __[=== o]___" 15 | Write-Host " |:::::::::::|\" 16 | Write-Host " --=========--()" 17 | Write-Host "`n" 18 | Write-Host "`n" 19 | Write-Host "This script will delete the VM specified" 20 | Write-Host "`n" 21 | Write-Host "### Checking Windows Azure PowerShell version: " -NoNewLine 22 | Try { 23 | Import-Module AzureRM -MinimumVersion 3.0.0 -ErrorAction Stop 24 | } Catch { 25 | Write-Host -ForegroundColor Red "Failed!" 26 | Write-Host "Please ensure you have the latest version of Azure PowerShell (please see https://azure.microsoft.com/en-us/documentation/articles/powershell-install-configure/)." 27 | exit 28 | } 29 | Write-Host -ForegroundColor Green "Good!" 30 | Write-Host "### Logging into Azure (please use your user@domain)" 31 | Login-AzureRmAccount -ErrorAction Stop 32 | Set-AzureRMContext -SubscriptionId $subid 33 | ############################################################################################# 34 | 35 | ################################### Get User Input ########################################## 36 | Do { $UniqueID = Read-Host "Please specify a Unique ID for the VM you wish to delete (e.g. Case Number)" } while ($UniqueID -eq "") 37 | 38 | $vmName = "$UniqueID" 39 | $vnetName = "$UniqueID-Vnet" 40 | $nsgName = "$UniqueID-NSG" 41 | $ipName = "$UniqueID-PIP" 42 | 43 | $vm = Get-AzureRmVM -ResourceGroupName $rgName -Name $vmName 44 | $diskName = $vm.StorageProfile.OsDisk.Name 45 | 46 | Write-Host -ForegroundColor Green "`n!!!! IMPORTANT !!!!" 47 | Write-Host "Please review. The following items will be deleted permanently:" 48 | Write-Host " VM: $vmName" 49 | Write-Host " NIC: $vmName" 50 | Write-Host " Disk: $diskName" 51 | Write-Host " NSG: $nsgName" 52 | Write-Host " PIP: $ipName" 53 | Write-Host "`n" 54 | $confirm = Read-Host "Enter the Unique ID again to proceed with deletion, otherwise press to cancel" 55 | if ($confirm -ne $UniqueID) 56 | { 57 | Write-Host -ForegroundColor Red "Cancelled!" 58 | exit 59 | } 60 | ############################################################################################# 61 | 62 | ###################################### Remove the VM ####################################### 63 | Select-AzureRMSubscription -SubscriptionName $subName 64 | 65 | try 66 | { 67 | Remove-AzureRmVM -ResourceGroupName $rgName -Name $vmName -Force -ErrorAction SilentlyContinue 68 | Write-Output "Removed VM $vmName" 69 | } 70 | catch 71 | { 72 | Write-output $_.Exception.Message 73 | } 74 | try 75 | { 76 | Remove-AzureRmDisk -ResourceGroupName $rgName -Name $diskName -Force -ErrorAction SilentlyContinue 77 | Write-Output "Removed Disk $diskName" 78 | } 79 | catch 80 | { 81 | Write-output $_.Exception.Message 82 | } 83 | try 84 | { 85 | Remove-AzureRmNetworkInterface -ResourceGroupName $rgName -Name $vmName -Force -ErrorAction SilentlyContinue 86 | Write-Output "Removed NIC $vmName" 87 | } 88 | catch 89 | { 90 | Write-output $_.Exception.Message 91 | } 92 | try 93 | { 94 | Remove-AzureRmNetworkSecurityGroup -ResourceGroupName $rgName -Name $nsgName -Force -ErrorAction SilentlyContinue 95 | Write-Output "Removed NSG $nsgName" 96 | } 97 | catch 98 | { 99 | Write-output $_.Exception.Message 100 | } 101 | try 102 | { 103 | Remove-AzureRmPublicIpAddress -ResourceGroupName $rgName -Name $ipName -Force -ErrorAction SilentlyContinue 104 | Write-Output "Removed PIP $ipName" 105 | } 106 | catch 107 | { 108 | Write-output $_.Exception.Message 109 | } 110 | try 111 | { 112 | Remove-AzureRmVirtualNetwork -ResourceGroupName $rgName -Name $vnetName -Force -ErrorAction SilentlyContinue 113 | Write-Output "Removed vNet $vnetName" 114 | } 115 | catch 116 | { 117 | Write-output $_.Exception.Message 118 | } 119 | try 120 | { 121 | Remove-AzureRmSnapshot -ResourceGroupName $rgName -SnapshotName $SnapshotName -Force -ErrorAction SilentlyContinue 122 | Write-Output "Removed Snapshot $SnapshotName" 123 | } 124 | catch 125 | { 126 | Write-output $_.Exception.Message 127 | } 128 | ############################################################################################# 129 | 130 | ################################### Remove Secret ########################################### 131 | Write-Host "Removing the Secrets for $UniqueID from KeyVault $keyVaultName." 132 | try 133 | { 134 | Remove-AzureKeyVaultSecret -VaultName $keyVaultName -Name $UniqueID -Force 135 | Write-Host "Done!" 136 | } 137 | catch 138 | { 139 | Write-Output $_.Exception.Message 140 | } 141 | ############################################################################################# -------------------------------------------------------------------------------- /4. Snapshot and Move to SAs.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subscription ID for the subscription containing the VM deployed to update 3 | $rgName = #Resource Group containing the VM deployed for upgrading 4 | $vmName = #Name of the VM to be updates 5 | $skuName = #Type fo SKU desired for storage account e.g "Standard_LRS" 6 | $imageContainerName = #Storage Container Name within the Storage Account e.g "images" 7 | $SnapshotName = #Name to be givent to the temporary snapshot created e.g. "temporary-snap" 8 | ############################################################################################# 9 | 10 | ################################ Login As Automation Account ################################ 11 | try 12 | { 13 | $Conn = Get-AutomationConnection -Name AzureRunAsConnection 14 | Login-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID ` 15 | -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 16 | Write-Output "Successfully connected as Automation Account" 17 | } 18 | catch 19 | { 20 | Write-Output "Error connecting as Automation Account" 21 | Write-Output $_.Exception.Message 22 | } 23 | 24 | $null = Set-AzureRMContext -SubscriptionId $subid 25 | $validLocations = Get-AzureRMLocation | where-object Providers -Contains Microsoft.Storage |Sort-Object Location | ForEach-Object {$_.Location} 26 | ############################################################################################# 27 | 28 | ############################## Stop and Mark VM as Generalised ############################## 29 | try 30 | { 31 | Stop-AzureRmVM -ResourceGroupName $rgName -Name $vmName -Force 32 | } 33 | catch 34 | { 35 | Write-Output "Error Stopping VM" 36 | Write-Output $_.Exception.Message 37 | } 38 | 39 | try 40 | { 41 | Set-AzureRmVm -ResourceGroupName $rgName -Name $vmName -Generalized 42 | } 43 | catch 44 | { 45 | Write-Output "Error Generalizing VM" 46 | Write-Output $_.Exception.Message 47 | } 48 | 49 | ############################################################################################# 50 | 51 | ################################### Remove Previous Images ################################## 52 | $images = Get-AzureRMResource -ResourceType Microsoft.Compute/images | ForEach-Object {$_.Name} 53 | foreach ($currentimage in $images) 54 | { 55 | Remove-AzureRmImage -ResourceGroupName $rgName -ImageName $currentimage -force 56 | } 57 | ############################################################################################# 58 | 59 | ######################## Create a Storage Container in all Locations ######################## 60 | foreach ($currentLocation in $validLocations) 61 | { 62 | $storageAccName = "tmplt" + $currentLocation 63 | 64 | Write-Output "Creating Destination Storage Account: $storageAccName" 65 | # Create Destination Storage Account (skip if exists) 66 | if ($null -eq (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name ${storageAccName} -ErrorAction SilentlyContinue)) 67 | { 68 | try 69 | { 70 | New-AzureRmStorageAccount -ResourceGroupName $rgName -AccountName $storageAccName -Location $currentLocation -Type $skuName -EnableHttpsTrafficOnly 1 -ErrorAction Continue 71 | $targetStorageContext = (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName).Context 72 | New-AzureStorageContainer -Name $imageContainerName -Context $targetStorageContext -Permission Container 73 | } 74 | catch 75 | { 76 | Write-Output $_.Exception.Message 77 | } 78 | } 79 | else 80 | { 81 | Write-Output " SA $storageAccName Already Exists!" 82 | } 83 | 84 | } 85 | ############################################################################################# 86 | 87 | ########################## Snapshot the OS Disk of the template VM ########################## 88 | $vm = Get-AzureRmVM -ResourceGroupName $rgName -Name $vmName 89 | $disk = Get-AzureRmDisk -ResourceGroupName $rgName -DiskName $vm.StorageProfile.OsDisk.Name 90 | $snapshot = New-AzureRmSnapshotConfig -SourceUri $disk.Id -CreateOption Copy -Location ukwest 91 | New-AzureRmSnapshot -ResourceGroupName $rgName -Snapshot $snapshot -SnapshotName $SnapshotName 92 | ############################################################################################# 93 | 94 | ########################## Copy the Snapshot to SAs in all Regions ########################## 95 | Write-Output "Starting Snapshot copy to other regions" 96 | 97 | $snapSasUrl = Grant-AzureRmSnapshotAccess -ResourceGroupName $rgName -SnapshotName $snapshotName -DurationInSecond 7200 -Access Read 98 | 99 | foreach ($currentLocation in $validLocations) 100 | { 101 | try 102 | { 103 | $storageAccName = "tmplt" + $currentLocation 104 | $imageBlobName = "tmpltsnap" + $currentLocation 105 | 106 | $targetStorageContext = (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName).Context 107 | 108 | Start-AzureStorageBlobCopy -AbsoluteUri $snapSasUrl.AccessSAS -DestContainer $imageContainerName -DestContext $targetStorageContext -DestBlob $imageBlobName -Force 109 | Write-Output "started copying $imageBlobName to $currentLocation" 110 | } 111 | catch 112 | { 113 | Write-Output $_.Exception.Message 114 | } 115 | } 116 | ############################################################################################# 117 | # Due to Runbook execute time limitations, this Runbook will terminate, allowing time for the 118 | # Copy process, which will be checked in "5. Create all Images and Cleanup" 119 | ############################################################################################# -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /CreateVM.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subsctiption in which you want to deploy the VM 3 | $imageRG = #Name of the Image file created in "3. Create all Images and Cleanup" 4 | $rgName = #Resource Group in which you want to deploy the VM 5 | $keyVaultName = #Name of the Keyvault within the RG you are deploying the VMs 6 | $subnets = #CIDR ranges you want to be able to access the VM, e.g. "10.1.0.0/16", "192.168.2.0/24" 7 | $vmSize = #SKU size for the VM you want to deploy https://docs.microsoft.com/en-us/azure/cloud-services/cloud-services-sizes-specs 8 | ############################################################################################# 9 | 10 | ############################################################################################# 11 | Write-Host " .----." 12 | Write-Host " .---------. | == |" 13 | Write-Host ' |.-"""""-.| |----|' 14 | Write-Host " || || | == |" 15 | Write-Host " || || |----|" 16 | Write-Host " |'-.....-'| |::::|" 17 | Write-Host ' `"")---(""` |___.|' 18 | Write-Host " /:::::::::::\" _ "" 19 | Write-Host "`n" 20 | Write-Host "This script will create a VM from an template in a region to be specified" 21 | Write-Host "`n" 22 | Write-Host "### Checking Windows Azure PowerShell version: " -NoNewLine 23 | Try { 24 | Import-Module AzureRM -MinimumVersion 3.0.0 -ErrorAction Stop 25 | } Catch { 26 | Write-Host -ForegroundColor Red "Failed!" 27 | Write-Host "Please ensure you have the latest version of Azure PowerShell (please see https://azure.microsoft.com/en-us/documentation/articles/powershell-install-configure/)." 28 | exit 29 | } 30 | Write-Host -ForegroundColor Green "Good!" 31 | Write-Host "### Logging into Azure (please use your user@domain)" 32 | Login-AzureRmAccount -ErrorAction Stop 33 | Set-AzureRMContext -SubscriptionId $subid 34 | ############################################################################################# 35 | 36 | ################################### Get User Input ########################################## 37 | Do { $UniqueName = Read-Host "Please specify a Unique ID for the VM (e.g. Case Number):" } while ($UniqueName -eq "") 38 | 39 | Write-Host "`n### Locations available ###" 40 | $validLocations = Get-AzureRMLocation | Sort-Object Location | ForEach-Object {$_.Location} 41 | $storageLocations = Get-AzureRMLocation | where-object Providers -Contains Microsoft.Storage |Sort-Object Location | ForEach-Object {$_.Location} 42 | $validLocations 43 | 44 | Do { 45 | $location = Read-Host "Enter Location" 46 | } while (-not $validLocations.Contains($location)) 47 | 48 | if ($location -notin $storageLocations){$location = "eastus"} 49 | 50 | $vmName = "$UniqueName" 51 | $DomainLabel = "$UniqueName" 52 | $imageName = "tmpltimage$location" 53 | $vnetName = "$UniqueName-Vnet" 54 | $subnetName = "$UniqueName-Subnet" 55 | $nsgName = "$UniqueName-NSG" 56 | $ipName = "$UniqueName-PIP" 57 | ############################################################################################# 58 | 59 | ################################ Create User Account ######################################## 60 | try 61 | { 62 | $Username = Read-Host "Enter Username" 63 | Add-Type -AssemblyName System.Web 64 | $securePassword = convertto-securestring ([System.Web.Security.Membership]::GeneratePassword(32,8)) -asplaintext -force 65 | $null = Set-AzureKeyVaultSecret -VaultName $keyVaultName -Name $UniqueName -SecretValue $securePassword 66 | $password = Get-AzureKeyVaultSecret -VaultName $keyVaultName -Name $UniqueName 67 | $cred = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ($Username, $password.SecretValue) 68 | Write-Host -ForegroundColor Green "`nSuccessfully created user and password pair. Username: $Username, Passoword: saved in KeyVault" 69 | } 70 | catch 71 | { 72 | Write-Host -ForegroundColor Red "Failed to Create User Password Pair" 73 | Write-Host $_.Exception.Message 74 | } 75 | ############################################################################################# 76 | 77 | ######################################## Create NSG ######################################### 78 | try 79 | { 80 | #Create the NSG 81 | $rdpRule = New-AzureRmNetworkSecurityRuleConfig -Name myRdpRule -Description "Allow RDP" ` 82 | -Access Allow -Protocol Tcp -Direction Inbound -Priority 110 ` 83 | -SourceAddressPrefix Internet -SourcePortRange * ` 84 | -DestinationAddressPrefix * -DestinationPortRange 3389 85 | 86 | $nsg = New-AzureRmNetworkSecurityGroup -ResourceGroupName $rgName -Location $location ` 87 | -Name $nsgName -SecurityRules $rdpRule -Force 88 | Write-Host -ForegroundColor Green "Created NSG and added custom RDP rule" 89 | } 90 | catch 91 | { 92 | Write-Host -ForegroundColor Red "Failed to Create NSG with custom RDP rule" 93 | Write-Host $_.Exception.Message 94 | } 95 | 96 | try 97 | { 98 | # Add Sprcified IPs or Ranges 99 | # Remove the open rdp rule 100 | $nsg | Remove-AzureRmNetworkSecurityRuleConfig -Name "myRdpRule" > $null 101 | $index=100 102 | foreach ($cidr in $subnets) { 103 | $name = "rpd-in-$index"; 104 | $nsg | Add-AzureRmNetworkSecurityRuleConfig -Name $name -Description "Allow RDP From Specified CIDR Range" -Access Allow -Protocol * -Direction Inbound -Priority $index -SourceAddressPrefix $cidr -SourcePortRange * -DestinationAddressPrefix * -DestinationPortRange 3389 > $null 105 | $index++ 106 | } 107 | $nsg | Set-AzureRmNetworkSecurityGroup > $null 108 | Write-Host -ForegroundColor Green "IPs added to RDP NSG" 109 | } 110 | catch 111 | { 112 | Write-Host -ForegroundColor Red "IPs failed to be added to RDP NSG" 113 | Write-Host $_.Exception.Message 114 | } 115 | ############################################################################################ 116 | 117 | ####################################### Create the VM ###################################### 118 | $ImageId = "/subscriptions/$subID/resourceGroups/$imageRG/providers/Microsoft.Compute/images/$imageName" 119 | 120 | New-AzureRmVm ` 121 | -ResourceGroupName $rgName ` 122 | -Location $location ` 123 | -Name $vmName ` 124 | -Credential $cred ` 125 | -VirtualNetworkName $vnetName ` 126 | -SubnetName $subnetName ` 127 | -PublicIpAddressName $ipName ` 128 | -DomainNameLabel $DomainLabel.ToLower() ` 129 | -SecurityGroupName $nsgName ` 130 | -Image $imageID ` 131 | -Size $vmSize 132 | 133 | ############################################################################################ 134 | 135 | #################################### Return Variables ###################################### 136 | # Verify that it was created 137 | #$vmList = Get-AzureRmVM -ResourceGroupName $rgName 138 | Write-Host -ForegroundColor Green "VM $vmName created successfully. To login to the VM, use the following information:" 139 | Write-Host -ForegroundColor Green "RDP (port 3389) to" (Get-AzureRmPublicIpAddress -name $ipName -ResourceGroupName $rgName).IpAddress 140 | Write-Host -ForegroundColor Green "RDP (port 3389) to" (Get-AzureRmPublicIpAddress -name $ipName -ResourceGroupName $rgName).DnsSettings.Fqdn 141 | Write-Host -ForegroundColor Green "Username: $Username" 142 | Write-Host -ForegroundColor Green "The password for this VM is in the Key Vault: $keyVaultName, within the RG $rgName, with the name - $UniqueName" 143 | ############################################################################################ -------------------------------------------------------------------------------- /5. Create all Images and Cleanup.ps1: -------------------------------------------------------------------------------- 1 | ######################################## variables ########################################## 2 | $subid = #Subsctiption ID in which you want to deploy the VM 3 | $subName = #Subsctiption Name in which you want to deploy the VM 4 | $vmName = #Name of the VM Tamplate Created in "1. Build New Template to Update" 5 | $rgName = #Name of the Resource Group the VM Tamplate Resides in, in "1. Build New Template to Update" 6 | $vnetName = #Name of the vnet Created in "1. Build New Template to Update" 7 | $ipName = #Name of the IP Created in "1. Build New Template to Update" 8 | $nsgName = ##Name of the NSG Tamplate Created in "1. Build New Template to Update" 9 | $imageContainerName = #Storage Container Name within the Storage Account created in "4. Snapshot and Move to SAs" e.g "images" 10 | $SnapshotName = #Name given to the snapshot created in "4. Snapshot and Move to SAs" e.g. "temporary-snap" 11 | ############################################################################################# 12 | 13 | ################################ Login As Automation Account ################################ 14 | try 15 | { 16 | $Conn = Get-AutomationConnection -Name AzureRunAsConnection 17 | Login-AzureRmAccount -ServicePrincipal -Tenant $Conn.TenantID ` 18 | -ApplicationId $Conn.ApplicationID -CertificateThumbprint $Conn.CertificateThumbprint 19 | Write-Output "Successfully connected as Automation Account" 20 | } 21 | catch 22 | { 23 | Write-Output "Error connecting as Automation Account" 24 | } 25 | 26 | $null = Set-AzureRMContext -SubscriptionId $subid 27 | $validLocations = Get-AzureRMLocation | where-object Providers -Contains Microsoft.Storage |Sort-Object Location | ForEach-Object {$_.Location} 28 | ############################################################################################# 29 | 30 | ################################### Check Status of Copy #################################### 31 | $completedArray = @() 32 | $complete = "0" 33 | 34 | while ($complete -eq 0) 35 | { 36 | foreach ($currentLocation in $validLocations) 37 | { 38 | $storageAccName = "tmplt" + $currentLocation 39 | $imageBlobName = "tmpltsnap" + $currentLocation 40 | $targetStorageContext = (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName).Context 41 | $CopyState = Get-AzureStorageContainer -Context $targetStorageContext -Name $imageContainerName | Get-AzureStorageBlobCopyState -Blob $imageBlobName 42 | $Message = $imageBlobName + " " + $CopyState.Status + " {0:N2}%" -f (($CopyState.BytesCopied/$CopyState.TotalBytes)*100) 43 | Write-Output $Message 44 | 45 | Write-Output "$CopyState.Status" 46 | 47 | if ($CopyState.Status -eq "Success") 48 | { 49 | $completedArray += "1" 50 | } 51 | elseif ($CopyState.Status -eq "Failed") 52 | { 53 | $completedArray += "1" 54 | } 55 | else 56 | { 57 | $completedArray += "0" 58 | } 59 | } 60 | 61 | Write-Output $completedArray 62 | 63 | if ($completedArray.Contains("0")) 64 | { 65 | $completedArray = @() 66 | } 67 | else 68 | { 69 | $complete = "1" 70 | } 71 | 72 | Start-Sleep -Seconds 300 73 | } 74 | ############################################################################################# 75 | 76 | ################################ Create Snapshot of each vhd ################################ 77 | foreach ($currentLocation in $validLocations) 78 | { 79 | try 80 | { 81 | $storageAccName = "tmplt" + $currentLocation 82 | $imageBlobName = "tmpltsnap" + $currentLocation 83 | 84 | $targetStorageContext = (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName).Context 85 | 86 | # Get the full URI to the blob 87 | $osDiskVhdUri = ($targetStorageContext.BlobEndPoint + $imageContainerName + "/" + $imageBlobName) 88 | Write-Output $osDiskVhdUri 89 | # Build up the snapshot configuration, using the target storage account's resource ID 90 | $snapshotConfig = New-AzureRmSnapshotConfig -AccountType Standard_LRS ` 91 | -OsType Windows ` 92 | -Location $currentLocation ` 93 | -CreateOption Import ` 94 | -SourceUri $osDiskVhdUri ` 95 | 96 | # Create the new snapshot in the target region 97 | $newSnapshotName = "snap" + $currentlocation 98 | New-AzureRmSnapshot -ResourceGroupName $rgName -SnapshotName $newSnapshotName -Snapshot $snapshotConfig 99 | write-output "Creating Snapshot $newSnapshotName" 100 | } 101 | catch 102 | { 103 | Write-Output $_.Exception.Message 104 | } 105 | } 106 | ############################################################################################# 107 | 108 | ############################### Create Image of Snapshots ################################### 109 | foreach ($currentLocation in $validLocations) 110 | { 111 | 112 | try 113 | { 114 | $newSnapshotName = "snap" + $currentLocation 115 | $imageName = "tmpltimage" + $currentLocation 116 | $snapshot = Get-AzureRmSnapshot -ResourceGroupName $rgName -SnapshotName $newSnapshotName 117 | $imageConfig = New-AzureRmImageConfig -Location $currentlocation 118 | $imageConfig = Set-AzureRmImageOsDisk -Image $imageConfig -OsState Generalized -OsType Windows -SnapshotId $snapshot.Id 119 | New-AzureRmImage -ImageName $imageName -ResourceGroupName $rgName -Image $imageConfig 120 | } 121 | catch 122 | { 123 | Write-output $_.Exception.Message 124 | } 125 | 126 | } 127 | ############################################################################################# 128 | 129 | ######################################## Remove SAs ######################################### 130 | foreach ($currentLocation in $validLocations) 131 | { 132 | $storageAccName = "tmplt" + $currentLocation 133 | 134 | if ($null -ne (Get-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName -ErrorAction SilentlyContinue)) 135 | { 136 | try 137 | { 138 | write-output "Removing Storage Account: $storageAccName" 139 | Remove-AzureRmStorageAccount -ResourceGroupName $rgName -Name $storageAccName -Force -ErrorAction SilentlyContinue 140 | } 141 | catch 142 | { 143 | Write-output $_.Exception.Message 144 | } 145 | } 146 | else 147 | { 148 | write-output "Storage Account: $storageAccName Not Found" 149 | } 150 | } 151 | ############################################################################################# 152 | 153 | ################################### Remove Snapshots ######################################### 154 | foreach ($currentLocation in $validLocations) 155 | { 156 | 157 | try 158 | { 159 | $tempSnapshotName = "snap" + $currentLocation 160 | Remove-AzureRmSnapshot -ResourceGroupName $rgName -SnapshotName $tempSnapshotName -Force 161 | } 162 | catch 163 | { 164 | Write-output $_.Exception.Message 165 | } 166 | 167 | } 168 | ############################################################################################# 169 | 170 | ################################## Remove the Template VM ################################### 171 | Select-AzureRMSubscription -SubscriptionName $subName 172 | $vm = Get-AzureRmVM -ResourceGroupName $rgName -Name $vmName 173 | $diskName = $vm.StorageProfile.OsDisk.Name 174 | 175 | try 176 | { 177 | Remove-AzureRmVM -ResourceGroupName $rgName -Name $vmName -Force -ErrorAction SilentlyContinue 178 | Write-Output "Removed VM $vmName" 179 | } 180 | catch 181 | { 182 | Write-output $_.Exception.Message 183 | } 184 | try 185 | { 186 | Remove-AzureRmDisk -ResourceGroupName $rgName -Name $diskName -Force -ErrorAction SilentlyContinue 187 | Write-Output "Removed Disk $diskName" 188 | } 189 | catch 190 | { 191 | Write-output $_.Exception.Message 192 | } 193 | try 194 | { 195 | Remove-AzureRmNetworkInterface -ResourceGroupName $rgName -Name $vmName -Force -ErrorAction SilentlyContinue 196 | Write-Output "Removed NIC $vmName" 197 | } 198 | catch 199 | { 200 | Write-output $_.Exception.Message 201 | } 202 | try 203 | { 204 | Remove-AzureRmNetworkSecurityGroup -ResourceGroupName $rgName -Name $nsgName -Force -ErrorAction SilentlyContinue 205 | Write-Output "Removed NSG $nsgName" 206 | } 207 | catch 208 | { 209 | Write-output $_.Exception.Message 210 | } 211 | try 212 | { 213 | Remove-AzureRmPublicIpAddress -ResourceGroupName $rgName -Name $ipName -Force -ErrorAction SilentlyContinue 214 | Write-Output "Removed PIP $ipName" 215 | } 216 | catch 217 | { 218 | Write-output $_.Exception.Message 219 | } 220 | try 221 | { 222 | Remove-AzureRmVirtualNetwork -ResourceGroupName $rgName -Name $vnetName -Force -ErrorAction SilentlyContinue 223 | Write-Output "Removed vNet $vnetName" 224 | } 225 | catch 226 | { 227 | Write-output $_.Exception.Message 228 | } 229 | try 230 | { 231 | Remove-AzureRmSnapshot -ResourceGroupName $rgName -SnapshotName $SnapshotName -Force -ErrorAction SilentlyContinue 232 | Write-Output "Removed Snapshot $SnapshotName" 233 | } 234 | catch 235 | { 236 | Write-output $_.Exception.Message 237 | } 238 | ############################################################################################# -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | The collection of scripts in this repository plus this readme will guide a user through the creation of an automated solution to create a templated image of a Windows VM which is updated and maintained weekly. 2 | 3 | Step 0. - Pre Requisites: 4 | 5 | It is expected that the user will have access to: 6 | * An Azure Subscription or MSDN Account with Azure Credits. 7 | * Rights within azure to create and edit resources. 8 | 9 | Step 1 - Environment Setup: 10 | 11 | * Create a Resource Group specifically for the use of storing and maintianing the VM template, images and automation 12 | * Within the Resourse group add an Automation Account, by using the Add option in the Resource Group, searching for and selecting Automation 13 | * Provide a relevant name for the automation account, ensure it is in the correct subscription and resource group, ensure the 'Create Azure Run As Account' it selected 'yes' 14 | * Select the newly created Automation Account, select modules in the left hand menu and ensure the following modules are present. if not use the 'add a module' and follow the UI instuctions to add them: 15 | * Azure 16 | * Azure.Storage 17 | * AzureRM.Automation 18 | * AzureRM.Compute 19 | * AzureRM.KeyVault 20 | * AzureRM.Network 21 | * AzureRM.Profile 22 | * AzureRM.Resources 23 | * AzureRM.Sql 24 | * AzureRM.Storage 25 | * Within the Resourse group add Key Vault, by using the Add option in the Resource Group, searching for an selecting Key Vault 26 | * Provide a relevant name for the Key Vault, ensure it is in the correct Subscription and Resource Group. 27 | * Select 'Access Policies' in the Key Vault Creation Page, which should have 1 principal (you). In the Access Policies menu select 'Add New' 28 | * In the Add Access Policy Menu, select 'Secret Management' as the Template. Click Add Principal and seach for and select the name of the previously created automation account 29 | * OK everything and Create the Key Vault 30 | * Within the Resourse group add Storage Account, by using the Add option in the Resource Group, searching for an selecting 'Storage account - blob, file, table, queue' 31 | * Provide a relevant name for the Storage Account, ensure it is in the correct Subscription and Resource Group. 32 | * Select Stoage Account from the Resource Group Dashboard, in the left hand menu select 'Blobs' under the 'Blob Service' section 33 | * In the Blobs pane that loads, click on '+ Container' at the top of the pane 34 | * Provide a name for this blob store (As it is going to be used to store scripts I recommend calling it 'scripts'), ensure the access level is set to 'Private' 35 | * Ok Eveything to create the Blog container. 36 | * Browse to the Blob Container in the Azure Portal, (Resource Group > Storage Account > Blob Container) 37 | * in the container pane, select the upload options and upload the following 2 scripts to this location: 38 | 1. "2a. WindowsUpdate.ps1" 39 | 2. "3a. Sysprep.ps1" 40 | 41 | 42 | Step 2 - Create Automation Runbooks: 43 | 44 | * Browse to the Runbooks section of the Automation Account previously created (Resource Group > Automation Account > Runbooks) 45 | 1. Add a runbook, select 'Create a new runbook', give this a name ("DeployTemplateVM"), Select 'PowerShell' in Runbook type then create the Runbook 46 | * When the Runbook is created it will take you to the 'Edit PowerShell Runbook' pane, enter the code from '1. Build New Template to Update.ps1' 47 | * enter the relevant variables in the top section of the code and use the test pane to ensure the code executes correctly. 48 | 2. Add a runbook, select 'Create a new runbook', give this a name ("InvokeUpdate"), Select 'PowerShell' in Runbook type then create the Runbook 49 | * When the Runbook is created it will take you to the 'Edit PowerShell Runbook' pane, enter the code from '2. Invoke Updates.ps1' 50 | * enter the relevant variables in the top section of the code and use the test pane to ensure the code executes correctly. 51 | 3. Add a runbook, select 'Create a new runbook', give this a name ("InvokeSysprep"), Select 'PowerShell' in Runbook type then create the Runbook 52 | * When the Runbook is created it will take you to the 'Edit PowerShell Runbook' pane, enter the code from '3. Invoke Sysprep.ps1' 53 | * enter the relevant variables in the top section of the code and use the test pane to ensure the code executes correctly. 54 | 4. Add a runbook, select 'Create a new runbook', give this a name ("SnapshotAndCopy"), Select 'PowerShell' in Runbook type then create the Runbook 55 | * When the Runbook is created it will take you to the 'Edit PowerShell Runbook' pane, enter the code from '4. Snapshot and Move to SAs.ps1' 56 | * enter the relevant variables in the top section of the code and use the test pane to ensure the code executes correctly. 57 | 5. Add a runbook, select 'Create a new runbook', give this a name ("ImageAndCleanup"), Select 'PowerShell' in Runbook type then create the Runbook 58 | * When the Runbook is created it will take you to the 'Edit PowerShell Runbook' pane, enter the code from '5. Create all Images and Cleanup.ps1' 59 | * enter the relevant variables in the top section of the code and use the test pane to ensure the code executes correctly. 60 | 61 | Step 3 - Create VM (Template/Baseline): 62 | 63 | * Create a Windows VM in azure, (whatever version of windows you would like) install relevant tools ect. to setup the environemnt. 64 | * Once you have built the VM and installed all required software, you need to use 'sysprep.exe' to prepare the VM for imaging. 65 | * In the VM, open Powershell as an Administrator enter the following, bare in mind that this will shutdown the VM and remove all user specific files.: 66 | 67 | >C:\Windows\System32\sysprep\sysprep.exe /generalize /oobe /shutdown 68 | 69 | * The VM will disconnect the RDP session. After while you will see the dashed lines for the resource useage data for the VM in the VM overview, this signifies the sysprep has completed. 70 | 71 | Step 4 - Create set of images. 72 | 73 | * Once the Sysprep has completed, browse to the runbook "SnapshotAndCopy" (Resource Group > SnapshotAndCopy) 74 | * In the Runbook, Click the 'Start' button at the top of the screen to initiate the first half of the image creation process 75 | * For obvious reasons Azure Runbooks will not run forwever and the copy process takes a few hours. Wait about 3 hours from the time the 'SnapshotAndCopy' was initiated 76 | * After waiting a couple of hours, browse to the runbook "ImageAndCleanup" (Resource Group > ImageAndCleanup) 77 | * In the Runbook, Click the 'Start' button at the top of the screen to initiate the second half of the image creation process. 78 | 79 | Step 5 - Check and Test. 80 | 81 | * Once completed, browse to your Resource Group, there you should see an image for all the locations you specified along with the runbooks, automation account, key vault and storage account required for the automation. 82 | * It is recommended to test a VM deployment from one of the newlsy created images to ensure that eveything has worked as expected. 83 | * You can do this by running the script "CreateInvestigationVM_v2.ps1" on you local machine (you will need to configure the script with relevant variables) 84 | * This will create a VM from one of the images you have created using the automation, (depending on your configuration you may need to create a new Resource Group with a Key Vault) 85 | 86 | Step 6 - Create Schedual. 87 | 88 | * After everything has been checked and you are happy to automate the process of updating the template and deploying updated images automatically, you need to create a scheduel to execute the runbooks in order. 89 | * Navigte to the Schedules within the Automation Account (Resource Group > Automation Account > Schedules). 90 | * Select "+ Add a schedule", provide a name, a start date and time, a timezone and a reccurrence, examples are as follows: 91 | * Name Start Date / Time Time Zone Reccurrence 92 | * Deploy_Saturday_PM 05/01/2019 23:00 UTC Every 1 week on a Sunday 93 | * Update_Sunday_AM_1 06/01/2019 00:00 UTC Every 1 week on a Sunday 94 | * Update_Sunday_AM_2 06/01/2019 01:00 UTC Every 1 week on a Sunday 95 | * Sysprep_Sunday_AM 06/01/2019 02:00 UTC Every 1 week on a Sunday 96 | * CopyToSAs_Sunday_AM 06/01/2019 04:00 UTC Every 1 week on a Sunday 97 | * ImageAndCleanup_Sunday_AM 06/01/2019 07:00 UTC Every 1 week on a Sunday 98 | * Finally you need to Assing the relevant Runbook to the Schedule you have just created. Navigate to each runbook (Resourse Group > Runbook) 99 | * Select Schedules in the left side menu, select 'Link a schedule to your runbook', select the relevant schedule then click OK to confirm the selection. 100 | * examples are as follows: 101 | * Schedule Name Associated Runbook 102 | * Deploy_Saturday_PM DeployTemplateVM 103 | * Update_Sunday_AM_1 InvokeUpdate 104 | * Update_Sunday_AM_2 InvokeUpdate 105 | * Sysprep_Sunday_AM InvokeSysprep 106 | * CopyToSAs_Sunday_AM SnapshotAndCopy 107 | * ImageAndCleanup_Sunday_AM ImageAndCleanup 108 | 109 | Thats it, you now have an automated setup to ensure regionally deployed images are updated regulaly. --------------------------------------------------------------------------------