AVD Deployment – Azure AD Join BICEP

UPDATE 04/07/2022 – Intune Enrolment

I was curious about how the portal-driven deployment auto enrols in Intune, as I could not find any way of doing the enrolment via BICEP/ARM.

So I went and started a deployment from the portal, then had a dig through the ARM template. I could see there was an Intune parameter to decide whether to enrol in Intune or not.

I dug through and found the nested template is used for the VM deployment. Under the AADLoginWithWindows extension, I found an extra config line

"settings": "[if(parameters('intune'), createObject('mdmId','0000000a-0000-0000-c000-000000000000'), json('null'))]"

This added a mdmId setting that is set if the intune parameter is true.

I added an intune parameter to my build and then set a piece of BICEP logic as below:

  properties: AADJoin ? {
    publisher: 'Microsoft.Azure.ActiveDirectory'
    type: 'AADLoginForWindows'
    typeHandlerVersion: '1.0'
    autoUpgradeMinorVersion: true
    settings: intune ? {
      mdmId: '0000000a-0000-0000-c000-000000000000'
    } : null
  } : {
    publisher: 'Microsoft.Compute'
    type: 'JsonADDomainExtension'

I have now added this to the extension scripting below and have an AAD joined, Intune-managed machine!


Introduction

Since I wrote my original blogs on AVD deployment there have been a number of changes in the AVD space.

One significant change is the ability to have pure Azure AD joined Session Hosts.

As of writing, this feature is still in preview but I thought I’d share how I have added this capability to my deployment scripting.

Be aware that the scripting will be in BICEP language. However in my repo I also include the transpiled JSON file if you want to view as a standard ARM template a

Full code for all my deployments can be found on my Github page:

https://github.com/jamesatighe/AVD-BICEP/tree/main/updatedJUN2022


Warning

Before we get going, I will again remind you that this is a preview feature and should not be used in a live production environment yet. This was a mistake as it has actually been GA for a while now . . .

Also, for anyone looking to use AAD joined Session Hosts, you need to be aware of a few limitations.

  • AAD Join is not supported for AVD Classic
  • AAD Joined VMs don’t currently support external identities, such as Azure AD Business-to-Business (B2B) and Azure AD Business-to-Consumer (B2C).
  • AAD Joined Session Hosts can only access Azure File shares using a Hybrid AD synced account.
  • No Windows Store client support.
  • Azure Virtual Desktop doesn’t currently support single sign-on for AAD Joined Session Hosts.

Configuring

Right with the limitations out of the way let’s get testing.

So with AAD Join, there are a couple of pre-requisites that are needed

  • VM must have a System Managed Identity – This is used to join the Azure AD Domain.
  • There must be no restrictions that would stop the AAD join from completing (MDM filtering etc)
  • The VNET the Session Hosts are being deployed to must be using Azure DNS to resolve the AAD domain.

Adding System Managed Identity to VM

In order for the Session Host VMs to be able to join the AAD domain, we need to ensure there is a System Managed Identity.

This is a special type of identity that is linked to the lifecycle of the resource, and therefore does not need to be manually administered.

This identity will have the required permissions to join the AAD domain.

I have added a simple bool parameter called AADJoin to the scripting. This is passed through to the VM module.

This parameter is then used in a ternary operation (think if statement) to add the System Managed identity section to the VM resource if set to true.

resource vm 'Microsoft.Compute/virtualMachines@2021-11-01' = [for i in range(0, AVDnumberOfInstances): {
  name: '${vmPrefix}-${i + currentInstances}'
  location: location
  identity: AADJoin ? {
    type: 'SystemAssigned'
  } : null
  properties: {
    licenseType: 'Windows_Client'
    hardwareProfile: {
      vmSize: vmSize
    }
    availabilitySet: {
      id: resourceId('Microsoft.Compute/availabilitySets', '${vmPrefix}-AV')
    }
    osProfile: {
      computerName: '${vmPrefix}-${i + currentInstances}'
      adminUsername: existingDomainUserName
      adminPassword: administratorAccountPassword
      windowsConfiguration: {
        enableAutomaticUpdates: false
        patchSettings: {
          patchMode: 'Manual'
        }
      }
    }
    storageProfile: {
      osDisk: {
        name: '${vmPrefix}-${i + currentInstances}-OS'
        managedDisk: {
          storageAccountType: ephemeral ? 'Standard_LRS' : vmDiskType
        }
        osType: 'Windows'
        createOption: 'FromImage'
        caching: 'ReadOnly'
        diffDiskSettings: ephemeral ? {
          option: 'Local'
          placement: 'CacheDisk'
        } : null
      }
      imageReference: {
        //id: resourceId(sharedImageGalleryResourceGroup, 'Microsoft.Compute/galleries/images/versions', sharedImageGalleryName, sharedImageGalleryDefinitionname, sharedImageGalleryVersionName)
        id: '/subscriptions/${sharedImageGallerySubscription}/resourceGroups/${sharedImageGalleryResourceGroup}/providers/Microsoft.Compute/galleries/${sharedImageGalleryName}/images/${sharedImageGalleryDefinitionname}/versions/${sharedImageGalleryVersionName}'
      }
      dataDisks: []
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: resourceId('Microsoft.Network/networkInterfaces', '${vmPrefix}-${i + currentInstances}${networkAdapterPostfix}')
        }
      ]
    }
  }
  tags: tagParams
  dependsOn: [
    availabilitySet
    nic[i]
  ]
}]

