Switching from direct Microsoft 365 license assignment to group-based licensing
Follow us on our journey to revamp a decade old license assignment process that although working, was not really aligned anymore with modern practices for a company our size...
While small companies can manage licensing for their Microsoft solutions perfectly fine through their tenant's Admin center, things are slightly more complicated at scale.
With the Michelin tenant having 120 000+ seats and numerous solutions and products we are subscribed to, managing licenses, as with every Cloud-based solution, has always been a critical operational aspect for our teams.
On the financial side it is so much a thing that a new role has emerged over the years: the Finops. Today though, I am going to talk less about the financial side of license management than the technical side of it at Michelin.
What was once was...
We migrated to (what was then called) Office 365 about a decade ago, and while we were not the first to do so, it was quite unprecedented for a French company of our size at the time.
Other major French companies were seeking our feedback on the experience: they wanted to know more about how we ran the project, or how we handled user adoption for example. One thing we kept telling people was that, at scale, there were not that many things which did not require PowerShell (PS) scripts one way or another.
Licenses were one of them and circa 2015 group-based licensing was not available as a feature since it was only introduced some time in 2017. So, everything worked through direct assignment which you could basically either perform via the Admin center or PowerShell...
In a company like ours, there are always numerous events - be they IT-related or not - happening that trigger some form of licensing action: people getting hired, retiring or leaving the company, people changing positions, countries or entities, people needing access to some restricted product, service or feature, trials for new products or services going on, etc. In the end, every involved party expects things that "just" work, and it better be quick and reliable !
What we did was define so-called license "profiles" that really were license bundles. Each of them representing our user profiles, that we tried to keep under control. For example the "Knowledge Worker" (KW) profile which is one of the most common for us these days is a bundle of: "Microsoft 365 E3" + "Microsoft Defender for Endpoint P2" + "Microsoft Teams Audio Conferencing includes dial-out to USA/CAN only". Originally we had like 3 or 4 different bundles like this one, and then over time things got... more complicated. More on that later.
Anyway back in the day we decided to use some of the Extension attributes that come with the mandatory Exchange schema extension to Active Directory to store Office 365 related values. As you can see below, in our case, Extension attribute 13 is the one we use for licensing profiles. And we make use of others as well for a number of M365 related matters, as they are pretty convenient. Note for example how attribute 14 says "Mailbox" or how attribute 9 contains the creation date of the account in Azure AD (AAD). By default, these attributes are synced to AAD from on-prem Active Directory.

