Simple Azure VM Start/Stop Chaining using only Tags, Event Grid and Azure Functions

When you are migrating VMs from on-premise to Azure, you always have to evaluate the needed availability of several VMs. Your decisions in terms of VM size, storage tiers, and pricing options do rely on this evaluation. In my current migration of on-prem Remote Desktop Services to Azure Virtual Desktop, we have a Remote App that is used quite irregularly. Sometimes once per week, sometimes one to two days, and sometimes not a single time in a week. So we will go with Pay as you Go for these needed VM’s. We can deal with this behavior easily in Azure Virtual Desktop (planned shutdown and start on connect), but that’s only the frontend VM. In my scenario, I have some additional backend VMs which hold some services needed for the running application (licenseservice and some webservices for the DMS integration). We don’t need to run the backend VMs if nobody uses the frontend application, so I want to link the running state of these VMs with each other. 

The Frontend VM will be triggered by AVD’s “Starts on Connect” feature, and the needed backend server will be automatically started and deallocated depending on the Frontend VM.

I know there are solutions using EventGrid + Logic App + Azure Automation. But as you may already know, serverless Azure Functions are simply more efficient in terms of scaling and pricing.

In my setup how-to, I decided to simplify the setup with two single VMs. It shouldn’t be hard for someone to adjust, because in the end, you only have to tag the dependent VMs with the same value.
So let’s start …

Create some VMs for testing

We created two resource groups for testing. In my example, I created one named “Lab_init” and one “Lab_triggered” ? This way, we can define which VM can trigger the start process by putting them into this resource group.

Now we create 2 VMS, one in our “Lab_init” resource group and one in our “Lab_triggered” resource group.

I’m going with Ubuntu this time, but it doesn’t really matter. We only want to start and stop, so go with whatever you prefer.

Next, we need to tag our VM’s. The value can be whatever we want, but it has to match on all VM’s that we want to trigger. The code of our function (we’ll get to this later) loop through all subscriptions and search for VM’s with the same value in the bootbinding tag.

Setup Azure Function App

Now we get to the funny part.

We will create a new Azure Function App. (Serverless tier is good enough for our needs 🙂 )
Because our Function App needs to start/stop our Azure VMs across multiple subscriptions, we need a Managed Identity.
Add the Virtual Machine Contributor role for every subscription where you place VMs which needs to be triggered.
Our Azure Function App needs some modules to do its job. We have to add these to the requirements.psd1 file.
Note: You shouldn’t add the full Az module, as it’s quite large. Only add the submodules you really need.
Now we create our function and select “Azure Event Grid trigger”!
We enter the following code for our function:
param($eventGridEvent, $TriggerMetadata)

# Make sure to pass hashtables to Out-String so they're logged correctly
# $eventGridEvent | Out-String | Write-Host

$tAction = ($ -split "/")[-2]
$tVmName = ($ -split "/")[-1]
$tSubscriptionId = $

