# Example: Membership Runbook

This tutorial provides guidance for implementing an Azure Automation runbook that synchronizes membership changes from ServiceChanger to on-premises Active Directory and Entra ID using Azure Arc-enabled Hybrid Worker(s).

### System Requirements

{% hint style="success" %}
**Infrastructure Prerequisites**

* Arc-enabled Hybrid Worker(s)
* PowerShell 7: Runtime environment configured in Azure Automation
* Active Directory Access: VMs must have network access to domain controllers
* Azure AD Connect: PowerShell remoting enabled for delta sync operations
  {% endhint %}

{% hint style="info" %}
**Required PowerShell Modules (on all Arc VMs)**

* Active Directory Module: Install-WindowsFeature -Name RSAT-AD-PowerShell
* Azure Arc Agent: Installed and connected to Azure
* WinRM Configuration: Enabled for Azure AD Connect remoting
  {% endhint %}

{% hint style="warning" %}
**Service Account Permissions**

* Hybrid Worker Service Account: Group membership management permissions in AD
* Azure AD Connect Service Account: Sync operation permissions and remote execution rights
* SMTP Service Account: Email server authentication (if alerts enabled)
  {% endhint %}

{% hint style="info" %}
**Optional Configuration (for Email Alerts)**

* **SMTP Service Account**: Access to email server (Office 365, Exchange, etc.)
  {% endhint %}

{% hint style="info" %}
**Arc-enabled Architecture Benefits**

* Enhanced security through Azure managed identity integration
* Improved connectivity and reliability via Azure Arc agent
* Centralized management through Azure portal
* Multiple VMs provide automatic redundancy and load balancing
  {% endhint %}

***

### Step 1: Configure Azure Automation Variables

Navigate to your Azure Automation Account and create the following variables under **Shared Resources > Variables**:

#### Core Configuration Variables

{% tabs %}
{% tab title="SC\_ApiKey" %}
**Configuration Details:**

* **Name**: `SC_ApiKey`
* **Type**: String
* **Encrypted**: Yes
* **Value**: \[Your ServiceChanger API Key]
* **Description**: ServiceChanger API authentication key

{% hint style="info" %}
Obtain your API key from ServiceChanger application: **Settings > ServiceChanger API Key**
{% endhint %}
{% endtab %}

{% tab title="SC\_LastSyncDate" %}
**Configuration Details:**

* **Name**: `SC_LastSyncDate`
* **Type**: String
* **Encrypted**: No
* **Value**: `2024-01-01T00:00:00Z` (or empty string for full synchronization)
* **Description**: ISO 8601 UTC timestamp of last successful synchronization
  {% endtab %}
  {% endtabs %}

#### Azure AD Connect Variables

{% tabs %}
{% tab title="SC\_AADConnectServer" %}
**Configuration Details:**

* **Name:** SC\_AADConnectServer
* **Type:** String
* **Encrypted:** No
* **Value:** ADCONNECT-CLUSTER.domain.local (server name or cluster VIP)
* **Description:** Azure AD Connect server where delta sync will be executed
  {% endtab %}

{% tab title="SC\_AADConnectUsername" %}
**Configuration Details:**

* **Name:** SC\_AADConnectUsername
* **Type:** String
* **Encrypted:** Yes
* **Value:** DOMAIN\svc-adconnect
* **Description:** Service account username for Azure AD Connect operations
  {% endtab %}

{% tab title="SC\_AADConnectPassword" %}
**Configuration Details:**

* **Name:** SC\_AADConnectPassword
* **Type:** String
* **Encrypted:** Yes
* **Value:** \[Service account password]
* **Description:** Password for Azure AD Connect service account
  {% endtab %}
  {% endtabs %}

#### Email Alert Variables (Optional)

{% hint style="warning" %}
**Note**: These variables are only required if email notifications for critical failures are desired
{% endhint %}

{% tabs %}
{% tab title="SC\_SmtpUsername" %}

