Michael Aitken

IT Engineer, Tinkerer, Educator


PowerShell: Entra Connect Remote Sync

Posted on: February 19th, 2025


Microsoft Entra Connect (formerly Azure AD Connect) has a simple GUI. It's very MIM-like, considering it was built on the same foundations. While kicking off sync cycles in the GUI is a fairly simple process, being on a heavily automation-focused blog, working in the GUI is unacceptable. Luckily for us, the ADSyncTools module is available for managing Entra Connect. Note that installing Entra Connect automatically adds the ADSyncTools module to that server. For the purposes of this article, remotely initiating sync cycles with Invoke-Command, that will suffice, and you will not need to install the module on your local client computer. However, if you want to play around with the module and all its cmdlets, it may benefit you to install it locally so you get Intellisense and can use other cmdlets to learn, like Get-Command.

Invoke-Command

Our script will be encased in an Invoke-Command shell, which will allow us to transmit our commands to our Entra Connect server. It's important to note that you will need administrative rights to the server and, on the server itself, be in either the ADSyncAdmins or ADSyncOperators groups local to the server. You can find those within Computer Management. Not having rights to remotely push commands and not being in one of those two local groups will result in your script failing.

If you're unfamiliar with Invoke-Command, it's fairly simple in terms of our needs here. We'll use it to declare our server's name and then hold our commands in a scriptblock. No need to specify a port or create a PSSession. PowerShell Remoting will create a session automatically, WinRM will allow remote management, and your credentials will be passed for you. You will receive an output when the scriptblock completes with your session details. You can suppress that with Out-Null if you want.

PowerShell

### Invoke-Command, specify server name, leave scriptblock blank for now
$server = 'SyncServer1'
Invoke-Command -ComputerName $server -ScriptBlock {
    Place commands here...
}

Starting a Sync Cycle

The cmdlet you will want to push to your Connect server is Start-AdSyncSyncCycle. This cmdlet is very simple. You will only need to specify what type of sync you want to run, your "policy type". There are two predefined values that will be accepted: "Delta" and "Initial". The initial policy will start a full sync. Be wary of how long that will take in your environment. Delta will start a delta sync. This will (assuming we get no permission errors) initiate your sync. If you're watching the GUI while doing this, you should see the sync process begin. The only feedback you'll receive from PowerShell is runspace information from your session.

PowerShell

### Invoke-Command, specify server name, initiate delta sync
$server = 'SyncServer1'
Invoke-Command -ComputerName $server -ScriptBlock {
    Start-AdSyncSyncCycle -PolicyType $policyType
}

If your goal was to manually start a sync remotely, you're all done. I ask you, where is the fun in that? Are we truly automating if we aren't unnecessarily overcomplicating our scripts? There are a few creature comforts you can add to a script like this that may be useful to you.

Useful Additions

The first thing we'll add is a simple Read-Host that allows you to specify what server you want to start a sync on. Entra Connect is best configured with an active and standby server. You may switch which one is active as you update and test, so you might find some use in being able to declare your server name without having to alter your script. No parameters will be needed here and -AsSecureString will cause WinRM to fail, so we will only want the variable name, the cmdlet, and the quotations.

You may also do the same to specify what policy type should be used in your manual sync. The same applies here, a simple Read-Host will do the job. We may want to add some assurance that we don't fat-finger the policy type and cause an error. We can add that as well with a simple do-while loop that will only exit once an acceptable input has been provided. We could do the same for our server name variable, but we will omit that from our example.

PowerShell

### Use Read-Host to get server name and policy type, initiate sync
$server = Read-Host "Enter the name of your active sync server"
### Do-While loop, only proceed once input matches a value in the array
do {
    $policyType = Read-Host "Enter the desired sync policy type (Delta/Initial)"
    if (@('delta','initial') -inotcontains $policyType) {
        Write-Host 'Invalid input provided. Input must be "Delta" or "Initial".' -ForegroundColor Red
    }
} until (@('delta','initial') -icontains $policyType)

Invoke-Command -ComputerName $server -ScriptBlock {
    Start-AdSyncSyncCycle -PolicyType $policyType
}

Our next addition could be another do-while loop within the Invoke-Command that avoids attempting a sync if one is currently running. If you've made an emergency fix to a user or group while a sync is running, it may be too late for the fix to be synced. If you try to kick off a manual sync with Start-AdSyncSyncCycle while one is already running, you'll get an error saying you have a busy connector. Adding in this do-while check will get your update through as quickly as possible without a current sync, which may run for an indeterminable amount of time, causing an error.

To do this, we'll leverage the Get-AdSyncScheduler cmdlet, particularly the 'SyncCycleInProgress' attribute which contains a Boolean true or false based on whether a sync is actively running or not. We'll just need to query for that attribute until we get a value of 'false'. Within our do-while, we can add a Start-Sleep to delay each re-query. The delay may be an addition you don't care about. Consider the reduced network traffic, avoiding possible rate limiting throttling your queries, reducing the logs you're generating, etc.

PowerShell

### Add do-while loop to check for active syncs before manually starting a new sync
$server = Read-Host "Enter the name of your active sync server"
do {
    $policyType = Read-Host "Enter the desired sync policy type (Delta/Initial)"
    if (@('delta','initial') -inotcontains $policyType) {
        Write-Host 'Invalid input provided. Input must be "Delta" or "Initial".' -ForegroundColor Red
    }
} until (@('delta','initial') -icontains $policyType)
Invoke-Command -ComputerName $server -ScriptBlock {
    do {
        $isRunning = (Get-AdSyncScheduler).SyncCycleInProgress
        if ($isRunning.SyncCycleInProgress -eq $true) {
            Start-Sleep 5
        }
    } until ($isRunning -eq $false)

    Start-AdSyncSyncCycle -PolicyType $policyType
}