Check MDM Device Join Restrictions

We also need to make sure that we have not configured any settings that may stop the AAD join from completing.

To do this check the MDM user scope under Devices > Windows is set to All

Amend VM Domain Join Extension

There is one major difference with the deployment if we want to go down the AAD Join route.

Normally when joining a standard Active Directory domain we use the Microsoft.Computer/JsonADDomain extension to do the join.

This extension takes admin credentials, a domain and optionally an OU to place the joined computer object.

    resource joindomain 'Microsoft.Compute/virtualMachines/extensions@2021-11-01' = [for i in range(0, AVDnumberOfInstances): {
  name: '${vmPrefix}-${i + currentInstances}/joindomain'
  location: location
  properties: {
    publisher: 'Microsoft.Compute'
    type: 'JsonADDomainExtension'
    typeHandlerVersion: '1.3'
    autoUpgradeMinorVersion: true
    settings: {
      name: domainToJoin
      ouPath: ouPath
      user: administratorAccountUserName
      restart: 'true'
      options: '3'
      NumberOfRetries: '4'
      RetryIntervalInMilliseconds: '30000'
    }
    protectedSettings: {
      password: administratorAccountPassword
    }
  }
  dependsOn: [
    vm[i]
    languagefix[i]
  ]
}]

Now with AAD Join we instead use a separate extension Microsoft.Azure.ActiveDirectory/AADLoginWithWindows

resource joindomain 'Microsoft.Compute/virtualMachines/extensions@2021-11-01' = [for i in range(0, AVDnumberOfInstances): {
  name: '${vmPrefix}-${i + currentInstances}/joindomain'
  location: location
  properties: AADJoin ? {
    publisher: 'Microsoft.Azure.ActiveDirectory'
    type: 'AADLoginForWindows'
    typeHandlerVersion: '1.0'
    autoUpgradeMinorVersion: true
    settings: intune ? {
      mdmId: '0000000a-0000-0000-c000-000000000000' //added to allow Intune join
    } : null //
  }
  dependsOn: [
    vm[i]
    languagefix[i]
  ]
}]

You’ll immediately see there is no domain or credentials noted in the extension. This is because the domain join will be performed using the System Managed Identity we added to the VM resource.

As an FYI when I originally attempted this I used typeHandlerVersion: ‘0.4’ and had a few issues with the extension failing. I switched this to ‘1.0’ and this was resolved.

