Task 01: OU Creation & Pre-Creation Artifacts
DOCUMENT CATEGORY: Runbook
SCOPE: Active Directory preparation
PURPOSE: Create Azure Local OU and LCM user account
MASTER REFERENCE: Microsoft Learn - AD Prerequisites
Status: Active Estimated Time: 15 minutes Last Updated: 2026-01-31
Overview
Create the Azure Local OU container and LCM (Lifecycle Manager) user account using Microsoft's AsHciADArtifactsPreCreationTool.
Prerequisites
| Requirement | Description |
|---|---|
| Permissions | Create OU and user account in AD |
| Module | AsHciADArtifactsPreCreationTool v2402+ |
AD structure should be defined during the planning phase. Update variables below for your environment.
Variables from variables.yml
| Variable Path | Type | Description |
|---|---|---|
identity.active_directory.ad_clusters_ou_path | string | OU path for Azure Local cluster computer objects |
identity.active_directory.ad_domain_fqdn | string | Active Directory domain FQDN |
identity.accounts.account_lcm_username | string | Lifecycle Manager deployment account username |
identity.accounts.account_lcm_password | string | LCM account password (keyvault:// URI) |
platform.kv_platform_name | string | Key Vault name for secret retrieval |
azure_vms.dc01.resource_group | string | Resource group of the domain controller VM |
azure_vms.dc01.name | string | Domain controller VM name (for AzVM execution) |
azure_vms.dc01.hostname | string | Domain controller hostname (for Arc execution) |
Execution
- Standalone Script
- Azure VM Run Command
- Orchestrated Script
Run directly on a domain controller or domain-joined machine with AD tools installed (e.g., RSAT).
# Adjust variable values for your environment; these are examples.
$lcm_password = '<password>' # Replace with a strong password for LCM user
$lcm_user = 'AzureLocalDeployUser' # LCM user name
$OuPath = "OU=AzureLocal-Cluster01,OU=AzureLocal,OU=Servers,DC=hybrid,DC=mgmt" # OU path
$password = ConvertTo-SecureString $lcm_password -AsPlainText -Force
$user = $lcm_user
$credential = New-Object System.Management.Automation.PSCredential ($user, $password)
if (-not (Get-Module -ListAvailable -Name AsHciADArtifactsPreCreationTool)) {
Install-Module AsHciADArtifactsPreCreationTool -Repository PSGallery -Force
}
if (-not (Get-KdsRootKey -ErrorAction SilentlyContinue)) {
Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10)
}
New-HciAdObjectsPreCreation -AzureStackLCMUserCredential $credential -AsHciOUName $OuPath -Verbose
When to use: Run remotely against an Azure VM (e.g., domain controller) using
Invoke-AzVMRunCommand. No RDP or direct connectivity required — commands are sent through the Azure fabric.
Script
Primary: scripts/deploy/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/powershell/Invoke-OUCreation-AzVM.ps1
Alternatives:
| Variant | Path |
|---|---|
| Azure CLI | scripts/deploy/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/azure-cli/Invoke-OUCreation-AzVM.ps1 |
| Bash | scripts/deploy/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/bash/invoke-ou-creation-azvm.sh |
Code
Run remotely against an Azure VM (e.g., domain controller) using Invoke-AzVMRunCommand. No RDP or direct connectivity required — commands are sent through the Azure fabric.
Prerequisites
Az.Computemodule installed (Install-Module Az.Compute)powershell-yamlmodule installed (Install-Module powershell-yaml)- Authenticated to Azure (
Connect-AzAccount)- Contributor or Virtual Machine Contributor role on the target VM
param(
[Parameter(Mandatory = $true)]
[string]$ConfigFile
)
$ErrorActionPreference = "Stop"
# Load configuration
Import-Module powershell-yaml -ErrorAction Stop
$config = Get-Content $ConfigFile -Raw | ConvertFrom-Yaml
# Resolve VM target from config
$ResourceGroupName = $config.azure_vms.dc01.resource_group
$VMName = $config.azure_vms.dc01.name
# Extract AD values from config
$clusterOUPath = $config.active_directory.ad_clusters_ou_path
if (-not $clusterOUPath) { $clusterOUPath = $config.cluster.arm_deployment.ou_path }
$lcmUsername = $config.accounts.account_lcm_username
$lcmSamAccount = if ($lcmUsername -match '^([^@]+)@') { $Matches[1] } else { $lcmUsername }
# Retrieve LCM password from Key Vault
$kvName = $config.platform.kv_platform_name
$lcmPassword = Get-AzKeyVaultSecret -VaultName $kvName -Name "lcm-password" -AsPlainText -ErrorAction Stop
# Build remote script — config values baked in as literals
$remoteScript = @"
`$ErrorActionPreference = 'Stop'
if (-not (Get-Module -ListAvailable -Name AsHciADArtifactsPreCreationTool)) {
Install-Module AsHciADArtifactsPreCreationTool -Repository PSGallery -Force
}
if (-not (Get-KdsRootKey -ErrorAction SilentlyContinue)) {
Add-KdsRootKey -EffectiveTime (Get-Date).AddHours(-10)
Write-Output 'KDS Root Key created.'
}
`$password = ConvertTo-SecureString '$lcmPassword' -AsPlainText -Force
`$credential = New-Object System.Management.Automation.PSCredential ('$lcmSamAccount', `$password)
New-HciAdObjectsPreCreation -AzureStackLCMUserCredential `$credential -AsHciOUName '$clusterOUPath' -Verbose
Write-Output 'OU creation and LCM user pre-creation complete.'
"@
# Verify Azure context
$ctx = Get-AzContext -ErrorAction Stop
if (-not $ctx) { Write-Host "ERROR: Run Connect-AzAccount first." -ForegroundColor Red; exit 1 }
# Verify VM is running
$vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -Status -ErrorAction Stop
$powerState = ($vm.Statuses | Where-Object { $_.Code -like "PowerState/*" }).DisplayStatus
if ($powerState -ne 'VM running') {
Write-Host "ERROR: VM '$VMName' is not running (state: $powerState)." -ForegroundColor Red; exit 1
}
# Execute remote script
Write-Host "Executing OU creation on $VMName..." -ForegroundColor Cyan
$result = Invoke-AzVMRunCommand `
-ResourceGroupName $ResourceGroupName `
-VMName $VMName `
-CommandId "RunPowerShellScript" `
-ScriptString $remoteScript `
-ErrorAction Stop
# Display output
$stdout = $result.Value | Where-Object { $_.Code -eq "ComponentStatus/StdOut/succeeded" }
$stderr = $result.Value | Where-Object { $_.Code -eq "ComponentStatus/StdErr/succeeded" }
if ($stdout.Message) { Write-Host $stdout.Message }
if ($stderr.Message) { Write-Host "--- Errors ---" -ForegroundColor Red; Write-Host $stderr.Message -ForegroundColor Red }
Tip: If empty output is returned, verify the VM name and resource group, and confirm the VM is running.
When to use: Multi-node deployment from management server using
variables.yml.
See Standalone Script tab for primary implementation.
Verification
- Standalone Script
- Azure VM Run Command
Run directly from a domain-joined machine or a session with AD tools installed (e.g., RDP into DC, jump box, or local RSAT).
# Verify OU was created
Get-ADOrganizationalUnit -LDAPFilter "(name=AzureLocal-Cluster01)" | Select-Object DistinguishedName
# Verify LCM user was created in OU
Get-ADUser -Identity "AzureLocalDeployUser" | Select-Object Name, DistinguishedName, Enabled
# Verify KDS Root Key exists
Get-KdsRootKey | Select-Object KeyId, EffectiveTime
When to use: Run remotely against an Azure VM (e.g., domain controller) using
Invoke-AzVMRunCommand. No RDP or direct connectivity required — commands are sent through the Azure fabric.
Script
Primary: scripts/validation/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/powershell/Test-OUCreation-AzVM.ps1
Alternatives:
| Variant | Path |
|---|---|
| Azure CLI | scripts/validation/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/azure-cli/Test-OUCreation-AzVM.ps1 |
| Bash | scripts/validation/03-onprem-readiness/phase-01-active-directory/task-01-ou-creation-pre-creation-artifacts/bash/test-ou-creation-azvm.sh |
Code
Run remotely against an Azure VM (e.g., domain controller) using Invoke-AzVMRunCommand. No RDP or direct connectivity required — commands are sent through the Azure fabric.
Prerequisites
Az.Computemodule installed (Install-Module Az.Compute)powershell-yamlmodule installed (Install-Module powershell-yaml)- Authenticated to Azure (
Connect-AzAccount)- Contributor or Virtual Machine Contributor role on the target VM
param(
[Parameter(Mandatory = $true)]
[string]$ConfigFile
)
$ErrorActionPreference = "Stop"
# Load configuration
Import-Module powershell-yaml -ErrorAction Stop
$config = Get-Content $ConfigFile -Raw | ConvertFrom-Yaml
# Resolve VM target from config
$ResourceGroupName = $config.azure_vms.dc01.resource_group
$VMName = $config.azure_vms.dc01.name
# Extract AD values from config
$adDomainFqdn = $config.active_directory.ad_domain_fqdn
if (-not $adDomainFqdn) { $adDomainFqdn = $config.active_directory.domain.fqdn }
$clusterOUPath = $config.active_directory.ad_clusters_ou_path
if (-not $clusterOUPath) { $clusterOUPath = $config.cluster.arm_deployment.ou_path }
$lcmUsername = $config.accounts.account_lcm_username
$lcmSamAccount = if ($lcmUsername -match '^([^@]+)@') { $Matches[1] } else { $lcmUsername }
# Build remote script — config values baked in as literals
$remoteScript = @"
`$ErrorActionPreference = 'Continue'
Import-Module ActiveDirectory -ErrorAction Stop
`$passed = 0; `$failed = 0; `$warned = 0
function Write-Check(`$Name, `$Status, `$Detail) {
`$icon = switch (`$Status) { 'PASS' { '✓' } 'FAIL' { '✗' } 'WARN' { '⚠' } 'INFO' { 'ℹ' } }
`$msg = " `$icon `$Name"
if (`$Detail) { `$msg += " - `$Detail" }
Write-Output `$msg
switch (`$Status) { 'PASS' { `$script:passed++ } 'FAIL' { `$script:failed++ } 'WARN' { `$script:warned++ } }
}
Write-Output "========================================="
Write-Output "Task 01: OU & Pre-Creation Validation (Remote)"
Write-Output "========================================="
Write-Output " Domain: $adDomainFqdn"
Write-Output " Cluster OU: $clusterOUPath"
Write-Output " LCM Account: $lcmSamAccount"
Write-Output ""
# CHECK: KDS Root Key
Write-Output "Checking KDS Root Key..."
try {
`$kds = Get-KdsRootKey -ErrorAction SilentlyContinue
if (`$kds) { Write-Check 'KDS Root Key' 'PASS' "Key ID: `$(`$kds[0].KeyId.ToString().Substring(0,8))..." }
else { Write-Check 'KDS Root Key' 'FAIL' 'Not found' }
} catch { Write-Check 'KDS Root Key' 'WARN' "Cannot check: `$_" }
# CHECK: OU Structure
Write-Output ""
Write-Output "Checking OU structure..."
try {
`$ou = Get-ADOrganizationalUnit -Identity "$clusterOUPath" -ErrorAction Stop
Write-Check 'Cluster OU' 'PASS' "$clusterOUPath"
} catch {
Write-Check 'Cluster OU' 'FAIL' "Not found: $clusterOUPath"
}
# CHECK: LCM User
Write-Output ""
Write-Output "Checking LCM user account..."
try {
`$user = Get-ADUser -Filter "SamAccountName -eq '$lcmSamAccount'" -Properties Enabled, DistinguishedName
if (`$user) {
Write-Check 'LCM user exists' 'PASS' "$lcmSamAccount (`$(`$user.DistinguishedName))"
if (`$user.Enabled) { Write-Check 'LCM user enabled' 'PASS' }
else { Write-Check 'LCM user enabled' 'FAIL' 'Account is disabled' }
} else { Write-Check 'LCM user exists' 'FAIL' "Not found: $lcmSamAccount" }
} catch { Write-Check 'LCM user exists' 'FAIL' "Query failed: `$_" }
# SUMMARY
Write-Output ""
Write-Output "========================================="
Write-Output "SUMMARY: Passed=`$passed Failed=`$failed Warnings=`$warned"
Write-Output "========================================="
if (`$failed -eq 0) { Write-Output '✓ AD configuration validation passed!' }
else { Write-Output "✗ `$failed check(s) failed. Review above and remediate." }
"@
# Verify Azure context
$ctx = Get-AzContext -ErrorAction Stop
if (-not $ctx) { Write-Host "ERROR: Run Connect-AzAccount first." -ForegroundColor Red; exit 1 }
# Verify VM is running
$vm = Get-AzVM -ResourceGroupName $ResourceGroupName -Name $VMName -Status -ErrorAction Stop
$powerState = ($vm.Statuses | Where-Object { $_.Code -like "PowerState/*" }).DisplayStatus
if ($powerState -ne 'VM running') { Write-Host "ERROR: VM is not running." -ForegroundColor Red; exit 1 }
# Execute remote script
Write-Host "Executing AD validation on $VMName..." -ForegroundColor Cyan
$result = Invoke-AzVMRunCommand `
-ResourceGroupName $ResourceGroupName `
-VMName $VMName `
-CommandId "RunPowerShellScript" `
-ScriptString $remoteScript `
-ErrorAction Stop
# Display output
$stdout = $result.Value | Where-Object { $_.Code -eq "ComponentStatus/StdOut/succeeded" }
$stderr = $result.Value | Where-Object { $_.Code -eq "ComponentStatus/StdErr/succeeded" }
if ($stdout.Message) { Write-Host $stdout.Message }
if ($stderr.Message) { Write-Host "--- Errors ---" -ForegroundColor Red; Write-Host $stderr.Message -ForegroundColor Red }
Tip: If empty output is returned, verify the VM name and resource group, and confirm the VM is running.
Validation Checklist
- AsHciADArtifactsPreCreationTool module installed (v2402+)
- KDS Root Key exists
- OU created with Block Inheritance enabled
- LCM user created in OU
- Credentials stored securely
Troubleshooting
| Issue | Cause | Resolution |
|---|---|---|
New-ADOrganizationalUnit fails with access denied | Insufficient permissions to create OUs | Run as Domain Admin or delegate OU creation rights on the parent container |
| KDS Root Key not effective immediately | Key needs 10-hour replication delay | Use -EffectiveImmediately in lab or wait 10 hours in production |
| AsHciADArtifactsPreCreationTool module not found | Module not installed or wrong PS version | Install via Install-Module AsHciADArtifactsPreCreationTool on PowerShell 5.1+ |
| LCM user creation fails with duplicate | Account already exists from prior attempt | Verify existing account properties match requirements or remove and recreate |
Next Steps
Proceed to Task 2 - Security Groups to create optional security groups.
Alternatives
The procedures in this task use the scripted methods shown in the tabs above. Additional deployment methods including Azure CLI and Bash scripts are available in the azurelocal-toolkit repository under scripts/deploy/.
| Method | Description |
|---|---|
| Azure CLI | PowerShell-based Azure CLI scripts for Azure resource operations |
| Bash | Linux/macOS compatible shell scripts for pipeline environments |
Navigation
| ← Phase 01: Active Directory | ↑ Part 3: On-Premises Readiness | Task 02: Security Groups → |
Version Control
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-31 | Azure Local Cloud | Initial document |
| 1.1 | 2026-03-03 | Azure Local Cloud | Standardized runbook format |
End of Task