The Cost of Running a Personal Windows 10 VM in Azure

As amazing as it may sound for an IT professional of many years, aside from my work laptops, I do not own a Windows computer! For my personal computing requirements, I use a £200 Chromebook to connect to the internet and do most everything I need in the cloud.

My dear wife, on the other hand, is not a fan of the Chromebook and likes a ‘good old-fashioned’ Windows computer. But since her computing requirements are minimal, I decided to investigate the cost of running a Windows 10 VM in Azure instead of buying a new Windows laptop that she won’t use very often. Turns out, it’s quite a cost effective way to run a Windows 10 OS 🙂 The only things you really pay for are the disk storage and VM compute time. To access the VM, we simply use a remote desktop app on the Chromebook.

The first thing you need is an Azure subscription. Currently you get some credits for the first month, then some services that are free for the first 12 months. Even after that, if you have no resources in the subscription you won’t pay a penny for it.

You can create a Windows 10 VM from the Azure Marketplace. Creating the VM will create a few resources in a resource group such as a NIC, an NSG (network security group), a disk and of course the VM itself. To save on cost, I didn’t add any data disk and just used the 127GiB disk that comes with the OS. I also used the basic sku for the NSG, and I didn’t assign a static public IP address – I simply added a DNS name. You’ll get charged for a static IP (something like £0.06 p/day) but if you use a dynamic IP with a DNS name you won’t get charged anything.

For the disk, I somehow assumed that I would need a Premium SSD to get good performance as that’s what I would typically use for corporate VMs, but as this is for home use and I’m not really concerned about SLAs, I experimented with the Standard SSD and the Standard HDD as well. I was surprised to find the the Standard HDD was perfectly adequate for every day use and I didn’t really notice much difference in performance with either of the SSD options. Of course you do get less IOPS with an HDD, but that hasn’t been any issue. Since an HDD is much cheaper than an SSD, it made sense to use one.

For the VM size, I used an F4s_V2 which is compute optimized, has 8GB RAM, 4vCPUs and runs great. You could certainly get away with a smaller size though and shave your compute costs, something like a DS2_V3 and it’ll still run just fine.

In the tables below I summarized the actual costs of running the VM and also compared the costs of using Premium SSD/Standard SSD/Standard HDD. These costs are in GBP (£) and are in the UK South Azure region and are true at the time of writing – prices will vary based on region and currency and VM compute hours. The costs are also from actual invoiced costs – not from the Azure price calculator. The price calculator can give a good ball-park figure but in my experience the actual cost will be different…

Note: there are also data egress costs, ie data coming out of Azure. Downloads etc within the VM are ingress and don’t generally get charged. But even for egress you get the first 5GB free anyway (see here).

Time periodCost (£ GBP)
Per hour0.16
Per day3.84
Per month116.8
Per year1401.6
Compute costs for F4s_V2 VM
Time periodCost (£ GBP) Premium SSDCost (£ GBP) Standard SSDCost (£ GBP) Standard HDD
Per day0.570.250.12
Per month17.347.63.65
Per year208.0591.2543.8
Disk storage costs

So the base cost for owning the VM is £3.65 p/month using a Standard HDD. On top of that is the compute time. For example, if I use the VM for 20 hours in a month, the compute cost is £3.20 for the month. Add that to the base cost, and it’s £6.85 for the month. That’s not bad 🙂

Some key things to remember are that you always pay for the disk storage whether you use the VM or not. You only pay for compute time when you actually turn on the VM and use it. Always remember to actually stop your VM when finished (not just shut down the OS) so that the resources are de-allocated and you are not charged for unnecessary compute time. Use the Auto-shutdown feature to ensure the VM gets stopped every night. Also, since you have a public IP address it’s a good idea to use some NSG rules to restrict who or from where and on which ports you can access your VM.

Using an Azure VM for personal computing needs is a great option – you benefit from the elasticity and scalability of the cloud, and you only pay for what you use. You can scale up or scale down at any time according to your needs and you can set a budget and keep your eye on how much you’re spending using Azure cost management.

Get HP Driver Pack Info with PowerShell – Web Scraping Method

So I was preparing an OSD task sequence in ConfigMgr to deploy Windows 10 1909 and I wanted to know if any of the HP workstations I would be deploying to had updated driver packs available for 1909, since I had simply copied the task sequence used for 1903.

A while ago I posted a blog with a script to download the latest driver packs from Dell using a web-scraping method, so I decided to take a similar approach for HP driver packs. HP publish a list of the driver packs available for various models and OS versions on the web in a tabular format, so I decided to try to convert that HTML table into data that could also be displayed in table format in PowerShell as well as being queryable, and the script below is the result.

This kind of data displays well in PowerShell’s gridview, like so:

Get-HPDriverPacks | Out-GridView 

In the first column you find the models and the next columns contain the driver pack version, release date and download URL for the various OS versions. You can then use the gridview’s native filtering capabilities to find something specific:

By default, the script will get the 64-bit driver packs, but you can also get 32-bit for Windows 7 etc (you’re not still using Windows 7 are you?!):

Get-HPDriverPacks -Architecture '32-bit'

I can also query the data within PowerShell directly, for example:

$DPs = Get-HPDriverPacks
$DPs | where {$_.Model -match "1040"} | Select Model,"Windows 10 64-bit, 1909"

To find out which models had driver packs for Windows 10 1909 that have been updated since the 1903 version, I did the following:

$DPs | 
    Where {$_."Windows 10 64-bit, 1909" -ne "-" -and $_."Windows 10 64-bit, 1909" -ne $_."Windows 10 64-bit, 1903"} | 
    Select Model,"Windows 10 64-bit, 1909" |

So those are the models whose driver packs I can update in the 1909 OSD task sequence.

You can also download the driver packs from this data too, eg:

$URL = ($DPs | where {$_.Model -match "1040 G6"} | Select -ExpandProperty "Windows 10 64-bit, 1909").Split()[0]
Invoke-WebRequest -Uri $URL -OutFile "C:\temp\$($url.Split('/') | Select -Last 1)"

Get Program Execution History from a ConfigMgr Client with PowerShell

Have you ever been in the situation where something unexpected happens on a users computer and people start pointing their fingers at the ConfigMgr admin and asking “has anyone deployed something with SCCM?” Well, I decided to write a PowerShell script to retrieve the execution history for ConfigMgr programs on a local or remote client. This gives clear visibility of when and which deployments such as applications/programs/task sequences have run on the client and hopefully acquit you (or prove you guilty!)

Program execution history can be found in the registry but it doesn’t contain the name of the associated package, so I joined that data with software distribution data from WMI to give a better view.

You can run the script against the local machine, or a remote machine if you have PS remoting enabled. You can also run it against multiple machines at the same time and combine the data if desired. I recommend to pipe the results to grid view.

Get-CMClientExecutionHistory -Computername PC001,PC002 | Out-GridView

Get Previous and Scheduled Evaluation Times for ConfigMgr Compliance Baselines with PowerShell

I was testing a compliance baseline recently and wanted to verify if the schedule defined in the baseline deployment is actually honored on the client. I set the schedule to run every hour, but it was clear that it did not run every hour and that some randomization was being used.

To review the most recent evaluation times and the next scheduled evaluation time, I had to read the scheduler.log in the CCM\Logs directory, because I could only find a single last evaluation time recorded in WMI.

The following PowerShell script reads which baselines are currently deployed to the local machine, displays a window for you to choose one, then basically reads the Scheduler log to find when the most recent evaluations were and when the next one is scheduled.

Select a baseline
Baseline evaluations

[Unsupported] Getting / triggering ConfigMgr Client Programs using Software Center Code

An odd title perhaps, but I recently had a requirement to retrieve the deadline for a deployed task sequence on the client side in the user context using PowerShell. You can find this info in WMI, using the CCM_Program class of the ROOT\ccm\ClientSDK namespace. Problem is, standard users do not have access to that.

I tried deploying a script in SYSTEM context to get the deadline from WMI and stamp it to a registry location where it could be read in the user context, however curiously the CCM_Program namespace is not accessible in SYSTEM context. A quick Google search assured me I was not alone scratching my head over that one.

I found a way to do it using a Software Center dll, which I’m sure is not supported, but it works at least. Run the following PowerShell code as the logged-on user to find the deadline for a deployed program (could be a classic package/program or task sequence).

$PackageID = "ABC0012B"
Add-Type -Path $env:windir\CCM\
$Connector = [Microsoft.SoftwareCenter.Client.Data.ClientConnectionFactory]::CreateDataConnector()
$Package = $Connector.AllProgramApplications | where {$_.PackageId -eq $PackageID}
If ($Package)
    $Deadline = Get-Date $Package.DeadlineDisplayValue

You can do some other nice things with that Software Center data connector class, for example, trigger a task sequence to run. But you didn’t hear that from me 😉

$PackageID = "ABC0012B"
Add-Type -Path $env:windir\CCM\
$Connector = [Microsoft.SoftwareCenter.Client.Data.ClientConnectionFactory]::CreateDataConnector()
$Package = $Connector.AllProgramApplications | where {$_.PackageId -eq $PackageID}

ConfigMgr Housekeeping Scripts

ConfigMgr is a bit like a garage – you throw all kinds of stuff in there over the years, and then one day you decide to go through everything and chuck out the stuff you don’t need anymore. It’s a time-consuming process and sometimes there are difficult decisions to be made – do I / don’t I? What if it might come in useful 3 years from now?! By the time you’ve finished going through everything you’re so exhausted you start chucking out everything for fear of having to do this again one day! But once it’s done, it’s done. For now…