# preflight check
Write-Host "Check trigger action"
if (($tAction -ne "start") -and ($tAction -ne "deallocate")) {
    Write-Warning "Unsupported action: [$tAction], we stop here"
Write-Host "##################### Triggerinformation #####################"
Write-Host "Vm: $tVmName"
Write-Host "Action: $tAction"
Write-Host "Subscription: $tSubscriptionId"

Write-Host "Get information about trigger vm"
$context = Set-AzContext -SubscriptionId $tSubscriptionId

if ($context.Subscription.Id -ne $tSubscriptionId) {
    # break if no access
    throw "Azure Function have no access to subscription with id [$tSubscriptionId], check permissions of managed identity"

$tVm = Get-AzVM -Name $tVmName
$bindingGroup = $tVm.Tags.bootbinding

if (!$bindingGroup) {
    Write-Warning "No tag with bootbinding found for [$tVmName], check your tagging"

# main
Write-Host "Query all subscriptions"
$subscriptions = Get-AzSubscription

foreach ($sub in $subscriptions) {

    Write-Host "Set context to subscription [$($sub.Name)] with id [$($]"
    $context = Set-AzContext -SubscriptionId $

    if (!$context) {

        # break if no access
        Write-Warning "Azure Function have no access to subscription with id [$tSubscriptionId], check permissions of managed identity"

    # get vms with bootbinding tag
    $azVMs = Get-AzVM -Status -ErrorAction SilentlyContinue |  Where-Object { ($_.Tags.bootbinding -eq $bindingGroup) -and ($_.Name -ne $tVmName) }
    if ($azVMs) {
        $azVMs | ForEach-Object {
            Write-Host "VM [$($_.Name)] is in same binding-group, perform needed action "
            $vmSplatt = @{
                Name              = $_.Name
                ResourceGroupName = $_.ResourceGroupName
                NoWait            = $true
            switch ($tAction) {
                start {
                    Write-Host "Start VM"
                    $_.PowerState -ne 'VM running' ? (Start-AzVM @vmSplatt | Out-Null) : (Write-Warning "$($_.Name) is already running")
                deallocate {
                    Write-Host "Stop VM"
                    $_.PowerState -ne 'VM deallocated' ? (Stop-AzVM @vmSplatt -Force | Out-Null) : (Write-Warning "$($_.Name) is already running")
                Default {}

Setup event grid

Thankfully, we can use an “Event Grid System Topic” for our solution, so we don’t have to code anything here. You can think of a Topic as the source, where we want to react to events that occur.
Because we want to react to events in our “Lab_init” resource group, we select Resource Groups as Types and select “Lab_init” as the resource group.
If we want to trigger something, we have to create an “Event Subscription”
First, we give our Event Subscription a name and an endpoint. The endpoint defines what we want to trigger.
We dont want to call our function on every event in the dependent resource group, so we make some adjustments to filter for specific events. Otherwise, we have unnecessary function calls and have to filter the event in your function code, which is not good practice if we really don’t need to, because there is no other solution. In the Basic section, we reduce invocations to only successfully completed events.
In the Filter section of our Event Subscription we should also add some string filtering for the subject. This helps us only trigger our function if the event is triggered by the Microsoft.Compute provider on a virtual machine.

Validate Setup

Now let’s test our configuration

We start our “initVM”
In our Topic view, we see that some events are received by our Topic and also that some events are matched by our advanced filter.
Same informations four our “Event Subscription”
And we can also check our function output.

Log into our VMs

Check initVM
Check triggeredVM

As you can see, there is most likely a time difference of 3 minutes between the boottimes, so keep that in mind. In my AVD scenario, it doesn’t really matter, because we have some buffer until the user logs in and starts the application. We never had problems with that.

Hope it can be usefull for somebody, feel free to a adjust

React to MEM Logs using Event Hubs and Azure Functions

Example: Convert User-Driven provisioned Autopilot Devices to Shared Devices

I’ve got an interesting challenge from one of my customers.
Long story short, we have hybrid ad joined devices (for no really good reason, I know 😉 ), but only “User Driven Provisioning” via Windows Autopilot is available (at the moment).
But mobile devices get shared regularly for weekend tasks, the customer wants to allow every user to use the company portal on these devices.
The only way to accomplish this at the moment is to remove the primary user from the device in the MEM Admin Portal, because this will “convert” the device into a shared device.
So he wants me to automate this when a new device gets enrolled in intune via Windows Autopilot.

In the past, I’ve often used Azure Monitor with Alerts and Runbooks to perform tasks like this.
But since I’ve dug a little bit into Azure Functions in my last project, I decided to go with an alternative approach this time. (Because Azure Functions are so damn awesome and a shout-out to Laura Kokkarinen, her blog post helps me a lot: Link )

So lets start, that’s the plan:

  1. Logstreaming MEM operational logs and looking for a specific “event”.
  2. Redirect formatted output of a specific event to a new logstream
  3. The redirected event triggers Azure Functions via binding
  4. Azure Function calls Microsoft Graph with a token from the Managed Identity endpoint (that’s by far the coolest part)

Create the necessary Event Hubs

We create two Event Hubs in our new namespace, one for operational logs and one for the filtered enrollment events (we will get into that later).

Forward MEM Operational Log to Event Hub

Return to the MEM Admin Portal and configure log forwarding.

Note: Now is a good time to enroll a test device, so you have some log entries to play with.

Configure Azure Stream Analytics Job

Let’s take a look at the logstream. We navigate back to our event hub namespace and open our previously selected event hub.

We save the query as a stream analytics job.
We add output to our “Analytics Job”.
Don’t forget to start the job (unfortunately discovered after 2 hours of troubleshooting :-))
Note: Again, now is another good time to enroll a device, so we can validate if entries are received by our event hubs.

Create Azure Function and bind to Event Hub

We now create a new Azure Function App with your favorite runtime stack. I usually go with Powershell and my demo code is also written in Powershell (so if you want simple copy and paste –> select Powershell Core).
The rest of the settings are good by default, and the serverless plan is the most beaty one :-). Application Insights give us a historical view.

Next we create our first function in our new Function App and bind it with our “newenrolleddevice” event hub.
Click create and the portal brings us to our new function, where we go to the “Code + Test” section and enter the following code and click “Save”.

param($eventHubMessages, $TriggerMetadata)

# Write-Host "PowerShell event hub trigger function called for message array: $eventHubMessages"

$eventHubMessages | ForEach-Object {

    # get Intune device id
    $jsonOut = $_ | convertto-json
    Write-Host "Processing event: $jsonOut"

    $deviceID = $
    Write-Host "DeviceID: $deviceID"

    try {
        # request accesstoken from managed identity
        Write-Host "Trying to get authentication token from managed identity."
        $authToken = Receive-MyMsiGraphToken

        #Invoke REST call to Graph API
        Write-Host "Call Microsoft Graph to remove primary user from device."
        Remove-MyPrimaryUser -IntuneDeviceID $deviceID -AuthToken $authToken
    catch {
        Write-Error $_

As you might see, I use 2 helper functions in this example. “Receive-MyMsiGraphToken” and “Remove-MyPrimaryUser”, we add this function to the “profile.ps1”. The “profile.ps1” file loads every time the function does a cold start.

We append the following code to our “profile.ps1” file.
function Receive-MyMsiGraphToken {
    $Scope = ""
    $tokenAuthUri = $env:IDENTITY_ENDPOINT + "?resource=$Scope&api-version=2019-08-01"

    $splatt = @{
        Method = "Get"
        Uri = $tokenAuthUri
        UseBasicParsing = $true
        Headers = @{
    $response = Invoke-RestMethod @splatt
    $accessToken = $response.access_token

    if ($accessToken) {
        return $accessToken
    else {
        throw "Could not receive auth token for msgraph, maybe managed Identity is not enabled for this function"
function Remove-MyPrimaryUser {
    param (
    $splatt = @{
        Method = "DELETE"
        Uri = "'$IntuneDeviceID')/users/`$ref"
        UseBasicParsing = $true
        ContentType = "application/json"
        # ResponseHeadersVariable = "RES"
        Headers = @{
            'Authorization'= 'Bearer ' +  $AuthToken
    $result = (Invoke-RestMethod @splatt).value

    if ([string]::IsNullOrEmpty($result)) {
        return $true
    else {
        throw "Removing primary user from device ('$IntuneDeviceID') failed"

Add MS Graph permissions to the Azure Function App

Now we have everything in place, for our final part. We have to add some permissions to our Azure Function App.

We enable the managed identity for our function app and we copy the object ID to our clipboard because we need it in the next step.
# replace with your managed identity object ID
$miObjectID = "place your object id here"

# MS Graph app ID
$appId = "00000003-0000-0000-c000-000000000000"

# replace with the API permissions required by your app
$permissionsToAdd = @(


$app = Get-AzureADServicePrincipal -Filter "AppId eq '$appId'"

foreach ($permission in $permissionsToAdd) {
    $role = $app.AppRoles | Where-Object Value -Like $permission | Select-Object -First 1
    New-AzureADServiceAppRoleAssignment -Id $role.Id -ObjectId $miObjectID -PrincipalId $miObjectID -ResourceId $app.ObjectId

# Restart app after changing permission

Try it out

We reset and reenroll our test device/VM and take a look. After some time, we should see the message received by our “newenrolleddevice” logstream.

And some “success” messages in our function monitoring

Some final words

  • This is only an example, so please feel free to select different tiers and plans to meet your needs
  • Why redirecting into an new Event Hub? For demonstration purposes only, if you use this method in an environment with thousands of clients, you can easily reduce the number of times your function is invoked.
  • The possibilities are basically endless; tagging based on geolocation, joining groups based on properties, which are not supported at the moment, etc…

Azure Virtual Desktop and AzureAD joined VM

Since some time it is possible to join a Windows VM to Azure AD directly. Now this is also possible with Azure Virtual Desktop.

This Blogpost will show all my steps until I am possible to login to my Windows 10 System.


Create a host pool

First of all we need some basic informations such as pool name.

next to the basics we need to define: VM Size, VM Availability, Image type and the number of VMs.

General Settings

addition to that we can use an existing network or we are able to create a new one.

Network Settings

After these Settings we need to define which domain we want to join. Her we can now choose between Active Directory and Azure Active Directory.

I have chosen AzureAD.


During the host pool creation it is possible to create a assignment to a workspace. I have created tech-guy-workspace as a new one.

Roles and Permissions

With AzureAD joined devices we need to create a role assignment and a app group assignment. With each host pool one default app group will be created. In my test lab it is called “tech-guys-personal-pool-DAG”.

default app group

within this app group we are able to assign users

2nd task is to assign rbac role to at least the virtual machine to that we want to login. I prefer to assign that role to my resource group that I have that assignment for all future host as well.

there are 2 roles we need to consider about.

RBAC Roles

As it says the first role is useful when you want to login and want to have admin privileges on that machine. second group is only for your users that they are able to login without admin permission. In my lab I assigned my test user to “virtual machine user login” and my cloud only user “virtual machine administrator login” role.

To access host pool VMs, your local computer must be:

  • Azure AD-joined or hybrid Azure AD-joined to the same Azure AD tenant as the session host.
  • Running Windows 10 version 2004 or later, and also Azure AD-registered to the same Azure AD tenant as the session host.

Host pool access uses the Public Key User to User (PKU2U) protocol for authentication. To sign in to the VM, the session host and the local computer must have the PKU2U protocol enabled. For Windows 10 version 2004 or later machines, if the PKU2U protocol is disabled, enable it in the Windows registry as follows:

  1. Navigate to HKLM\SYSTEM\CurrentControlSet\Control\Lsa\pku2u.
  2. Set AllowOnlineID to 1

and here we go.

If you need to use an other client rather than the windows one, than you need enable the RDSTLS protocol. Just add a new custom RDP Property to the host pool, targetisaadjoined:i:1. Azure Virtual Desktop then uses this protocol instead of PKU2U.

Nutanix Prism Central 2021.9 Released

Short Post about the Release of Prism Central 2021.9 which is available now.

The 2021.9 release brings a few new feature, some enhancements and some bug fixes including e.g:

  • Application and relationships discovery
  • Granular Role Based Access Control (RBAC) for AHV VMs.
  • Bandwidth throttling capabilities when both creating an image and also for checkout.
  • Role Based Access Control (RBAC) for ESXi clusters
  • vCenter entities in Reporting
  • Contextual Entity Relationship Navigation

The following software is bundled with PC 2021.9

  • Nutanix Cluster Check (NCC) 4.3
  • MSP 2.3.2
  • Calm 3.3.1
  • Objects
  • Karbon 2.2.3
  • LCM 2.1.6835

Check out

Nutanix .Next 2021 – News

The .Next is every time the starting point for new product announcements and features. This time, too, there was something new. I have summarized the most important information about the announcements.

Nutanix Cloud Platform to Deliver Strengthened Data Services for Unstructured and Structured Data

Nutanix Cloud Platform Breaks Down Silos in Hybrid Multicloud Operations

Nutanix and Citrix® Team to Power Future of Work

Simple Disaster Recovery with Nutanix AOS 6

AOS 6 Advances Performance and Resilience for Business-Critical Applications

Embrace a Zero-Trust Security Model for Protection Against Ransomware and Other Threats

Nutanix and Azure—hybrid made easy

Nutanix Files and Varonis Partner to Provide Data Visibility Across Your Datacenter

Intel Optane SSDs Unleashed: How Nutanix Uses Tiering

Building an Agile Hybrid & Multi-Cloud Infrastructure with Solutions from Nutanix and Intel

HPE & Nutanix – Accelerating Business Outcomes with Leading HCI Solutions

AMD: A Proud Sponsor of the Nutanix Global. NEXT Digital Experience 2021

Deploy Azure Arc enabled Data-Services on Nutanix Karbon

While most of you already have container workloads deployed in different flavors on-premise, the ability to deploy Cloud PaaS Services into your on-prem container is a relatively new thing. With the announced support for Azure Arc enabled Data Services on Karbon it is possible to deploy Azure managed SQL instances or PostgreSQL Hyperscale Services to your Arc managed Nutanix Karbon Kubernetes Cluster.

In this post i will guide you thru the process to deploy a Karbon Cluster, register it to Azure Arc, create a Data Controller, a custom location and a PostgreSQL Instance on your on-premise infrastructure.

Create a Karbon Cluster

To create your Karbon Cluster you have to enable Karbon on your Prism Central instance. Note that a IPAM enabled Network is required. Prism need to control the Network where the Kubernetes Clusters are deployed.

Example for a Production Cluster. You can choose the Dev Option as well.
Name the Cluster and Choose the Version and Host OS Image.
Choose the Nutanix managed Network and decide how much worker and etcd Ressources you need. If you have a external Load Balancer you can use it, or go with the Active-Passive Control Plane.
I used the default values here.
Fill out the needed Data to provide Storage Services to your Cluster.
Ready deployed Cluster in the Karbon Console.

Register Karbon Cluster to Azure Arc

To link your Kubernetes Cluster to Azure you need a Subsription where you are able to deploy resources in. The Service User needs Contributor und Monitoring Metrics Publisher rights.

The Prerequisites are:

  • A new or existing Kubernetes cluster
    The cluster must use Kubernetes version 1.13 or later (including OpenShift 4.2 or later and other Kubernetes derivatives).
  • Access to ports 443 and 9418. Make sure the cluster has access to these ports, and the required outbound URLs
  • Azure CLI
  • CLI extensions. Install the latest connectedk8s and k8sconfiguration CLI extensions.
  • Helm 3
  • Kubeconfig file with cluster admin permissions (you can download the config from the actions section in the Karbon Portal)
Select the Subscription/Resource Group and choose a Cluster name
Connect to Arc Service

To Connect the Karbon Cluster to Arc you need an elevated Shell with installed Prerequisites and cluster config to connect to your K8s Cluster. You should see the following success page in Azure after Verification.

Next Step is to create a namespace on your Cluster to go through the next steps. Set a custom Namespace with: kubectl create namespace namespace-name –cluster arc-cluster-name

Next Step ist to create a Data Controller and deploy it to your Arc managed Cluster. In this Example i connect to with direct-connectivity mode. There is also a option to connect in indirect connectivity mode.

Fill out the needed Fields Data controller name and create a custom location. Select “azure-arc-kubeadm” as the Kubernetes configuration template and select “onpremise” as the Infrastructure.

To get the correct Data storage class from your Kubernets Cluster run “kubectl get storageclass” in an elevated promt. In my case i have “default-storageclass”.

At Service Type choose Node Port.

At the end we need a Service Principal to Upload usage Data and logs.

To create it use:

az ad sp create-for-rbac –name SP-Name –role Contributor –scopes /subscriptions/subscription-id/resourceGroups/ressourcegroup-name


az role assignment create –assignee SP-ID –role ‘Monitoring Metrics Publisher’ –scope /subscriptions/subscription-id/resourceGroups/ressourcegroup-name

to get the Client Secret from your Service Prinzipal use:

az ad sp credential reset –name SP-Name

The Deployment take a while till the Controller is up and in ready state, so catch a cup of coffee 😀

When youre Data Controller is Ready. You can create SQL Managed Instances or PostgresSQL Hyperscale server group. In this example i create a Postgres Instance.

This will take a few minutes. You can watch the progress with the Kibana Instance which was automatically deployed from Karbon to you Cluster. Navigate to the Cluster and under Add-On you can Launch Kibana. With LogTrail you can view and filter real time events and see what´s going on on your Cluster and deployment of your instance.

Ready Deployed Instance

As you can see, we got an External Enpoint to Connect to the instance and see the Health of the Service. The Server Group Nodes where the Server Group runs on and the Node configuration.

Next we hop to our Azure Data Studio and connect to the Data Controller to manage the Instance.

To add a Data Controller just klick Connect Controller and fill out the needed Fields Namespace, Kube Config File Path and give it a name. After Discovery you can right click the instance and manage it.

Connected Azure Data Studio

You can view your connections Strings, Worker Node Parameters or Edit Compute + Storage Settings of your Server Group, or jump to Kibana or Grafana to get insights from your Instance. Some Metrics are also available in the Azure Portal on the Metrics.

Metrics in the Azure Portal

Now you can play around like Scale up Worker Nodes, push Data to the Database or what else you like to see.

I hope this short walk thru helps a little bit to get this up an running for testing.

Cluster-based Licensing for Prism Central

With the freshly released Version pc.2021.8 of Prism Central, Nutanix integrates new Features to licence Clusters under Prism Central management.

With cluster-based-licensing you were able to choose the license level of a managed cluster. But with this update some changes apply in how you have to apply licenses in the future.

Licence Options Example

The benefits of this new way to apply licences to your managed environment is, that you can choose a different tiers of licences for each cluster under management. For example you need Ultimate Features for Cluster X, but only Pro Features for Cluster Y. For Dev or Testing Clusters you can even leave the Cluster unlicensed, but every node in a cluster must have the same licence tier.

Nutanix changed the way it handles licence features with cluster-based-licencing in this release, because if you access a feature which is within a higher licence tier, the feature is disabled and PC displays a “Feature Disabled” message. The pulled data from a managed cluster which uses widgets or reports from a higher tier feature is filtered out.

The metering Types available are capacity and nodes, based on cluster, for each node. Flow and Calm is available as Core also.

The Feature comes with the following Limitations (copied from the License Manager Guide out of the Nutanix Portal):

  • Cluster-based licensing is not available for dark site clusters or deployments where clusters are not connected to the Internet.
  • To use Prism Central cluster-based licensing, Prism Element AOS clusters registered with Prism Central must be licensed with an AOS Starter, AOS Pro, or AOS Ultimate license.
  • If you have not implemented cluster-based licensing for your managed clusters, you have access to features provided by your existing Prism Central license tier for all clusters registered to Prism Central as usual.
  • When using Prism Central cluster-based licensing, a Prism Element cluster is considered unlicensed if no cluster-based license is applied. Only Prism Central Starter features are available to manage a cluster without a cluster-based licensed applied.
  • If you have not implemented cluster-based licensing for your managed clusters when it is available in your Prism Central version, the next time you update your license from Prism Central (for example, applying a new license or consuming unused existing licenses), the Licensing page at the Nutanix Support portal will now present the cluster-based licensing work flow tasks. That is, you must now use cluster-based licensing for eligible registered clusters.

Flow Pro and Starter Option

If you crawl thru the information’s on the Nutanix portal some Screenshots show some of the upcoming add-on tiles Tiers.

about Tommy

Tommy Kneetz

Tommy is working as a Senior Cloud Consultant with more than 20 years of experience. Since 10 years his focus is on Microsoft Azure especially on Azure Governance, Azure Network, Azure Security, Azure IaaS, PaaS and my favorite Azure Virtual Desktop.

Tommy is Co-Founder and organizer of “Azure Meetup Schwerin” – a Gemany based Azure Community.