So we are storing values for license bundles in AD, sync them up to AAD, but how do we license users from there ? Well, 10+ years ago we did not have much choice and had to have some scheduled PS scripts running on-premises, that would periodically check for new users, read the value of O365-related attributes, and act accordingly. In our case our main licensing script would merely look for the finite set of possible values for Extension attribute 13 and assign corresponding skuid (Service Plans).
This script implemented our own business logic and acted as a mapping table between a simple to understand license profile - think of it as a persona - and the corresponding service plan assignments that were actually applied to the user object. Here is an excerpt from our old main licensing script. Note how this handled a different license profile than the one mentioned before, namely the "Light Knowledge Worker" (LKW) here.
if($LicenceType -eq "Light Knowledge Worker")
{
$goLicense = 0
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEE1.ID} -RemoveLicenses @()
WriteCSVFile ("Assing E1 licence for user : $mail")
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEEMS.ID} -RemoveLicenses @()
WriteCSVFile ("Assing EMS licence for user : $mail")
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSESP.ID} -RemoveLicenses @()
WriteCSVFile ("Assing Sharepoint plan 2 licence for user : $mail")
#Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEDLP.ID} -RemoveLicenses @()
#WriteCSVFile ("Assing Data Loss Prevention licence for user : $mail")
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEW10.ID} -RemoveLicenses @()
WriteCSVFile ("Assing Windows 10 licence for user : $mail")
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEAUDIO.ID} -RemoveLicenses @()
WriteCSVFile ("Assing LICENSE Microsoft_Teams_Audio_Conferencing for user : $mail")
Set-MgUserLicense -UserId $mail -addlicenses @{SkuId = $LICENSEMDE.ID} -RemoveLicenses @()
WriteCSVFile ("Assing LICENSE MDE for user : $mail")
$usagelocation = (get-mailbox $mail).usagelocation
$export = ""
$export =("$uid;$mail;$employetype;$Licencetype")
$export >> "E:\IN_EUX\Collaboration\Production\Others\LicenseReporting\Count-user-by-day\licence-done-$date2.csv"
}Excerpt from old licensing script showing skuid assignments for the LKW profile
I am sure that at this stage, you are wondering where the license profile came from originally, and the answer is in fact pretty simple: upstream to our Active Directory, Office 365 values are either set directly in Active Directory (previously directly into our internal LDAP directory and then synced to AD) by our Human Resources (HR) system for new employees as part of the hiring process, or through our User Provisioning Portal for all other cases: contractors, special accounts, profile changes, etc.
This would limit direct manipulation of Active Directory attributes and ensure upstream systems would only set expected, predefined values.
...is no more
While the setup described above served us well for years, it certainly was not without flaws.
- The main script had grown exponentially over the years as we needed to rework existing profiles, introduce new ones or alter the underlying business logic.
- As it was scheduled and not event-driven, there always was a built-in delay between an operation in the HR / provisioning tool, amplified by the unavoidable 30 minute sync schedule of AAD Connect, and it is translation in the real world which some found... borderline unacceptable (too slow).
- Running from on-prem meant we were susceptible to the server it was running from being down or half broken, potentially leading to failed processing and rework + user unsatisfaction, even with built-in rolling windows to account for such occurrences.
- Direct assignment also meant admins could simply assign whatever licenses to users, regardless of what they were supposed to get. This led to misalignment between what was supposed to be and what really was, complicating the license planning process as consumption was no longer in line with predictions, and is another reason, in addition to the previous point, we did have to run manual reconciliation from time to time.
For all these reasons and probably others that escape me right now, we decided to rework the existing process and make the switch to group-based licensing after all these years. As well as exclusively assign licenses that way for every product / service in the tenant from then on because... it felt right. In truth we were already using group-based licensing for a handful of specialty services such as Copilot but since we started them group-based, there never was any direct assignment migration to handle.
Regardless, on paper, the plan to address our core licenses seemed quite simple:
- We would create one dynamic group in AAD for each of our profile. For example the "Knowledge Worker" group would have a formula of (user.extensionAttribute13 -eq "Knowledge Worker").
- We would then associate (assign) the licenses corresponding to this profile to the group. Reusing the previously mentioned "Knowledge Worker" example, the group would grant the following licenses: "Microsoft 365 E3" + "Microsoft Defender for Endpoint P2" + "Microsoft Teams Audio Conferencing includes dial-out to USA/CAN only".
- We would wait for the dynamic groups to populate.
- Finally we would unassign previously directly assigned licenses on all user objects now getting their licenses through group membership (cleanup step).
We quickly added a preliminary step though as we realized we needed first to fix a number of inconsistencies (see above), then went through the 4 steps above in order until we had processed all user accounts. Save for a few special cases, mostly Cloud-only accounts not aligning with standard use cases, everything seemed to work well : upstream HR System and user provisioning tool were untouched, assignment was no longer depending on a scheduled script, and design was simple and elegant.
Except after a few days we started receiving incident tickets and discovered people were complaining about new users not getting licensed, or some license changes not being performed when it should have been fast and fully automated...
This came as a surprise: we had tested the steps above in our Indus tenant, and even on a reduced scale, in Production, and did not run into any issue. Focusing on the potentially disrupting aspects of the change in particular, we made sure the transition from directly assigned to group-based licensing would not result in any temporary loss of license, breaking in passing Power platform connections or flows for example. And indeed the switchover was working pretty well with no noticeable side effect or glitch. So what was going on ? Why were receiving all these tickets ?
Eventually investigations led to the shocking discovery that in a group associated with multiple licenses, ONE license type not available means NONE of the other licenses can be be assigned to new users. In other words shortage of a single license type would block assignment of the entire bundle... As far as I know this is undocumented. Or if it is we did not stumble upon the info. And, of course, we never thought this could be a thing simply because our (small) Indus tenant is rather static and does not see much licensing action... So this went unnoticed and never was a focus of ours in testing !
Unfortunately, as we tightly manage licenses in our tenant we more often than not are short on specific licenses, and having a large buffer for all types just is not an option. This meant we needed to rework our design as the new solution was now proving worse than the one it replaced !
After several rounds of discussions and careful considerations, we (obviously) opted to drop the "bucket" approach with one group per license profile and took the decision to push further by going fully modular and giving each combination of profile AND license type its own dynamic group. At least, that way, unavailability in one area, especially of the "smaller" option-type licenses, would not prevent the rest from being assigned... See below what we came up with.



This has now been running for a few weeks without hiccups and has resulted in better control over our core licenses which certainly pleases management with faster license assignment, easier and better reporting or reduced manual rework due to improved automation. For example there is no longer a need to manually track and reassign missing licenses when they are made available again in the tenant.
Overall this move should now allow us to work on a couple of detection scripts / groups to track rogue direct assignments from tenant admins (spanking is in order) and address the weird Cloud-only cases I mentioned before. Because after all we would not be Michelin if our general rules had no exceptions, and our exceptions, exceptions of their own !
Acknowledgements
As is often the case, this was less of a one-man army type of job and more of a team effort, so let me acknowledge here the contributions of Laurent Bros, Stephane Acknin, Raphael Charreyron and Danish Patel who were all instrumental in completing this endeavor.
Bonus - Intune rant
Totally unrelated - I will keep it short, if Workstation security is a major thing for you, then you definitely know what User Rights Assignments are, and what you should do with them. When pushing these from Intune, ALWAYS put a * (star character) in front of the SID you are targeting.
Documentation is at best ambiguous about this, and BAD things will happen to you if you do not prefix with *. We learned this the HARD way when we ended up with a messed up policy that wiped everything and disgruntled, locked out users flooding our Service Desk. You have been warned.
Note: Mr T-Bone has a nice blog article about it, unfortunately for us it came up two months too late. And as we can attest, symptoms can be way more severe than the policy "fail[ing] or throw[ing] cryptic errors"...