Recently I ran a housekeeping project for a ConfigMgr environment and ended up writing a bunch of SQL queries to help identify items that are good candidates for deletion based on various criteria. I decided to publish them on GitHub in case they might help others in their own spring-cleaning efforts.

I’d welcome any contributions as it’s challenging to identify legacy items that might exist in any environment. In the initial commit, the following queries are included:

  • Active Applications not deployed or referenced in a Task Sequence
  • Application deployments with 0 deployment results or targeted at 0 resources
  • Boot images not referenced by a Task Sequence
  • Collections with 0 members
  • Compliance Baseline deployments with 0 deployment results or targeted to 0 resources
  • Compliance Items not used in a Compliance Baseline
  • Deployed Applications with no Last Enforcement Message in the last 180 days.
  • Disabled Compliance Baselines
  • Disabled Task Sequences
  • Driver Packages not referenced in a Task Sequence
  • Enabled Compliance Baselines not deployed
  • OS Image Packages not referenced in a Task Sequence
  • OS Upgrade Packages not referenced in a Task Sequence
  • Retired Applications
  • Software Update Deployment Packages not referenced by an Automatic Deployment Rule
  • Software Update Groups not deployed
  • Standard Package deployments with 0 deployment results or targeted to 0 resources
  • Standard Packages not deployed or referenced in a Task Sequence
  • Superseded Applications
  • Task Sequence deployments with 0 deployment results or targeted to 0 resources
  • Task Sequence deployments with no execution history in the last 180 days
  • Task Sequences not deployed

There’s also a couple of PowerShell scripts to help identify orphaned content in your content source share, but use these with appropriate discretion.

Setting the Computer Description During Windows Autopilot

I’ve been getting to grips with Windows Autopilot recently and, having a long history working with SCCM, I’ve found it hard not to compare it with the power of traditional OSD using a task sequence. In fact, one of my goals was to basically try to reproduce what I’m doing in OSD with Autopilot in order to end up with the same result – and it’s been a challenge.

I like the general concept of Autopilot and don’t get me wrong – it’s getting better all the time – but it still has its shortcomings that require a bit of creativity to work around. One of the things I do during OSD is to set the computer description in AD. That’s fairly easy to do in a task sequence; you can just script it and run the step using credentials that have the permission to make that change.

In Autopilot however (hybrid AAD join scenario), although you can run Powershell scripts too, they will only run in SYSTEM context during the Autopilot process. That means you either need to give computer accounts the permission to change their own properties in AD, or you have to find a way to run that code using alternate credentials. You can run scripts in the context of the logged-on user, but I don’t want to do that – in fact I disable the user ESP – I want to use a specific account that has those permissions.

You could use SCCM to do it post-deployment if you are co-managing the device, but ideally I want everything to be native to Autopilot where possible, and move away from the hybrid mentality of do what you can with Intune, and use SCCM for the rest.

It is possible to execute code in another user context from SYSTEM context, but when making changes in AD the DirectoryEntry operation kept erroring with “An operations error occurred”. After researching, I realized it is due to AD not accepting the authentication token as it’s being passed a second time and not directly. I tried creating a separate powershell process, a background job, a runspace with specific credentials – nothing would play ball. Anyway, I found a way to get around that by using the AccountManagement .Net class, which allows you to create a context using specific credentials.

In this example, I’m setting the computer description based on the model and serial number of the device. You need to provide the username and password for the account you will perform the AD operation with. I’ve put the password in clear text in this example, but in the real world we store the credentials in an Azure Keyvault and load them in dynamically at runtime with some POSH code to avoid storing them in the script. I hope in the future we will be able to run Powershell scripts with Intune in a specific user context, as you can with steps in an SCCM task sequence.

# Set credentials
$ADAccount = "mydomain\myADaccount"
$ADPassword = "Pa$$w0rd"

# Set initial description
$Model = Get-WMIObject -Class Win32_ComputerSystem -Property Model -ErrorAction Stop| Select -ExpandProperty Model
$SerialNumber = Get-WMIObject -Class Win32_BIOS -Property SerialNumber -ErrorAction Stop | Select -ExpandProperty SerialNumber
$Description = "$Model - $SerialNumber"

# Set some type accelerators
Add-Type -AssemblyName System.DirectoryServices.AccountManagement -ErrorAction Stop
$Accelerators = [PowerShell].Assembly.GetType("System.Management.Automation.TypeAccelerators")

# Connect to AD and set the computer description
$Domain = [System.DirectoryServices.ActiveDirectory.Domain]::GetCurrentDomain()
$PrincipalContext = [PrincipalContext]::new([ContextType]::Domain,$Domain,$ADAccount,$ADPassword)
$Account = [Principal]::FindByIdentity($PrincipalContext,[IdentityType]::Name,$env:COMPUTERNAME)
$LDAPObject = $Account.GetUnderlyingObject()
If ($LDAPObject.Properties["description"][0])
    $LDAPObject.Properties["description"][0] = $Description