Much like with the System Managed Identity I will again use a ternary operator to allow us to use the relevant domain join extension based on the AADJoin parameter. The full extension resource is below:

resource joindomain 'Microsoft.Compute/virtualMachines/extensions@2021-11-01' = [for i in range(0, AVDnumberOfInstances): {
  name: '${vmPrefix}-${i + currentInstances}/joindomain'
  location: location
  properties: AADJoin ? {
    publisher: 'Microsoft.Azure.ActiveDirectory'
    type: 'AADLoginForWindows'
    typeHandlerVersion: '1.0'
    autoUpgradeMinorVersion: true
    settings: {
      mdmId: '0000000a-0000-0000-c000-000000000000'
    }
  } : {
    publisher: 'Microsoft.Compute'
    type: 'JsonADDomainExtension'
    typeHandlerVersion: '1.3'
    autoUpgradeMinorVersion: true
    settings: {
      name: domainToJoin
      ouPath: ouPath
      user: administratorAccountUserName
      restart: 'true'
      options: '3'
      NumberOfRetries: '4'
      RetryIntervalInMilliseconds: '30000'
    }
    protectedSettings: {
      password: administratorAccountPassword
    }
  }
  dependsOn: [
    vm[i]
    languagefix[i]
  ]
}]

The ternary operator works as condition ? if true : if false

So in our case, if AADJoin is true it will create extension properties matching the AADLoginWithWindows extension, if false it will create with the extension with JsonADDomainExtension properties.

RBAC Roles

Something that we have to be aware of is how to access the AVD environment when the Session Hosts are AAD joined.

The users will need suitable RBAC roles assigned to be allow access to login. This is provided by the following 2 roles

  • Virtual Machine User Login – For standard AVD users
  • Virtual Machine Administrator Login – For VM local administrators

There are a number of ways to grant access. It can be done via the Azure portal, via PowerShell or Azure CLI.

Below is an example of setting the RBAC roles via Azure CLI.

#Get the resource group id that you wish to scope the role assignment to
$resourceGroup = $(az group show --resource-group "rg-test-avd-vms" --query id -o tsv)
az role assignment create \
--role "Virtual Machine User Login"
--assignee "<userprincipalname>
--scope $rg

Connecting from Non-AAD Joined Devices

It is important to note that you will only be able to connect to AAD Joined Session Hosts from devices that are either joined or registered to the same AAD domain.

If you attempt to connect from a non-AAD joined device you will not be able to connect. For instance, from my iPhone RD client, I get the below error.

There is however an easy way to fix this. We need to add the following setting to the RDP Properties for the Host Pool.

targetisaadjoined:i:1

With this added the connection will succeed.

Update DevOps Release

Now I have configured the various settings and amended my deployment scripts I need to update my DevOps release settings

I need to add the AADJoin parameter to my AVD Selectable Variables variable group.

I then need to update the release task settings to use this new variable during deployment.

I need to add -AADJoin “$(AADJoin)” to the Override Template Parameters for the main AVD Deployment task.


Deployment Time

Finally, it’s time to test the deployment and see AAD join in action!

Let’s set off a deployment from Azure DevOps.

Once this deployment is complete we should see that the machine has indeed joined our Azure AD as shown in the Azure Active Directory > Devices tab.

Also as long as we have set the RBAC roles correctly (and RDP Properties if needed) we should be able to login to the AVD environment.

If we run dsregcmd /status we should see the Session Host is indeed AzureAdJoined

And there we have it. AVD Session Hosts running on AAD without the need for any actual Active Directory servers or Azure AD DS!

You will need to be aware that if you host your FSLogix containers on an Azure File share then you will only be able to access them using a hybrid synched user account.

Also, the machine must have connectivity to the required domain for Kerberos authentication to work.

Furthermore not being on an actual Active Directory domain you will not have access to GPOs for settings.

So you will ideally need to enrol the Session Hosts into Endpoint Manager to be able to manage them. The update at the top of the blog explains how this is done.

One thought on “AVD Deployment – Azure AD Join BICEP

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s