Task 05: Configure DNS Servers
DOCUMENT CATEGORY: Runbook
SCOPE: DNS client configuration
PURPOSE: Set DNS server addresses on the management NIC of all nodes using explicit values fromvariables.yml— adapter resolved by exact name, not auto-detected MASTER REFERENCE: Phase 03: OS Configuration
Status: Active
Overview
Configure the primary and secondary DNS server addresses on the management network adapter of each Azure Local node. DNS must be set before Active Directory operations, Azure Arc registration, and any hostname resolution required later in the deployment.
Task 04 (disable DHCP) must be complete. Setting DNS before DHCP is disabled has no harmful effect, but the standard sequence is Tasks 03 → 04 → 05.
What the scripts do:
- Hard-fail on any
REPLACEplaceholder value remaining in#region CONFIGURATION - Find the management adapter by exact name — exit and list adapters if not found
- Check idempotency — exit clean if DNS is already set to the target values
- Call
Set-DnsClientServerAddresswith the two explicit DNS IPs - Read back and validate the DNS configuration
Prerequisites
| Requirement | Description | Source |
|---|---|---|
| Task 04 complete | DHCP disabled on all adapters | Task 04: Disable DHCP |
| DNS IPs confirmed | Validated DNS server addresses | variables.yml: dns.primary, dns.secondary |
| NIC name confirmed | Management adapter name | variables.yml: cluster.management_nic_name |
Configuration Reference
variables.yml path | Script variable | Example |
|---|---|---|
cluster.management_nic_name | $ManagementNIC | Embedded NIC 1 |
dns.primary | $DNSPrimary | 10.100.10.2 |
dns.secondary | $DNSSecondary | 10.100.10.3 |
nodes.<name>.management_ip | PSRemoting target (orchestrated only) | 10.100.200.11 |
Execution Options
- SConfig Utility
- Orchestrated Script
- Standalone Script
Configure DNS interactively on each node via console, KVM, or RDP.
SConfig requires repeating these steps on every node individually. For multi-node deployments, the Direct or Orchestrated tab is faster and less error-prone.
Steps — for each node:
- Open SConfig (if not already running, type
sconfigin PowerShell) - Select option
8— Network settings - Select the management adapter (the one configured with a static IP in Task 03)
- Select option
2— Set DNS Servers - Enter the Primary DNS IP from
variables.yml→dns.primary - Enter the Secondary DNS IP from
variables.yml→dns.secondary - Press Enter to confirm
Verify in SConfig:
After setting DNS, return to the Network settings menu and confirm the DNS servers shown
match the values from variables.yml.
Alternatively, verify in PowerShell:
# Run on the node after SConfig configuration
Get-DnsClientServerAddress -AddressFamily IPv4 |
Where-Object { $_.ServerAddresses.Count -gt 0 } |
Select-Object InterfaceAlias, ServerAddresses |
Format-Table -AutoSize
Run from the management server. Reads all DNS and NIC values from variables.yml — no manual variable editing required.
Toolkit script: scripts/deploy/04-cluster-deployment/phase-03-os-configuration/task-05-configure-dns-servers/powershell/Invoke-ConfigureDNS-Orchestrated.ps1
variables.yml values used:
| Path | Purpose |
|---|---|
cluster.management_nic_name | Which adapter to configure DNS on |
dns.primary | Primary DNS server IP |
dns.secondary | Secondary DNS server IP |
nodes.<name>.management_ip | PSRemoting connection target per node |
#Requires -Version 5.1
<#
.SYNOPSIS
Invoke-ConfigureDNS-Orchestrated.ps1
Configures DNS servers on the management NIC of every Azure Local node using PSRemoting.
.DESCRIPTION
Runs from the management server. Reads DNS and node IP values from variables.yml,
connects to each node over PSRemoting, and sets the DNS server addresses on the
management NIC.
variables.yml paths used:
nodes.<name>.management_ip - PSRemoting connection target
cluster.management_nic_name - Adapter name to configure DNS on
dns.primary - Primary DNS server IP
dns.secondary - Secondary DNS server IP
.NOTES
Author: Azure Local Cloud Azure Local Cloud
Version: 1.0.0
Phase: 03-os-configuration
Task: task-05-configure-dns-servers
Execution: Run from management server (PSRemoting outbound to nodes)
Prerequisites: PowerShell 5.1+, WinRM enabled on all nodes, admin credentials
Run after: Task 04 — DHCP disabled on all adapters
.EXAMPLE
.\Invoke-ConfigureDNS-Orchestrated.ps1
.\Invoke-ConfigureDNS-Orchestrated.ps1 -ConfigPath "C:\config\variables.yml"
#>
[CmdletBinding()]
param(
[string]$ConfigPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ============================================================================
# LOGGING
# ============================================================================
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$color = switch ($Level) {
"ERROR" { "Red" }
"WARN" { "Yellow" }
"SUCCESS" { "Green" }
"HEADER" { "Cyan" }
default { "White" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
# ============================================================================
# CONFIGURATION RESOLVER
# ============================================================================
function Resolve-ConfigPath {
[CmdletBinding()]
param([string]$Provided)
if ($Provided -and (Test-Path $Provided)) {
Write-Log "Config: $Provided"
return $Provided
}
$searchPaths = @(
(Join-Path $PSScriptRoot "..\..\..\..\configs"),
(Join-Path $PSScriptRoot "..\..\..\..\..\configs"),
"C:\configs",
"C:\AzureLocal\configs"
)
$found = @()
foreach ($dir in $searchPaths) {
if (Test-Path $dir) {
$found += Get-ChildItem -Path $dir -Filter "infrastructure*.yml" -File -ErrorAction SilentlyContinue
}
}
$found = @($found | Sort-Object FullName -Unique)
if ($found.Count -eq 0) {
Write-Log "No infrastructure*.yml found. Provide -ConfigPath manually." "ERROR"
throw "Config file not found."
}
if ($found.Count -eq 1) {
Write-Log "Config: $($found[0].FullName)"
return $found[0].FullName
}
Write-Log "Multiple config files found:" "WARN"
for ($i = 0; $i -lt $found.Count; $i++) {
Write-Host " [$($i+1)] $($found[$i].FullName)" -ForegroundColor Yellow
}
$choice = Read-Host "Select config [1-$($found.Count)]"
$idx = [int]$choice - 1
if ($idx -lt 0 -or $idx -ge $found.Count) { throw "Invalid selection." }
return $found[$idx].FullName
}
# ============================================================================
# YAML PARSER (PowerShell 5.1 compatible)
# ============================================================================
function Get-YamlValue {
param([string[]]$Lines, [string[]]$KeyPath)
$current = $Lines
foreach ($key in $KeyPath) {
$pattern = "^\s*${key}\s*:"
$lineIdx = -1
for ($i = 0; $i -lt $current.Count; $i++) {
if ($current[$i] -match $pattern) { $lineIdx = $i; break }
}
if ($lineIdx -eq -1) { return $null }
if ($current[$lineIdx] -match "^\s*${key}\s*:\s*(.+)$") {
$val = $Matches[1].Trim().Trim('"').Trim("'")
if ($KeyPath[-1] -eq $key) { return $val }
return $null
}
$keyIndent = ($current[$lineIdx] -replace "^(\s*).*", '$1').Length
$blockLines = @()
for ($j = $lineIdx + 1; $j -lt $current.Count; $j++) {
$line = $current[$j]
if ($line -match "^\s*$") { continue }
$thisIndent = ($line -replace "^(\s*).*", '$1').Length
if ($thisIndent -le $keyIndent) { break }
$blockLines += $line
}
$current = $blockLines
}
return $null
}
function Get-NodeNames {
param([string[]]$Lines)
$inNodes = $false
$names = @()
foreach ($line in $Lines) {
if ($line -match "^\s*nodes\s*:") { $inNodes = $true; continue }
if ($inNodes) {
if ($line -match "^\s*(\w[\w\-_]*):\s*$") { $names += $Matches[1] }
elseif ($line -match "^\S" -and $line -notmatch "^\s*#") { break }
}
}
return $names
}
# ============================================================================
# GET CLUSTER CONFIG
# ============================================================================
function Get-ClusterConfig {
[CmdletBinding()]
param([string]$ConfigPath)
$raw = Get-Content -Path $ConfigPath -Raw
$lines = $raw -split "`n"
# Cluster-level values
$mgmtNIC = Get-YamlValue -Lines $lines -KeyPath @("cluster", "management_nic_name")
if (-not $mgmtNIC) { throw "cluster.management_nic_name not found in $ConfigPath." }
$dnsPrimary = Get-YamlValue -Lines $lines -KeyPath @("dns", "primary")
$dnsSecondary = Get-YamlValue -Lines $lines -KeyPath @("dns", "secondary")
if (-not $dnsPrimary) { throw "dns.primary not found in $ConfigPath." }
if (-not $dnsSecondary) { throw "dns.secondary not found in $ConfigPath." }
Write-Log " management_nic_name : $($mgmtNIC.Trim())"
Write-Log " dns.primary : $($dnsPrimary.Trim())"
Write-Log " dns.secondary : $($dnsSecondary.Trim())"
# Per-node IPs
$nodeNames = Get-NodeNames -Lines $lines
if ($nodeNames.Count -eq 0) { throw "No nodes found in $ConfigPath." }
$nodes = @()
foreach ($nodeName in $nodeNames) {
$mgmtIP = Get-YamlValue -Lines $lines -KeyPath @("nodes", $nodeName, "management_ip")
if (-not $mgmtIP) {
Write-Log " WARN: nodes.$nodeName.management_ip not found — skipping" "WARN"
continue
}
$nodes += [PSCustomObject]@{
NodeName = $nodeName
IP = $mgmtIP.Trim()
}
Write-Log " Node: $nodeName IP: $($mgmtIP.Trim())"
}
if ($nodes.Count -eq 0) { throw "No valid node entries found." }
return [PSCustomObject]@{
ManagementNIC = $mgmtNIC.Trim()
DNSPrimary = $dnsPrimary.Trim()
DNSSecondary = $dnsSecondary.Trim()
Nodes = $nodes
}
}
# ============================================================================
# REMOTE SCRIPTBLOCK
# ============================================================================
$RemoteScriptBlock = {
param([string]$ManagementNIC, [string]$DNSPrimary, [string]$DNSSecondary)
Set-StrictMode -Version Latest
# Find adapter by exact name
$adapter = Get-NetAdapter | Where-Object { $_.Name -eq $ManagementNIC }
if (-not $adapter) {
$available = (Get-NetAdapter | Sort-Object Name | ForEach-Object { $_.Name }) -join ", "
return [PSCustomObject]@{
Result = "Error"
Detail = "Adapter '$ManagementNIC' not found. Available: $available"
DNSAfter = ""
}
}
# Idempotency check
$current = (Get-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
if ($current -and $current.Count -ge 2 -and $current[0] -eq $DNSPrimary -and $current[1] -eq $DNSSecondary) {
return [PSCustomObject]@{
Result = "AlreadyConfigured"
Detail = "DNS already set to $DNSPrimary, $DNSSecondary"
DNSAfter = "$DNSPrimary, $DNSSecondary"
}
}
# Set DNS
try {
Set-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex `
-ServerAddresses @($DNSPrimary, $DNSSecondary) -ErrorAction Stop
$after = (Get-DnsClientServerAddress -InterfaceIndex $adapter.ifIndex -AddressFamily IPv4 -ErrorAction SilentlyContinue).ServerAddresses
$ok = ($after -and $after.Count -ge 2 -and $after[0] -eq $DNSPrimary -and $after[1] -eq $DNSSecondary)
return [PSCustomObject]@{
Result = if ($ok) { "Changed" } else { "Failed" }
Detail = if ($ok) { "DNS set successfully" } else { "Post-set DNS mismatch: $($after -join ', ')" }
DNSAfter = $after -join ", "
}
} catch {
return [PSCustomObject]@{
Result = "Error"
Detail = $_.ToString()
DNSAfter = ""
}
}
}
# ============================================================================
# MAIN
# ============================================================================
try {
Write-Log "=== Invoke-ConfigureDNS-Orchestrated.ps1 ===" "HEADER"
$resolvedConfig = Resolve-ConfigPath -Provided $ConfigPath
Write-Log "Loading configuration from: $resolvedConfig"
$config = Get-ClusterConfig -ConfigPath $resolvedConfig
Write-Log "Nodes to process: $($config.Nodes.Count)"
$credential = Get-Credential -Message "Enter local admin credentials for Azure Local nodes"
$nodeResults = @()
foreach ($node in $config.Nodes) {
Write-Log "--- $($node.NodeName) ($($node.IP)) ---" "HEADER"
try {
$session = New-PSSession -ComputerName $node.IP -Credential $credential -ErrorAction Stop
Write-Log " Connected to $($node.IP)"
$result = Invoke-Command -Session $session -ScriptBlock $RemoteScriptBlock `
-ArgumentList $config.ManagementNIC, $config.DNSPrimary, $config.DNSSecondary
$level = switch ($result.Result) {
"Changed" { "SUCCESS" }
"AlreadyConfigured" { "SUCCESS" }
default { "WARN" }
}
Write-Log " $($node.NodeName): $($result.Result) — $($result.Detail)" $level
if ($result.DNSAfter) {
Write-Log " DNS after: $($result.DNSAfter)"
}
$nodeResults += [PSCustomObject]@{
Node = $node.NodeName
IP = $node.IP
Result = $result.Result
DNSAfter = $result.DNSAfter
Detail = $result.Detail
}
Remove-PSSession -Session $session -ErrorAction SilentlyContinue
} catch {
Write-Log " ERROR connecting to $($node.NodeName) ($($node.IP)): $_" "ERROR"
$nodeResults += [PSCustomObject]@{
Node = $node.NodeName
IP = $node.IP
Result = "ConnectionFailed"
DNSAfter = ""
Detail = $_.ToString()
}
}
}
Write-Log "=== ORCHESTRATION SUMMARY ===" "HEADER"
$nodeResults | Format-Table -AutoSize | Out-String | Write-Host
$failed = @($nodeResults | Where-Object { $_.Result -notin @("Changed","AlreadyConfigured") })
if ($failed.Count -gt 0) {
Write-Log "$($failed.Count) node(s) had issues. Review above." "WARN"
exit 1
}
Write-Log "All nodes: DNS configuration complete." "SUCCESS"
exit 0
} catch {
Write-Log "CRITICAL ERROR: $_" "ERROR"
exit 1
}
Expected output (excerpt):
[2026-03-05 09:22:00] [HEADER] === Invoke-ConfigureDNS-Orchestrated.ps1 ===
[2026-03-05 09:22:00] [INFO] Loading configuration from: C:\config\variables.yml
[2026-03-05 09:22:00] [INFO] management_nic_name : Embedded NIC 1
[2026-03-05 09:22:00] [INFO] dns.primary : 10.100.10.2
[2026-03-05 09:22:00] [INFO] dns.secondary : 10.100.10.3
[2026-03-05 09:22:00] [INFO] Node: node01 IP: 10.100.200.11
[2026-03-05 09:22:00] [INFO] Node: node02 IP: 10.100.200.12
[2026-03-05 09:22:00] [INFO] Nodes to process: 2
[2026-03-05 09:22:01] [HEADER] --- node01 (10.100.200.11) ---
[2026-03-05 09:22:01] [INFO] Connected to 10.100.200.11
[2026-03-05 09:22:02] [SUCCESS] node01: Changed — DNS set successfully
[2026-03-05 09:22:02] [INFO] DNS after: 10.100.10.2, 10.100.10.3
[2026-03-05 09:22:03] [HEADER] === ORCHESTRATION SUMMARY ===
Node IP Result DNSAfter Detail
---- -- ------ -------- ------
node01 10.100.200.11 Changed 10.100.10.2, 10.100.10.3 DNS set successfully
node02 10.100.200.12 Changed 10.100.10.2, 10.100.10.3 DNS set successfully
[2026-03-05 09:22:10] [SUCCESS] All nodes: DNS configuration complete.
When to use: Use this option for a self-contained deployment without a shared configuration file.
Script: See azurelocal-toolkit for the standalone script for this task.
Standalone script content references the toolkit repository. See the Orchestrated Script tab for the primary implementation.
Validation
Verify DNS server configuration on each node:
# Run on each node (or via PSRemoting)
Get-DnsClientServerAddress -AddressFamily IPv4 |
Where-Object { $_.ServerAddresses.Count -gt 0 } |
Select-Object InterfaceAlias, ServerAddresses |
Format-Table -AutoSize
Validation checklist:
| Check | Expected | Status |
|---|---|---|
| Primary DNS on management NIC | dns.primary from variables.yml | ☐ |
| Secondary DNS on management NIC | dns.secondary from variables.yml | ☐ |
| No other DNS entries on management NIC | Only 2 servers listed | ☐ |
Task 06 performs a full DNS resolution validation. Basic resolution testing at this stage is optional — proceed to Task 06 for a comprehensive verification.
Variables from variables.yml
| Variable | Config Path | Example |
|---|---|---|
| Primary DNS | cluster.network.dns.primary | 10.0.0.5 |
| Secondary DNS | cluster.network.dns.secondary | 10.0.0.6 |
| DNS Suffix | cluster.network.dns.suffix | contoso.local |
Troubleshooting
| Issue | Root Cause | Remediation |
|---|---|---|
| Script hard-fails on startup | REPLACE placeholder values remain | Edit #region CONFIGURATION with real values from variables.yml |
Adapter not found error | NIC name in config doesn't match | Run Get-NetAdapter on the node; update cluster.management_nic_name in variables.yml |
| DNS validation fails (post-set mismatch) | Transient WMI delay | Wait 5 seconds and re-run the validation query manually |
| DNS not resolving names (verified in Task 06) | DNS servers unreachable | Verify DNS server IPs are reachable from node; check routing and firewall |
Orchestrated script: cluster.management_nic_name not found | Key missing from variables.yml | Add cluster.management_nic_name to the cluster: block in yml |
Orchestrated script: dns.primary not found | Key missing from variables.yml | Add dns.primary and dns.secondary to the dns: block in yml |
| Node unreachable via PSRemoting | WinRM not configured or firewall blocking | Verify Task 01 (WinRM) completed; check firewall rules |
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
| ← Task 04: Disable DHCP | ↑ Phase 03: OS Configuration | Task 06: Verify DNS → |
Version Control
| Version | Date | Author | Changes |
|---|---|---|---|
| 1.0 | 2026-01-31 | Azure Local Cloud | Initial document |
| 2.0 | 2026-03-05 | Azure Local Cloud | Full rewrite to standards — complete frontmatter, 3-tab structure (SConfig, Direct, Orchestrated), Standalone tab removed, full embedded scripts, explicit NIC name from variables.yml, variables.yml integration in orchestrated script |