* **Name**: `SC_SmtpUsername`
* **Type**: String
* **Encrypted**: Yes
* **Value**: \[SMTP service account email address]
  {% endtab %}

{% tab title="SC\_SmtpPassword" %}

* **Name**: `SC_SmtpPassword`
* **Type**: String
* **Encrypted**: Yes
* **Value**: \[SMTP service account password]
  {% endtab %}

{% tab title="SC\_SmtpTo" %}

* **Name**: `SC_SmtpTo`
* **Type**: String
* **Encrypted**: No
* **Value**: `admin1@company.com,admin2@company.com`
* **Description**: Comma-separated email addresses for alert recipients
  {% endtab %}
  {% endtabs %}

***

### Step 2: Create and Configure the Runbook

#### Navigation and Creation

{% hint style="info" %}
**Navigation Path:** Azure Portal → Automation Account → Process Automation → Runbooks
{% endhint %}

1. Select **"+ Create Runbook"**

#### Runbook Configuration

| Setting             | Value                                                                              |
| ------------------- | ---------------------------------------------------------------------------------- |
| **Name**            | `ServiceChanger-AD-MembershipSync`                                                 |
| **Runbook Type**    | PowerShell                                                                         |
| **Runtime Version** | 7.2                                                                                |
| **Description**     | Enterprise AD membership synchronization with ServiceChanger API using Arc Workers |

#### Code Deployment

1. Open the newly created runbook
2. Insert the runbook code below
3. Save your changes
4. Publish the runbook

{% hint style="warning" %}
**Note:** If you encounter any issues during setup, verify that your Hybrid Worker is correctly configured and that PowerShell 7.2 is installed on your server.
{% endhint %}

{% hint style="danger" %}
Use the script below at your own risk. We strongly recommend thoroughly reading and testing it before execution.
{% endhint %}

