Azure AD Connect – Automatic Synchronization on User Change

Automatic Azure AD Connect Synchronization
(Script download link and setup info at the end of the post)
 

As described in a separate post, Azure AD Connect synchronizes Active Directory changes to Azure every 30 minutes by default. This means any on-premises user changes (except password changes) may take up to 30 minutes before they are visible in Azure/Office 365.

To most admins, this also means A LOT of manual synchronizations of Azure AD Connect. Neither you nor your user wants to wait up to half an hour before testing that new mail alias you just configured on the on-prem user object…

So what to do about it?

Can’t we just set Azure AD Connect to synchronize every minute? Unfortunately not… The most frequent schedule allowed is the default 30 minutes as set by the AllowedSyncCycleInterval attribute of the ADSyncScheduler. This is configured per design and can’t be set administratively

PS C:\Windows\system32> Get-ADSyncScheduler
AllowedSyncCycleInterval            : 00:30:00
CurrentlyEffectiveSyncCycleInterval : 00:30:00
CustomizedSyncCycleInterval         :
NextSyncCyclePolicyType             : Delta
NextSyncCycleStartTimeInUTC         : 5/3/2019 9:09:31 AM
PurgeRunHistoryInterval             : 7.00:00:00
SyncCycleEnabled                    : True
MaintenanceEnabled                  : True
StagingModeEnabled                  : False
SchedulerSuspended                  : False
SyncCycleInProgress                 : False

Now what??? Wouldn’t it be nice if Azure AD Connect just decided to do an automatic synchronization every time we created or updated a user object? Sure, why not!? Let’s go…

The “Automatically Synchronize Azure AD Connect on Every On-Premises User Object Change”-PowerShell Script!

Active Directory LDAP queries are pretty fast as long as you limit the complexity of the query, don’t include too many non-indexed attributes in your query, and don’t retrieve too many attributes. In most scenarios, you should be able to run a user search that filters on the whenChanged attribute in less than a second. That is unless your DC’s are already pressed for resources and/or if your user count is well above 5,000.

To test your DC performance try to time the below code on your system (it will get basic attributes for all users that were changed within the last week):

Import-Module ActiveDirectory
$OneWeekAgo = (Get-Date) + (New-TimeSpan -Days -7)
[array]$Users = Get-ADUser -Filter 'whenChanged -gt $OneWeekAgo'

If the query (line 3) took more than a few seconds to complete, investigate why. If your DC’s don’t process the query effectively, you should not use a schedule of 15 seconds as suggested in this article.

But if the above code is executed in around one second or less, I would say it’s reasonable to run it every 15 seconds against one of your Domain Controllers.

Timing and Control of the Script Using a Log File

Having confirmed the low impact of the whenChanged query, let’s try and structure it. We need the following:

  1. Run a query to see if any users were changed since the last time we ran the query
  2. If users were changed, trigger the Azure AD Connect synchronization
  3. When done, wait for 15 seconds before starting over

There are many methods to control this flow. I prefer to use a log file as it’s relatively simple to handle and can give us numerous benefits such as execution history and troubleshooting info.

The log file should perform the following:

Write the time we made a query (so we know how far back to look for user changes next time we run the script):

$Now = Get-Date
$Now.ToString("yyyyMMddHHmmss") + " Some status info." | Out-File $LogFile -Append

Read the last time we made a query and check for users and groups that were changed since then:

$LastLine = Get-Item -Path $LogFile | Get-Content -Tail 1       # Read last line of log file
$LastSync = [datetime]::parseexact($LastLine.Substring(0, 14), 'yyyyMMddHHmmss', $null) # Get last synchronization time
[array]$Users = Get-ADUser -Filter ‘whenChanged -gt $LastSync’  # Find users that changed since last time
[array]$Groups = Get-ADGroup -Filter 'whenChanged -gt $LastSync'  # Find groups that changed since last time
In case any users were changed we need to initiate the Azure AD Connect synchronization:
$ChangeCount = $Users.Count + $Groups.Count
If ($ChangeCount -gt 0) {
  Import-Module ADSync
  Start-ADSyncSyncCycle -PolicyType Delta
}
We should also handle cases where the log file doesn’t exist or if it has grown too large and needs to be purged:
If ((Test-Path $LogFile) -eq $False){  # Create log file if it doesn't exist
    "Time           Message" | Out-File $LogFile
    "19990101000001 SyncAllChanges" | Out-File $LogFile -Append
}
$LastLine = Get-Item -Path $LogFile | Get-Content -Tail 1  # Read last line of log file
If ((Get-Item $LogFile).Length -gt $MaxSize){	           # Reset log file if it has grown larger than $MaxSize
    "Time           Message" | Out-File $LogFile
    $LastLine | Out-File $LogFile -Append
}
And we should make sure to trigger a synchronization on the next run in case something goes wrong:
Try {
  # Main code goes here
}
Catch {
  "19990101000001 " + $ErrorMessage | Out-File $LogFile -Append
}

(Setting last synchronization time to 1st of January 1999 will ensure the number of changed users on next run is larger than 0 and hence will trigger an Azure AD Connect delta sync.)

Wrapping up the Azure AD Connect Synchronization Script

With all the bits and pieces defined, we’re pretty much ready to schedule it. Regarding scheduling, some admins prefer to centralize their scheduled tasks on a dedicated batch server to keep track of automation. To support this, we need to implement remoting which requires a bit of tweaking to perform proper error handling.

