How can I build out a series of progressively larger rings for deployment?
(Oh yeah, by the way I’m using Intune.)
About a week ago Mike Terrill was talking to me about a blog that he was working on. He was calling it “Lord of the Deployment Rings”. The focus was on building out a series of collections in Configuration Manager that could be used for “ramp up” deployments, deployments that start with a small group and gradually expand to larger and larger groups.
You can find it here.
I asked him if he wouldn’t mind if I were to piggyback on the concept but for use with Intune. Oh, little did I know what I was getting myself into.
I won’t repeat all of the details found in Mike’s blog. I just tried to tailor my Intune solution to match his ConfigMg solution as best as possible. There are some things I cannot do with Intune, but I’ll do my best to point those out and offer up alternatives.
One Ring to Rule them All…
One SET of rings to rule them all actually. The goal was to create a set of groups that could be universally used for a consistent deployment experience that “ramps up” and follows the “crawl/walk/run” approach. These rings could be used for any deployment, and in the case of Intune any assignments whether they are applications, profiles, etc.
All told there are 9 groups (aka ConfigMgr collections):
- Enterprise High Risk Ring (Assigned Membership)
- Enterprise Ring 0 – Pilot (Assigned Membership)
- Crawl
- Enterprise Ring 1 (~2%)
- Enterprise Ring 2 (~3%)
- Enterprise Ring 3 (~5%)
- Walk
- Enterprise Ring 4 (~10%)
- Enterprise Ring 5 (~20%)
- Run
- Enterprise Ring 6 (~30%)
- Enterprise Ring 7 (~30%)
Chapter 1: In Search of the Runes
The first step on my grand adventure was to figure out how on earth was I going to query for the devices? I wanted to match the intent of Mike’s blog, so I started looking at the DeviceID since it too was made up of hexadecimal characters just like the SMSUniqueIdentifier from ConfigMgr that Mike was using.
From there, things started getting difficult.
First stumble was that there isn’t a SubString option in the Intune query world. So, extracting the last 2 characters was out of the question. There is a StartsWith operator available with Intune. That should do the trick! It will compare the beginning of the DeviceID with whatever string I want.
StartsWith Example:
(device.deviceId -startsWith "2D")
So, I can at least compare the beginning of the DeviceID with a string value. Now, how can I compare it to an array of values? I next looked at the In operator that is available with Intune. It will compare an item with an array of values.
In Example:
user.department -in ["50001","50002","50003","50005","50006","50007","50008"]
This is looking promising! Can I combine the two operators with something like this?
(device.deviceId -startsWith -in ['43','CC','C4','5B','30'])
No. You cannot.
This then lead to all sorts of searching and asking and eventually I sought out the wisdom of wizards at Microsoft. They came back with 2 options. Option 1 would be some sort of RegEx query while option 2 would be individual StartsWith queries for each targeted option. This lead to my first query for Ring 1 (~2% of the fleet):
(device.deviceId -startsWith "43") or (device.deviceId -startsWith "CC") or (device.deviceId -startsWith "C4") or (device.deviceId -startsWith "5B") or (device.deviceId -startsWith "30")
Not ideal but I can work with it. Besides, I’m going to script creating all of this. But that’s a “future me” problem, what could go wrong?
I noticed something when I tested out the query. It was pulling in Android, iOS and Mac devices in addition to the Windows devices I was looking for.
Here you have 2 options and which path you choose depends on your environment and goals.
Option 1: Add OS Check to Query
One option would be to include a test for the OS as part of the query. The downside is that it extends the size of the query, and it also limits these rings to only Windows devices. If you wish to journey down this path your Ring 1 query will then be something like this:
(device.deviceOSType -eq "Windows") and ((device.deviceId -startsWith "43") or (device.deviceId -startsWith "CC") or (device.deviceId -startsWith "C4") or (device.deviceId -startsWith "5B") or (device.deviceId -startsWith "30"))
Pay close attention and note the extra parenthesis above enclosing all of the StartsWith expressions. If you do not include those you will end up with some very unintended results.
Option 2: Filters
Another option would be to use a Filter to control what OS your object (applications, profile, etc.) will apply to. This route would then allow you to leverage these rings for more than just Windows, you could use them for any platform you are managing with Intune. Personally, this is the route I would choose.
Now, I’ve mentioned a couple of times that the size of the query grows rapidly. Why is that? Well the query for Ring 7 comes in right around 3000 characters. That does not leave very much wiggle room before hitting the 3072 character limit. This also means that you cannot, using this particular method, create a ring that will hold more than about 30% of your environment. There just isn’t enough room to add more StartsWith queries to pull in more devices. The query to add another roughly 10% would add almost 1000 characters. Adding just another 5% would increase the query by nearly 500 more characters.
Chapter 2: Casting the Spell
With the queries sorted out the next challenge was how to script this. How hard can that be, right?
Again, we have some options (I like options). Not all environments allow PowerShell access to their tenant. For those environments I have a version of the script that will simply output the queries to the console and you can cut/paste them into the groups that you create manually.
NOTE: Be careful of the word-wrapping of the output. To be safe, paste it into Notepad and ensure that the word-wrapping has not added hard carriage returns and broken up the text.
Script 1: No PowerShell access to your tenant
#Lord of the Deployment Rings
#==============================================================================
function Generate-Query
{
param($array)
$n = 0
$query = $NULL
FOREACH ($a in $array)
{
IF ($n -ne 0)
{
$query = $query + " or "
}
$query = $query + "(device.deviceId -startsWith ""$a"")"
$n++
}
Return $query
}
$arrRing1 = @('43','CC','C4','5B','30')
$arrRing2 = @('6C','AA','55','C9','72','BD','54')
$arrRing3 = @('9E','F1','12','8C','34','FC','ED','77','87','D1','48','57','5A')
$arrRing4 = @('0F','74','0B','2D','59','AE','27','DD','99','A9','4F','FB','BB','1B','66','C3','52','AC','85','84','B9','A8','26','8F','BC')
$arrRing5 = @('21','63','9A','3A','D2','36','AF','E3','5C','AD','B5','25','3D','88','DF','D5','DE','6E','15','7B','09','FE','B8','3F','CA','0A','95','0D','EE','33','97','A7','3C','D0','5D','E4','9C','1F','4C','1C','18','49','4E','3E','AB','89','D4','8D','C6','0C','53')
$arrRing6 = @('DA','CB','67','11','40','BF','16','7F','D3','6D','08','50','7C','1A','14','94','E6','60','3B','38','7D','7E','98','F8','E9','37','E5','FF','A3','B3','10','90','81','1E','4B','51','DB','8E','35','F4','47','CD','A5','00','5E','19','4D','69','92','75','06','CF','31','F0','E1','93','03','45','1D','5F','E8','91','F2','CE','B1','73','D7','22','82','76','71','4A','86','EC','B7','80','F3')
$arrRing7 = @('C5','A4','46','79','D8','BA','C0','A1','58','BE','68','78','29','02','E2','39','05','F5','E7','D9','28','24','6F','9B','8B','20','83','70','B4','61','A0','6A','96','23','2F','A6','04','DC','13','F6','0E','6B','01','E0','65','62','9D','2E','44','F7','C8','B0','FA','8A','C2','F9','2A','C1','D6','B2','41','EA','EF','FD','A2','17','7A','56','B6','2B','64','9F','42','EB','C7','07','2C','32')
#Define Enterprise Ring Collections $Rings = @(
@{ GroupName = 'Enterprise Ring 1'; Comment = 'Approximately 2%' ; Query = $arrRing1}
@{ GroupName = 'Enterprise Ring 2'; Comment = 'Approximately 3%' ; Query = $arrRing2}
@{ GroupName = 'Enterprise Ring 3'; Comment = 'Approximately 4%' ; Query = $arrRing3}
@{ GroupName = 'Enterprise Ring 4'; Comment = 'Approximately 10%'; Query = $arrRing4}
@{ GroupName = 'Enterprise Ring 5'; Comment = 'Approximately 20%'; Query = $arrRing5}
@{ GroupName = 'Enterprise Ring 6'; Comment = 'Approximately 30%'; Query = $arrRing6}
@{ GroupName = 'Enterprise Ring 7'; Comment = 'Approximately 30%'; Query = $arrRing7}
)
#Create Enterprise Ring Collections
foreach ($Ring in $Rings)
{
$GroupName = $Ring.GroupName
$Comment = $Ring.Comment
$IntuneGroupQuery = Generate-Query -array $($Ring.Query)
Write-Host "Creating Group $GroupName"
Write-Host "Description: $Comment"
Write-Host $IntuneGroupQuery
Write-Host " "
}
If you have PowerShell access to your tenant, then there is a catch. The “production” AzureAD PowerShell module does not include code to create dynamic groups. You will need to use the “preview” AzureADPreview module instead. I have a second version of the script that includes the commands to connect to your tenant and create the groups and queries for you.
Script 2: PowerShell access to your tenant
#Lord of the Deployment Rings
#==============================================================================
function Generate-Query
{
param($array)
$n = 0
$query = $NULL
FOREACH ($a in $array)
{
IF ($n -ne 0)
{
$query = $query + " or "
}
$query = $query + "(device.deviceId -startsWith ""$a"")"
$n++
}
Return $query
}
$arrRing1 = @('43','CC','C4','5B','30')
$arrRing2 = @('6C','AA','55','C9','72','BD','54')
$arrRing3 = @('9E','F1','12','8C','34','FC','ED','77','87','D1','48','57','5A')
$arrRing4 = @('0F','74','0B','2D','59','AE','27','DD','99','A9','4F','FB','BB','1B','66','C3','52','AC','85','84','B9','A8','26','8F','BC')
$arrRing5 = @('21','63','9A','3A','D2','36','AF','E3','5C','AD','B5','25','3D','88','DF','D5','DE','6E','15','7B','09','FE','B8','3F','CA','0A','95','0D','EE','33','97','A7','3C','D0','5D','E4','9C','1F','4C','1C','18','49','4E','3E','AB','89','D4','8D','C6','0C','53')
$arrRing6 = @('DA','CB','67','11','40','BF','16','7F','D3','6D','08','50','7C','1A','14','94','E6','60','3B','38','7D','7E','98','F8','E9','37','E5','FF','A3','B3','10','90','81','1E','4B','51','DB','8E','35','F4','47','CD','A5','00','5E','19','4D','69','92','75','06','CF','31','F0','E1','93','03','45','1D','5F','E8','91','F2','CE','B1','73','D7','22','82','76','71','4A','86','EC','B7','80','F3')
$arrRing7 = @('C5','A4','46','79','D8','BA','C0','A1','58','BE','68','78','29','02','E2','39','05','F5','E7','D9','28','24','6F','9B','8B','20','83','70','B4','61','A0','6A','96','23','2F','A6','04','DC','13','F6','0E','6B','01','E0','65','62','9D','2E','44','F7','C8','B0','FA','8A','C2','F9','2A','C1','D6','B2','41','EA','EF','FD','A2','17','7A','56','B6','2B','64','9F','42','EB','C7','07','2C','32')
# Import the preview AzureAD module
Install-Module AzureADPreview
# Connect to your Azure Tenant
Connect-AzureAD
#Create Enterprise High Risk Group
New-AzureADGroup -DisplayName "Enterprise High Risk Ring" -MailEnabled $False -MailNickname "Junk" -Description 'Place high-risk systems in this collection' -SecurityEnabled $True
#Create Enterprise Ring 0 Pilot Group
New-AzureADGroup -DisplayName "Enterprise Ring 0 Pilot" -MailEnabled $False -MailNickname "Junk" -Description 'Place pilot systems in this collection' -SecurityEnabled $True
#Define Enterprise Ring Groups
$Rings = @(
@{ GroupName = 'Enterprise Ring 1'; Comment = 'Approximately 2%' ; Query = $arrRing1}
@{ GroupName = 'Enterprise Ring 2'; Comment = 'Approximately 3%' ; Query = $arrRing2}
@{ GroupName = 'Enterprise Ring 3'; Comment = 'Approximately 4%' ; Query = $arrRing3}
@{ GroupName = 'Enterprise Ring 4'; Comment = 'Approximately 10%'; Query = $arrRing4}
@{ GroupName = 'Enterprise Ring 5'; Comment = 'Approximately 20%'; Query = $arrRing5}
@{ GroupName = 'Enterprise Ring 6'; Comment = 'Approximately 30%'; Query = $arrRing6}
@{ GroupName = 'Enterprise Ring 7'; Comment = 'Approximately 30%'; Query = $arrRing7}
)
#Create Enterprise Ring Collections
foreach ($Ring in $Rings)
{
$GroupName = $Ring.GroupName
$Comment = $Ring.Comment
$IntuneGroupQuery = Generate-Query -array $($Ring.Query)
Write-Host "Creating Group $GroupName"
Write-Host "Description: $Comment"
Write-Host " "
# Create Dynamic Azure Active Directory Group
New-AzureADMSGroup `
-Description "$($Comment)" `
-DisplayName "$($GroupName)" `
-MailEnabled $false `
-SecurityEnabled $true `
-MailNickname "IntuneDevices" `
-GroupTypes "DynamicMembership" `
-MembershipRule "$($IntuneGroupQuery)" `
-MembershipRuleProcessingState "On"
}
Chapter 3: Embarking on the Journey
After setting up these groups, how can they be used?
The most straight-forward way would be to deploy directly to these groups. That though could get cumbersome, again the intent was to create re-usable groups that may or may not be OS specific. If you just keep pilling assignments onto these groups they’ll get a bit messy.
One way would be to create a group and target your assignment there (also include the filtering). Then make each of these ring groups members as you ramp up the rollout.
The advantage with this method is that, should you not include the OS in the query and instead rely on filters you only need to add the filter once to the deployment group and not to several individual assignments.
Another alternative (and kudos to Adam Gross for this idea), would be to target the assignment to “All Devices” (apply filtering there) and add all of the ring groups as exclusions to the assignment. Then as you remove each excluded group those devices then receive the assignment. Remove Ring 1 and you are deploying to roughly 2% of your fleet. Let that settle for a period of time, then remove Ring 2 to add another 3% receiving the assignment.
The advantage again is that you only need to worry about the filter the one time. Another advantage is that when all of the Enterprise Rings have been removed you are left with a finished rollout, meaning the assignment can stand as-is in perpetuity.
Bonus Content
Now, I have not talked about the pilot and “high risk” groups. Both of these groups are static assigned groups and do not have queries associated with them.
How do we go about handling them?
Enterprise High Risk Ring
The “Enterprise High Risk Ring” would be your critical devices that need special “hand holding” for deployments. Working in a hospital these would be devices in the operating rooms and the devices in the emergency department. Devices where we cannot have rebooting unexpectedly or otherwise unavailable.
A device that is a member of the “high risk” group will also be a member of one of the other ring groups. This is okay because of how Intune handles assignment conflicts. In this case the exclude would win.
In Mike’s solution, he had this collection excluded from each of the 7 “ring” collections. Unfortunately, you cannot do the equivalent with Intune. You can make one group a member of another, but you cannot exclude the members of one group from the other. What you will need to do is to set this “Enterprise High Risk Ring” group as an exclude on your application’s required assignment and keep it there.
Enterprise Ring 0 Pilot
Now the “Enterprise Ring 0 Pilot” will be (of course) your pilot devices. Members of this group are assigned and not populated by any query. If you have a standing set of pilot devices that are used universally then they would go here.
Assigning an application is simple. If for example, I were deploying 7Zip, I would create a group specific for that application (i.e. “7Zip Pilot Testing”) assign the application to that, add a filter to apply only to Windows devices, and finally make the pilot group a member. After pilot testing is complete you can move to assigning the application using one of the methods discussed above.
Epilogue: Some things to keep in mind
- First, remember the 3072 character limit for group queries. That Ring 7 query will come in around the 3000 mark. Because of this, attempting to get more than 30% is difficult.
- If you use 1 character from the DeviceID, since it is hexadecimal you will have 16 possible options. That works out to 6.25% each. If you use 2 characters that is 256 possible options, or roughly 0.4% each combo.
- Somewhere between 1000 and 1900 characters the ability to validate the query breaks.
- I’ve seen odd “blurring” when attempting to edit queries of 1900 characters or more. The top image is the original query. The image below that is what happened once I clicked anywhere inside the Rule syntax box. I have only seen this when using Google Chrome. The problem does not appear when using Edge.
Closing Credits: References
Rules for dynamically populated groups membership – Azure AD | Microsoft Docs
Create simpler and faster rules for dynamic groups – Azure AD | Microsoft Docs
Include and exclude app assignments in Microsoft Intune | Microsoft Docs