```powershell
<#
.SYNOPSIS
    Enterprise Azure Automation Runbook: ServiceChanger AD Membership Synchronization

.DESCRIPTION
    This runbook synchronizes membership changes from ServiceChanger API to on-premises Active Directory.
    It polls the ServiceChanger API for membership changes where onPremisesSyncEnabled is true,
    then applies those changes to local AD groups using a Hybrid Runbook Worker.

.FEATURES
    • Delta synchronization using persistent LastSyncDate tracking
    • Batch processing for optimal performance
    • AD object caching to minimize directory queries
    • Automatic Azure AD Connect delta sync to Entra ID via PowerShell remoting
    • Comprehensive error handling with retry logic
    • Optional email alerting on failures
    • Idempotent operations to prevent duplicate changes
    • Detailed logging with UTC timestamps

.PREREQUISITES
    • Azure Automation Account with Hybrid Runbook Worker (ARC-enabled recommended)
    • PowerShell 7 runtime
    • Active Directory PowerShell module on Hybrid Worker
    • Azure AD Connect server with PowerShell remoting enabled
    • Valid ServiceChanger API key
    • SMTP service account (if email alerts enabled)
    • Hybrid Worker service account with AD group management permissions
    • Dedicated service account for Azure AD Connect operations

.ARC HYBRID WORKERS
    This runbook is fully compatible with Azure Arc-enabled Hybrid Workers, which provide:
    • Enhanced security through Azure managed identity integration
    • Improved connectivity and reliability via Azure Arc agent
    • Centralized management through Azure portal
    • Better monitoring and diagnostics capabilities
    
    For ARC-enabled deployments:
    • Ensure ActiveDirectory PowerShell module is installed on all ARC machines
    • Verify outbound HTTPS connectivity to ServiceChanger API (api.servicechanger.com)
    • Confirm PowerShell remoting access to Azure AD Connect server from all VMs
    • Consider using managed identity for future authentication enhancements
    • Multiple ARC-enabled VMs provide automatic redundancy and load balancing
    
    Network Requirements for ARC Workers:
    • HTTPS 443 outbound to Azure (handled by ARC agent)
    • Domain connectivity for Active Directory operations
    • Network access to Azure AD Connect server for PowerShell remoting
    • SMTP connectivity if email alerts are enabled

.AZURE AUTOMATION VARIABLES
    Required (Encrypted):
    • SC_ApiKey: ServiceChanger API authentication key
    • SC_AADConnectUsername: Azure AD Connect service account username
    • SC_AADConnectPassword: Azure AD Connect service account password
    
    Required (Not Encrypted):
    • SC_LastSyncDate: ISO 8601 UTC timestamp of last successful sync
    • SC_AADConnectServer: Server name where Azure AD Connect is installed
    
    Optional (Encrypted - for email alerts):
    • SC_SmtpUsername: SMTP service account username
    • SC_SmtpPassword: SMTP service account password
    • SC_SmtpTo: Comma-separated email addresses for alerts

.NOTES
    Version: 1.2
    Author: ServiceChanger BV - Ruben van der Graaf
    Last Modified: 2025-07-31
    
    This runbook automatically triggers Azure AD Connect delta sync after making AD changes
    using PowerShell remoting to execute commands on the designated Azure AD Connect server.
    The Hybrid Worker must have network connectivity to the AD Connect server and appropriate
    service account permissions for remote execution.
    
.PARAMETER EnableEmailAlerts
    Enable email notifications on errors. Default: $false
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $false)]
    [bool]$EnableEmailAlerts = $false
)

#region Configuration
# ServiceChanger API Configuration
$ApiBaseUrl = "https://api.servicechanger.com/public/directory-changes"

# SMTP Configuration (customize for your environment)
$SmtpServer = "smtp.office365.com"
$SmtpPort = 587
$SmtpFrom = "serviceaccount@yourdomain.com"
$EnableSsl = $true

# Processing Configuration
$RetryCount = 3
$BatchSize = 100
$LogFilePath = "$env:TEMP\MembershipSyncLog_$(Get-Date -Format 'yyyyMMdd_HHmmss').txt"
#endregion

#region Azure Automation Variables
try {
    # Core ServiceChanger variables
    $ApiKey = Get-AutomationVariable -Name 'SC_ApiKey'
    $lastSyncDate = Get-AutomationVariable -Name 'SC_LastSyncDate'
    
    # Azure AD Connect variables
    $AADConnectServer = Get-AutomationVariable -Name 'SC_AADConnectServer'
    $AADConnectUsername = Get-AutomationVariable -Name 'SC_AADConnectUsername'
    $AADConnectPassword = Get-AutomationVariable -Name 'SC_AADConnectPassword'
    
    # SMTP variables (only load if email alerts enabled)
    if ($EnableEmailAlerts) {
        $SmtpUsername = Get-AutomationVariable -Name 'SC_SmtpUsername'
        $SmtpPassword = Get-AutomationVariable -Name 'SC_SmtpPassword'
        $SmtpToVar = Get-AutomationVariable -Name 'SC_SmtpTo'
        $SmtpTo = if ($SmtpToVar) { $SmtpToVar.Split(',') | ForEach-Object { $_.Trim() } } else { @() }
    } else {
        # Initialize SMTP variables as null for safety
        $SmtpUsername = $null
        $SmtpPassword = $null
        $SmtpTo = @()
    }
    
} catch {
    throw "Failed to load Azure Automation Variables. Ensure all required variables are configured: $_"
}
#endregion

#region Helper Functions
function Write-Log {
    <#
    .SYNOPSIS
        Writes timestamped log messages with color coding for Azure Automation console
    #>
    param (
        [Parameter(Mandatory = $true)]
        [string]$Message,
        
        [Parameter(Mandatory = $false)]
        [ValidateSet("INFO", "WARNING", "ERROR", "SUCCESS")]
        [string]$Level = "INFO"
    )
    
    $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
    $logMessage = "[$timestamp] [$Level] $Message"
    
    # Azure Automation console output with ANSI color codes
    switch ($Level) {
        "INFO"    { Write-Output "`e[32m$logMessage`e[0m" }
        "WARNING" { Write-Warning "`e[33m$logMessage`e[0m" }
        "ERROR"   { Write-Error "`e[31m$logMessage`e[0m" }
        "SUCCESS" { Write-Output "`e[36m$logMessage`e[0m" }
    }
    
    # Append to log file for email attachments
    Add-Content -Path $LogFilePath -Value $logMessage -ErrorAction SilentlyContinue
}

function Send-ErrorAlert {
    <#
    .SYNOPSIS
        Sends email alert with log attachment when runbook fails after all retries
    #>
    param (
        [Parameter(Mandatory = $true)]
        [string]$ErrorMessage
    )
    
    if (-not $EnableEmailAlerts) { return }
    
    # Validate SMTP variables are available
    if (-not $SmtpUsername -or -not $SmtpPassword -or $SmtpTo.Count -eq 0) {
        Write-Log "Email alerts enabled but SMTP variables not properly configured" "WARNING"
        return
    }
    
    Write-Log "Sending email alert to $($SmtpTo -join ', ')..." "INFO"
    
    try {
        $securePassword = ConvertTo-SecureString $SmtpPassword -AsPlainText -Force
        $credential = New-Object System.Management.Automation.PSCredential ($SmtpUsername, $securePassword)
        
        $subject = "ServiceChanger AD Sync - Error Alert"
        $body = @"
ServiceChanger AD Synchronization Runbook failed after $RetryCount retries.

Error Details:
$ErrorMessage

Timestamp: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss UTC')

Please review the attached log file for detailed information and take appropriate action.

This is an automated alert from Azure Automation.
"@
        
        Send-MailMessage -SmtpServer $SmtpServer -Port $SmtpPort -UseSsl:$EnableSsl `
                         -Credential $credential -From $SmtpFrom -To $SmtpTo `
                         -Subject $subject -Body $body -Attachments $LogFilePath
        
        Write-Log "Email alert sent successfully" "SUCCESS"
    } catch {
        Write-Log "Failed to send email alert: $_" "ERROR"
    }
}

function Invoke-AADConnectSync {
    <#
    .SYNOPSIS
        Triggers Azure AD Connect delta sync on remote server using PowerShell remoting
    #>
    param (
        [Parameter(Mandatory = $true)]
        [int]$ChangesProcessed
    )
    
    if ($ChangesProcessed -eq 0) {
        Write-Log "No changes processed - skipping Azure AD Connect sync" "INFO"
        return
    }
    
    Write-Log "Triggering Azure AD Connect delta sync to push $ChangesProcessed changes to Entra ID..." "INFO"
    
    try {
        # Validate required variables
        if (-not $AADConnectServer) {
            throw "SC_AADConnectServer variable not configured"
        }
        if (-not $AADConnectUsername) {
            throw "SC_AADConnectUsername variable not configured"
        }
        if (-not $AADConnectPassword) {
            throw "SC_AADConnectPassword variable not configured"
        }
        
        # Create credential object for remote connection
        $securePassword = ConvertTo-SecureString $AADConnectPassword -AsPlainText -Force
        $credential = New-Object System.Management.Automation.PSCredential ($AADConnectUsername, $securePassword)
        
        Write-Log "Executing sync command on remote server: $AADConnectServer" "INFO"
        
        # Execute Azure AD Connect sync on remote server
        $syncResult = Invoke-Command -ComputerName $AADConnectServer -Credential $credential -ScriptBlock {
            try {
                Import-Module ADSync -ErrorAction Stop
                $result = Start-ADSyncSyncCycle -PolicyType Delta -ErrorAction Stop
                return $result
            } catch {
                throw "Remote sync execution failed: $($_.Exception.Message)"
            }
        } -ErrorAction Stop
        
        if ($syncResult) {
            Write-Log "Azure AD Connect delta sync initiated successfully on $AADConnectServer - Result: $($syncResult.Result)" "SUCCESS"
        } else {
            Write-Log "Azure AD Connect sync command executed but no result returned from $AADConnectServer" "WARNING"
        }
        
    } catch {
        Write-Log "Failed to trigger Azure AD Connect sync: $_" "WARNING"
        Write-Log "Changes were applied to local AD but may not sync to Entra ID immediately" "WARNING"
        Write-Log "Verify network connectivity to $AADConnectServer and service account permissions" "WARNING"
    }
}
#endregion

#region Main Execution
try {
    # Initialize execution
    "" | Out-File $LogFilePath
    Write-Log "Starting ServiceChanger AD membership synchronization" "INFO"
    Write-Log "Runbook version: 1.2 | Batch size: $BatchSize | Retry count: $RetryCount" "INFO"
    Write-Log "Azure AD Connect server: $AADConnectServer" "INFO"
    
    # Load required PowerShell module
    Import-Module ActiveDirectory -ErrorAction Stop
    Write-Log "Active Directory PowerShell module loaded successfully" "SUCCESS"
    
    # Prepare API request parameters
    $utcNow = (Get-Date).ToUniversalTime()
    $queryParams = @{
        resourceType = "membership"
        onPremisesSyncEnabled = "true"
    }
    if ($lastSyncDate) { 
        $queryParams['lastSyncDate'] = $lastSyncDate
        Write-Log "Querying changes since last sync: $lastSyncDate" "INFO"
    } else {
        Write-Log "No previous sync date found - performing full synchronization" "INFO"
    }
    
    # Build API request URL with proper encoding
    $queryString = ($queryParams.GetEnumerator() | ForEach-Object { "$($_.Key)=" + [uri]::EscapeDataString($_.Value) }) -join "&"
    $fullUrl = $ApiBaseUrl + "?" + $queryString
    Write-Log "API endpoint: $fullUrl" "INFO"
    
    # Execute API call with retry logic
    $apiResponse = $null
    for ($attempt = 1; $attempt -le $RetryCount; $attempt++) {
        try {
            Write-Log "API call attempt $attempt of $RetryCount" "INFO"
            
            $headers = @{
                "x-api-key" = $ApiKey
                "Content-Type" = "application/json"
            }
            
            $response = Invoke-RestMethod -Uri $fullUrl -Method Get -Headers $headers -ErrorAction Stop
            
            # Validate and extract ServiceChanger API response structure
            if (-not $response.data) {
                throw "Invalid API response structure. Expected 'data' property."
            }
            $apiResponse = $response.data
            
            Write-Log "API call successful - Retrieved $($apiResponse.Count) membership changes" "SUCCESS"
            break
        } catch {
            Write-Log "API call attempt $attempt failed: $_" "WARNING"
            if ($attempt -eq $RetryCount) { 
                throw "API call failed after $RetryCount attempts: $_"
            }
            Start-Sleep -Seconds (5 * $attempt)
        }
    }
    
    # Early exit if no changes to process
    if ($apiResponse.Count -eq 0) {
        $newSyncDate = $utcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
        Write-Log "No membership changes found - Updating LastSyncDate to $newSyncDate" "SUCCESS"
        Set-AutomationVariable -Name 'SC_LastSyncDate' -Value $newSyncDate
        Write-Log "Synchronization completed successfully" "SUCCESS"
        return
    }
    
    Write-Log "Processing $($apiResponse.Count) membership changes from ServiceChanger API" "INFO"
    
    # Pre-validate AD objects and build cache for performance optimization
    Write-Log "Pre-validating Active Directory objects for performance optimization..." "INFO"
    $groupCache = @{}
    $userCache = @{}
    $validChanges = @()
    
    foreach ($change in $apiResponse) {
        $groupId = $change.resource.groupId
        $userId = $change.resource.userId
        
        # Cache group lookups to minimize AD queries
        if (-not $groupCache.ContainsKey($groupId)) {
            $localGroup = Get-ADGroup -Identity $groupId -ErrorAction SilentlyContinue
            $groupCache[$groupId] = $localGroup
        }
        
        # Cache user lookups to minimize AD queries
        if (-not $userCache.ContainsKey($userId)) {
            $localUser = Get-ADUser -Identity $userId -ErrorAction SilentlyContinue
            $userCache[$userId] = $localUser
        }
        
        # Only process changes where both AD objects exist
        if ($groupCache[$groupId] -and $userCache[$userId]) {
            $validChanges += $change
        } else {
            $missingGroup = if (-not $groupCache[$groupId]) { "group '$groupId'" } else { "" }
            $missingUser = if (-not $userCache[$userId]) { "user '$userId'" } else { "" }
            $missing = ($missingGroup, $missingUser | Where-Object { $_ }) -join " and "
            Write-Log "Skipping change: Local $missing not found in Active Directory" "WARNING"
        }
    }
    
    Write-Log "Validation complete: $($validChanges.Count) valid changes, $($apiResponse.Count - $validChanges.Count) skipped" "INFO"
    
    # Early exit if no valid changes after AD validation
    if ($validChanges.Count -eq 0) {
        $newSyncDate = $utcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
        Write-Log "No valid changes to process (all changes reference missing AD objects)" "WARNING"
        Set-AutomationVariable -Name 'SC_LastSyncDate' -Value $newSyncDate
        Write-Log "LastSyncDate updated to prevent reprocessing these changes" "INFO"
        return
    }
    
    # Process changes in batches for optimal performance
    Write-Log "Processing $($validChanges.Count) valid changes in batches of $BatchSize" "INFO"
    $processedCount = 0
    $errorCount = 0
    
    for ($i = 0; $i -lt $validChanges.Count; $i += $BatchSize) {
        $batchEnd = [Math]::Min($i + $BatchSize, $validChanges.Count)
        $batch = $validChanges[$i..($batchEnd - 1)]
        Write-Log "Processing batch: items $($i + 1) to $batchEnd" "INFO"
        
        foreach ($change in $batch) {
            try {
                # Extract change details from ServiceChanger API response
                $groupId = $change.resource.groupId
                $userId = $change.resource.userId
                $action = $change.changeType
                $timestamp = $change.timestamp
                
                # Retrieve cached AD objects for performance
                $localGroup = $groupCache[$groupId]
                $localUser = $userCache[$userId]
                
                # Get human-readable names for logging
                $groupName = $localGroup.Name
                $userName = $localUser.DisplayName ?? $localUser.SamAccountName
                
                Write-Log "Processing $action - '$userName' in group '$groupName'" "INFO"
                
                # Apply membership changes with idempotent operations
                $actualChangePerformed = $false
                switch ($action) {
                    "created" {
                        $currentMembers = Get-ADGroupMember -Identity $localGroup -ErrorAction SilentlyContinue
                        $memberNames = if ($currentMembers) { $currentMembers.SamAccountName } else { @() }
                        if ($memberNames -notcontains $localUser.SamAccountName) {
                            Add-ADGroupMember -Identity $localGroup -Members $localUser -ErrorAction Stop
                            Write-Log "Added '$userName' to group '$groupName' (timestamp: $timestamp)" "SUCCESS"
                            $actualChangePerformed = $true
                        } else {
                            Write-Log "User '$userName' already member of group '$groupName' - No action required" "INFO"
                        }
                    }
                    
                    "deleted" {
                        $currentMembers = Get-ADGroupMember -Identity $localGroup -ErrorAction SilentlyContinue
                        $memberNames = if ($currentMembers) { $currentMembers.SamAccountName } else { @() }
                        if ($memberNames -contains $localUser.SamAccountName) {
                            Remove-ADGroupMember -Identity $localGroup -Members $localUser -Confirm:$false -ErrorAction Stop
                            Write-Log "Removed '$userName' from group '$groupName' (timestamp: $timestamp)" "SUCCESS"
                            $actualChangePerformed = $true
                        } else {
                            Write-Log "User '$userName' not member of group '$groupName' - No action required" "INFO"
                        }
                    }
                    
                    "updated" {
                        Write-Log "Membership update detected for '$userName' in group '$groupName' - Manual review recommended" "WARNING"
                    }
                    
                    default {
                        Write-Log "Unknown change type '$action' for user '$userName' in group '$groupName' - Skipping" "WARNING"
                    }
                }
                
                if ($actualChangePerformed) {
                    $processedCount++
                }
                
            } catch {
                $errorCount++
                $userDisplayName = if ($userName) { "'$userName'" } else { $userId }
                $groupDisplayName = if ($groupName) { "'$groupName'" } else { $groupId }
                Write-Log "Failed to process change for user $userDisplayName in group $groupDisplayName - Error: $_" "ERROR"
            }
        }
    }
    
    # Trigger Azure AD Connect delta sync via PowerShell remoting
    Invoke-AADConnectSync -ChangesProcessed $processedCount
    
    # Update LastSyncDate to current UTC time
    $newSyncDate = $utcNow.ToString('yyyy-MM-ddTHH:mm:ssZ')
    
    # Provide comprehensive completion summary
    $skippedChanges = $apiResponse.Count - $validChanges.Count
    $reviewedChanges = $validChanges.Count - $errorCount
    if ($skippedChanges -gt 0) {
        Write-Log "Synchronization completed: $processedCount actual changes, $reviewedChanges reviewed, $errorCount errors, $skippedChanges skipped (missing AD objects)" "INFO"
    } else {
        Write-Log "Synchronization completed successfully: $processedCount actual changes, $reviewedChanges reviewed, $errorCount errors" "SUCCESS"
    }
    
    Write-Log "LastSyncDate updated to $newSyncDate for next synchronization cycle" "SUCCESS"
    Set-AutomationVariable -Name 'SC_LastSyncDate' -Value $newSyncDate
    
} catch {
    Write-Log "Critical error in runbook execution: $_" "ERROR"
    Send-ErrorAlert $_
    throw $_
} finally {
    # Cleanup temporary log file if email alerts are disabled
    if (-not $EnableEmailAlerts) { 
        Remove-Item $LogFilePath -Force -ErrorAction SilentlyContinue
    }
    Write-Log "ServiceChanger AD membership synchronization completed" "INFO"
}
#endregion 
```

***

### Step 3: Email Alert Configuration (Optional)

{% hint style="info" %}
Email notifications are triggered only for critical runbook failures that prevent completion
{% endhint %}

To enable email notifications:

1. **Edit the runbook**
2. **Customize environment-specific settings**:

{% code title="SMTP Configuration (Lines 57-60)" %}

```powershell
# Line 57: Configure SMTP server for your environment
$SmtpServer = "smtp.office365.com"

# Line 59: Set sender email address
$SmtpFrom = "serviceaccount@yourdomain.com"
```

{% endcode %}

3. **Modify the parameter at the top of the script**:

{% code title="Enable Email Alerts" %}

```powershell
# Change from:
[bool]$EnableEmailAlerts = $false

# Change to:
[bool]$EnableEmailAlerts = $true
```

{% endcode %}

{% hint style="info" %}
**Alternative**: Pass the parameter when starting the runbook manually or configure it in the schedule.
{% endhint %}

4. **Save and republish the runbook**

***

### Step 4: Schedule Configuration

#### Schedule Creation

{% hint style="success" %}
**Recommended Frequency:** 1 hour (minimum interval supported by Azure Automation)
{% endhint %}

1. Select **"+ Link to schedule"**
2. Choose **"Link a schedule to your runbook"**
3. Configure the new schedule:

| Setting         | Recommended Value              |
| --------------- | ------------------------------ |
| **Name**        | `ServiceChanger-Sync-Schedule` |
| **Frequency**   | Recurring                      |
| **Recur every** | 1 hour                         |
| **Time zone**   | Your organizational timezone   |

#### Execution Settings

* **Run on**: Hybrid Worker
* **Hybrid Worker Group**: \[Your configured group name]

{% hint style="warning" %}
**Important**: Ensure the runbook is configured to run on the Hybrid Worker, not in Azure. The runbook requires access to on-premises Active Directory.
{% endhint %}

***

### Step 5: Testing and Validation

#### Initial Test Execution

1. Execute **"Start"** within the runbook interface
2. Monitor the **Output** tab for execution logs and status
3. Verify successful completion and review log entries

#### Validation Checklist

{% hint style="success" %}
**System Verification Points:**

* API connection established successfully
* Active Directory objects located and processed
* SC\_LastSyncDate variable updated correctly
* Azure AD Connect sync initiated (if applicable)
* Email alert functionality operational (if enabled)
  {% endhint %}

***

### Troubleshooting Guide

#### Common Error Scenarios

{% tabs %}
{% tab title="Variable Configuration" %}
**Error Message:** `"Failed to load Azure Automation Variables"`

**Resolution Steps:**

* Verify all required variables exist with correct naming (case-sensitive)
* Confirm encrypted variables are properly marked as encrypted
* Validate variable names for typographical errors
* Ensure the Automation Account has proper permissions
* Check specifically for new Azure AD Connect variables (SC\_AADConnectServer, SC\_AADConnectUsername, SC\_AADConnectPassword)
  {% endtab %}

{% tab title="API Connectivity" %}
**Error Message:** `"API call failed after 3 attempts"`

**Resolution Steps:**

* Validate ServiceChanger API key authenticity and permissions
* Verify x-api-key header format (not Authorization Bearer)
* Confirm query parameters: resourceType=membership, onPremisesSyncEnabled=true
* Test API endpoint connectivity from Arc VM: Test-NetConnection api.servicechanger.com -Port 443
* Check firewall rules and proxy settings on Arc VMs
  {% endtab %}

{% tab title="Active Directory" %}
**Error Message:** `"Local user/group not found"`

**Resolution Steps:**

* Confirm users/groups exist in local Active Directory
* Verify ObjectGUID matching between ServiceChanger and AD (direct lookup approach)
* Validate service account has read permissions on AD
* Test AD connectivity: Get-ADDomain from Arc VM
* Check domain trust relationships if multiple domains involved
  {% endtab %}

{% tab title="Azure AD Connect" %}
**Error Message:** `"Failed to trigger Azure AD Connect sync: Access denied"`

**Resolution Steps:**

* Verify SC\_AADConnectServer variable contains correct server/cluster name
* Confirm PowerShell remoting is enabled on Azure AD Connect server
* Validate service account permissions for remote execution
* Test WinRM connectivity: Test-WsMan ADCONNECT-SERVER.domain.local
* Check ADSync module availability on Azure AD Connect server
* Verify service account is member of ADSyncAdmins group
  {% endtab %}

{% tab title="Arc Connectivity" %}
**Error Message:** `"Job failed to start on Hybrid Worker"`

**Resolution Steps:**

* Check Azure Arc agent status: azcmagent show
* Verify Arc VM connectivity in Azure portal
* Confirm Hybrid Worker group configuration
* Test Azure Automation connectivity from Arc VM
* Check Arc agent logs in Event Viewer
* Restart Azure Arc agent service if needed
  {% endtab %}
  {% endtabs %}

#### Support Resources

{% hint style="info" %}
**Technical Support:** The runbook includes comprehensive error handling and detailed logging. All operations generate timestamped log entries with specific error descriptions for troubleshooting purposes.
{% endhint %}

***


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://servicechanger.gitbook.io/servicechanger.com/general/api-documentation/example-membership-runbook.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