We can add a second similar check after Start-AdSyncSyncCycle. This will cause the script to not end until your new sync is complete, at which point you could display a message or, even better, the time of the next scheduled sync. We'll use the Get-AdSyncScheduler cmdlet again but this time we want the 'NextSyncCycleStartTimeInUtc' attribute. It will always be in UTC, which may not be very helpful to you unless you're incredibly familiar with your time zone's difference. I, personally, am not, so we need to convert it.

We can use Get-TimeZone to get our own time zone without the need to explicitly declare it. Plus, if you travel to different time zones, you will get a time you can quickly reference. From there we grab our next sync time and convert it to your local time.

PowerShell

### Duplicate the first sync check after Start-AdSyncSyncCycle
### Get NextSyncCycleStartTimeInUtc with Get-AdSyncScheduler, calculate next sync time in local timezone
$server = Read-Host "Enter the name of your active sync server"
do {
    $policyType = Read-Host "Enter the desired sync policy type (Delta/Initial)"
    if (@('delta','initial') -inotcontains $policyType) {
        Write-Host 'Invalid input provided. Input must be "Delta" or "Initial".' -ForegroundColor Red
    }
} until (@('delta','initial') -icontains $policyType)
Invoke-Command -ComputerName $server -ScriptBlock {
    do {
        $isRunning = (Get-AdSyncScheduler).SyncCycleInProgress
        if ($isRunning.SyncCycleInProgress -eq $true) {
            Write-Host "Sync cycle is currently running. Waiting 10 seconds to retry. (Ctrl+C if you would like to cancel)" -ForegroundColor Gray
            Start-Sleep 10
        }
    } until ($isRunning -eq $false)

    Start-AdSyncSyncCycle -PolicyType $policyType
    
    Start-Sleep 5
    do {
        $isRunning = (Get-AdSyncScheduler).SyncCycleInProgress
        if ($isRunning.SyncCycleInProgress -eq $true) {
            Write-Host "Sync cycle is currently running. Waiting 10 seconds to retry. (Ctrl+C if you would like to cancel)" -ForegroundColor Gray
            Start-Sleep 10
        }
    } until ($isRunning -eq $false)

    $tz = (Get-TimeZone).Id
    $nextRunUtc = (Get-AdSyncScheduler).NextSyncCycleStartTimeInUtc
    $nextRun = ($nextRunUtc.AddHours($tz.TotalHours)).ToLocalTime()

    Write-Host "Next sync time: $nextRun" -ForegroundColor Yellow
} | Out-Null

One last thing we could add in that you may find useful is a connection test that aborts the script if it fails X number of times. Not incredibly necessary, but it may add a bit of convenience. We'll use yet another do-until loop with Test-Connection to check for availability before trying to push commands. If Test-Connection fails the specified amount of times, we'll use Write-Error to provide a reason and set the -ErrorAction parameter to stop our script.

PowerShell

### Add in the connection check before Invoke-Command and after our Read-Hosts
$server = Read-Host "Enter the name of your active sync server"
do {
    $policyType = Read-Host "Enter the desired sync policy type (Delta/Initial)"
    if (@('delta','initial') -inotcontains $policyType) {
        Write-Host 'Invalid input provided. Input must be "Delta" or "Initial".' -ForegroundColor Red
    }
} until (@('delta','initial') -icontains $policyType)

$testCounter = 0
do {
    $testCounter = $testCounter + 1
    if ($testCounter -lt 10) {
        $connTest = Test-Connection -ComputerName $server -Count 1 2>$null
    } else {
        $connTest = Test-Connection -ComputerName $server -Count 1 `
        -ErrorVariable dcConnectionFailed 2>$null
        if ($connectionFailed) {
            Write-Error -Message "Connection test failed. Script will not function normally without this connection." `
            -RecommendedAction "Check network connection. Connect to VPN if remote." `
            -ErrorAction Stop
        }
    }
} until ($testCounter -eq 10 -or $connTest -ne $null)

Invoke-Command -ComputerName $server -ScriptBlock {
    do {
        $isRunning = (Get-AdSyncScheduler).SyncCycleInProgress
        if ($isRunning.SyncCycleInProgress -eq $true) {
            Write-Host "Sync cycle is currently running. Waiting 10 seconds to retry. (Ctrl+C if you would like to cancel)" -ForegroundColor Gray
            Start-Sleep 10
        }
    } until ($isRunning -eq $false)

    Start-AdSyncSyncCycle -PolicyType $policyType
    
    Start-Sleep 5
    do {
        $isRunning = (Get-AdSyncScheduler).SyncCycleInProgress
        if ($isRunning.SyncCycleInProgress -eq $true) {
            Write-Host "Sync cycle is currently running. Waiting 10 seconds to retry. (Ctrl+C if you would like to cancel)" -ForegroundColor Gray
            Start-Sleep 10
        }
    } until ($isRunning -eq $false)

    $tz = (Get-TimeZone).Id
    $nextRunUtc = (Get-AdSyncScheduler).NextSyncCycleStartTimeInUtc
    $nextRun = ($nextRunUtc.AddHours($tz.TotalHours)).ToLocalTime()

    Write-Host "Next sync time: $nextRun" -ForegroundColor Yellow
} | Out-Null

This script is generic enough that it should work in any environment, given the correct permissions and configuration is in place to allow the script to run. You can find the full PS1 file for this script (with notes) on my my GitHub, or copy whichever version you'd like from the above code boxes. Is there anything you would add or do differently? I'd love to hear about it. Shoot an email to [email protected].