Below you’ll find the final script in its entirety. 

# Azure AD Connect Auto Sync script - Provided as-is, use at own risk.
# Make sure ActiveDirectory module is available on system where script is scheduled.
# Will trigger Azure AD Connect synchronization when on-prem AD user or group is changed.
# Schedule to run every 5 minutes and to start after scheduled start is missed.
# For full info on script and how to schedule it:
# https://www.easy365manager.com/azure-ad-connect-automatic-synchronization-on-user-change
# Configure parameters before scheduling the script:
$AADConnectServer = ""               # Azure AD Connect server (blank assumes local system)
$LogFile = "AD_Azure_Sync_Log.txt"   # Log file used for keeping track of AD changes and troubleshooting
$MaxSize = 5242880                   # Maximum size of log file in bytes (5MB)
$StartTime = Get-Date
While ((New-TimeSpan $StartTime (Get-Date)).Seconds -lt 300) {
  Try {
    Import-Module ActiveDirectory
    If ((Test-Path $LogFile) -eq $False) {
      # Create log file if it doesn't exist
      "Time           Message" | Out-File $LogFile
      "19990101000001 SyncAllChanges" | Out-File $LogFile -Append
    }
    $LastLine = Get-Item -Path $LogFile | Get-Content -Tail 1  # Read last line of log file
    If ((Get-Item $LogFile).Length -gt $MaxSize) {
      # Reset log file if it has grown larger than $MaxSize
      "Time           Message" | Out-File $LogFile
      $LastLine | Out-File $LogFile -Append
    }
    $Now = Get-Date
    $LastSync = [datetime]::parseexact($LastLine.Substring(0, 14), 'yyyyMMddHHmmss', $null) # Get last synchronization time
    [array]$Users = Get-ADUser -Filter 'whenChanged -gt $LastSync'  # Find users that changed since last time
    [array]$Groups = Get-ADGroup -Filter 'whenChanged -gt $LastSync'  # Find groups that changed since last time
    $ChangeCount = $Users.Count + $Groups.Count
    If ($ChangeCount -gt 0) {
      # If any users or groups were changed start the Azure AD Synchronization
      If ($AADConnectServer -ne "" -and $AADConnectServer.ToLower() -ne $env:COMPUTERNAME.ToLower()) {
        # Connect to Azure AD Connect server if not running on local system
        $Session = New-PSSession $AADConnectServer
        $Result = Invoke-Command -Session $Session -Scriptblock {
          Try {
            Import-Module ADSync
            Start-ADSyncSyncCycle -PolicyType Delta
          }
          Catch {
            return $_
          }
        }
        Remove-PSSession $Session
      }
      Else {
        Import-Module ADSync
        Start-ADSyncSyncCycle -PolicyType Delta
      }
      If (!$Result -or $Result.ToString() -eq "Microsoft.IdentityManagement.PowerShell.ObjectModel.SchedulerOperationStatus") {
        $Now.ToString("yyyyMMddHHmmss") + " Synchronized " + $ChangeCount + " Users/Groups." | Out-File $LogFile -Append
      }
      Else {
        "19990101000001 " + ($Result.ToString().Split("`r")[0]) | Out-File $LogFile -Append
      }
    }
    Else {
      $Now.ToString("yyyyMMddHHmmss") + " No changes." | Out-File $LogFile -Append
    }
  }
  Catch {
    "19990101000001 " + ($_.ToString().Split("`r")[0]) | Out-File $LogFile -Append
  }
  Start-Sleep 15
}

Thanks to Phil Ready from the Environmental Protection Authority in New Zealand for suggesting to include group changes in the AD scan!

Scheduling the Synchronization Script

To synchronize the script use the below settings from the task scheduler. The script is configured to run for five minutes, and the task scheduler should start the job every five minutes. This combination ensures you have it running steadily around the clock. Take care in following these instructions to avoid having multiple scripts running in parallel or no scripts running at all.

Automatic Azure AD Connect PowerShell Script Scheduler

Make sure to use a service/batch account with a non-expiring password. Also, consider that the account needs access to trigger Azure AD Connect synchronization and to write to the log file.

Set the start date/time within the next minutes and let it start automatically. If you start it manually, you risk having two simultaneous threads, which will put extra load on your Domain Controller and blur the log file’s contents.

Automatic Azure AD Connect PowerShell Script Scheduler
Automatic Azure AD Connect PowerShell Script Scheduler

The Program/script path should be the full path to the PowerShell.exe file. The argument should be the full path to the script file. The start folder should be configured to the folder containing the script file.

Make sure you set the task to run as soon as possible after a scheduled start is missed to have it start automatically after a system reboot.

The Smart Solution

You can solve pretty much any problem using scripting – if you have the time… But if you prefer to work smart, we suggest you look at Easy365Manager, which is available as a fully functional free trial.

Easy365Manager increases the functionality of the AD Users & Computers console by adding two new tabs. Using the new settings, you can automatically trigger a synchronization of Azure AD Connect whenever you configure your on-premises user accounts. Easy365Manager also allows you to configure Office 365 licenses and mailbox settings directly.

Instead of jumping around between AD Users & Computers, the Office 365 Portal, Azure AD Connect, and PowerShell – install Easy365Manager and work smart in the extended AD Users & Computers console.

To keep on scripting your way through the world, download below 😉

For detailed instructions on how to schedule the script read this section.