|
#region Functions |
|
|
|
# Configuration values |
|
$synchronizeTypes = "A", "CNAME" |
|
$metadataSyncPropertyName = "recordSource" |
|
|
|
function Get-DnsZoneInfo { |
|
[CmdletBinding()] |
|
param( |
|
[Parameter(Mandatory = $true)] |
|
[string]$ResourceGroupName, |
|
[Parameter(Mandatory = $true)] |
|
[string]$ZoneName, |
|
[string]$SubscriptionId |
|
) |
|
|
|
process { |
|
Write-Verbose "Loading zone $ZoneName in resource group $ResourceGroupName" |
|
|
|
if($SubscriptionId) { |
|
$currentContext = Get-AzContext |
|
if($currentContext.Subscription.Id -ne $SubscriptionId) { |
|
Write-Verbose "Switching context from $($currentContext.Subscription.Id) to $SubscriptionId" |
|
Set-AzContext -Subscription $SubscriptionId | Out-Null |
|
} |
|
} |
|
|
|
$zone = Get-AzPrivateDnsZone -ResourceGroupName $ResourceGroupName -Name $ZoneName -ErrorAction SilentlyContinue |
|
if(-not $zone) { |
|
return |
|
} |
|
$zoneInfo = [PSCustomObject]@{ |
|
Zone = $zone |
|
Records = Get-AzPrivateDnsRecordSet -Zone $zone |
|
RecordsToSync = @() |
|
RecordsSyncedFromOther = @() |
|
RecordsSkipped = @() |
|
} |
|
|
|
foreach($record in $zoneInfo.Records) { |
|
if($record.RecordType -notin $synchronizeTypes) { |
|
Write-Verbose " - skipping record '$($record.Name)' of type $($record.RecordType) as is not in allowed record types." |
|
$zoneInfo.RecordsSkipped += $record |
|
continue |
|
} |
|
|
|
if($record.Metadata -and $record.Metadata.ContainsKey($metadataSyncPropertyName)) { |
|
$zoneInfo.RecordsSyncedFromOther += $record |
|
} else { |
|
$zoneInfo.RecordsToSync += $record |
|
} |
|
} |
|
|
|
Write-Verbose "Records to synchronize: $(($zoneInfo.RecordsToSync | Measure-Object).Count)" |
|
Write-Verbose "Records synchronized from other zones: $(($zoneInfo.RecordsSyncedFromOther | Measure-Object).Count)" |
|
Write-Verbose "Records skipped: $(($zoneInfo.RecordsSkipped | Measure-Object).Count)" |
|
|
|
# Return back to original context if needed |
|
if($SubscriptionId -and $currentContext.Subscription.Id -ne $SubscriptionId) { |
|
Write-Verbose "Switching context back to $($currentContext.Subscription.Id)" |
|
Set-AzContext -Context $currentContext | Out-Null |
|
} |
|
|
|
$zoneInfo |
|
} |
|
} |
|
|
|
function Sync-DnsZone { |
|
[CmdletBinding()] |
|
param( |
|
[string]$SourceSubscriptionId, |
|
[Parameter(Mandatory = $true)] |
|
[string]$SourceResourceGroupName, |
|
[Parameter(Mandatory = $true)] |
|
[string]$SourceZoneName, |
|
|
|
[string]$DestinationSubscriptionId, |
|
[Parameter(Mandatory = $true)] |
|
[string]$DestinationResourceGroupName, |
|
[Parameter(Mandatory = $true)] |
|
[string]$DestinationZoneName |
|
) |
|
|
|
process { |
|
$sourceZone = Get-DnsZoneInfo -SubscriptionId $sourceSubscriptionId -ResourceGroupName $sourceResourceGroupName -ZoneName $sourceZoneName |
|
if(-not $sourceZone) { |
|
throw "Source zone $sourceZoneName in RG $SourceResourceGroupName not found" |
|
} |
|
|
|
$destinationZone = Get-DnsZoneInfo -SubscriptionId $destinationSubscriptionId -ResourceGroupName $destinationResourceGroupName -ZoneName $destinationZoneName |
|
if(-not $destinationZone) { |
|
throw "Source zone $destinationZoneName in RG $destinationResourceGroupName not found" |
|
} |
|
|
|
$recordsVerifiedInDestination = @() |
|
$recordsToCreateInDestination = @() |
|
$recordsToUpdateInDestination = @() |
|
foreach($record in $sourceZone.RecordsToSync) { |
|
$recordInDestination = $destinationZone.Records | Where-Object { $_.Name -eq $record.Name } |
|
if(-not $recordInDestination) { |
|
Write-Verbose " [+] record $($record.Name) is missing in destination zone" |
|
|
|
$recordsToCreateInDestination += $record |
|
} elseif (-not $recordInDestination.Metadata -or -not $recordInDestination.Metadata.ContainsKey($metadataSyncPropertyName)) { |
|
Write-Warning " [!] record $($record.Name) already exists in destination zone but is not originating from this source zone -> skipping" |
|
} elseif($recordInDestination.Metadata -and $recordInDestination.Metadata.ContainsKey($metadataSyncPropertyName) -and $recordInDestination.Metadata[$metadataSyncPropertyName] -eq $sourceZone.Zone.ResourceId) { |
|
# record already exists and is originating from this source zone |
|
$sourceRecords = [array]($record.Records | Select-Object -ExpandProperty Ipv4Address) |
|
$destRecords = [array]($recordInDestination.Records | Select-Object -ExpandProperty Ipv4Address) |
|
|
|
# compare if IPs match |
|
$result = Compare-Object -ReferenceObject @($sourceRecords | Select-Object) -DifferenceObject @($destRecords | Select-Object) |
|
if($result) { |
|
Write-Verbose " [*] values in existing record $($record.Name) has changed, record should be updated" |
|
$recordsToUpdateInDestination += @{ SourceRecord = $record; DestinationRecord = $recordInDestination } |
|
} else { |
|
Write-Verbose " [✔] record $($record.Name) is already synchronized" |
|
} |
|
|
|
$recordsVerifiedInDestination += $recordInDestination |
|
} else { |
|
Write-Verbose " [?] unexpected state of the record $($record.Name)" |
|
} |
|
} |
|
|
|
$areChangesInDestinationPending = $recordsVerifiedInDestination.Count -gt 0 -or $recordsToCreateInDestination.Count -gt 0 -or $recordsToUpdateInDestination.Count -gt 0 |
|
# switch to destination subscription if are any pending changes on the destination |
|
if($areChangesInDestinationPending -and $DestinationSubscriptionId) { |
|
$currentContext = Get-AzContext |
|
if($currentContext.Subscription.Id -ne $DestinationSubscriptionId) { |
|
Write-Verbose "Switching context from $($currentContext.Subscription.Id) to $DestinationSubscriptionId" |
|
Set-AzContext -Subscription $DestinationSubscriptionId | Out-Null |
|
} |
|
} |
|
|
|
foreach($record in $recordsToCreateInDestination) { |
|
Write-Verbose " [+] creating a new record $($record.Name) in destination zone" |
|
$metadata = @{ |
|
$metadataSyncPropertyName = $sourceZone.Zone.ResourceId |
|
} |
|
|
|
New-AzPrivateDnsRecordSet -Name $record.Name -RecordType $record.RecordType -Zone $destinationZone.Zone -Ttl $record.Ttl -PrivateDnsRecord $record.Records -Metadata $metadata | Out-Null |
|
} |
|
|
|
foreach($record in $recordsToUpdateInDestination) { |
|
Write-Verbose " [*] updating a record set for $($record.Name) in destination zone" |
|
|
|
for($i = 0; $i -lt $record.DestinationRecord.Records.Count; $i++) { |
|
Remove-AzPrivateDnsRecordConfig -RecordSet $record.DestinationRecord -Ipv4Address $recordInDestination.Records[$i].Ipv4Address | Out-Null |
|
} |
|
|
|
foreach($config in $record.SourceRecord.Records) { |
|
Add-AzPrivateDnsRecordConfig -RecordSet $record.DestinationRecord -Ipv4Address $config.Ipv4Address | Out-Null |
|
} |
|
|
|
Set-AzPrivateDnsRecordSet -RecordSet $record.DestinationRecord | Out-Null |
|
} |
|
|
|
$recordsVerifiedInDestinationNames = $recordsVerifiedInDestination | Select-Object -ExpandProperty Name |
|
foreach($record in $destinationZone.RecordsSyncedFromOther) { |
|
if(-not $record.Metadata -or -not $record.Metadata.ContainsKey($metadataSyncPropertyName) -or $record.Metadata[$metadataSyncPropertyName] -ne $sourceZone.Zone.ResourceId) { |
|
continue # we are interested only in records with metadata pointing to source zone |
|
} |
|
|
|
if($record.Name -notin $recordsVerifiedInDestinationNames) { |
|
Write-Verbose " [-] removing record $($record.Name) from destination as was removed in source" |
|
Remove-AzPrivateDnsRecordSet -RecordSet $record |
|
} |
|
} |
|
|
|
# Return back to original context if needed |
|
if($areChangesInDestinationPending -and $DestinationSubscriptionId -and $currentContext.Subscription.Id -ne $SubscriptionId) { |
|
Write-Verbose "Switching context back to $($currentContext.Subscription.Id)" |
|
Set-AzContext -Context $currentContext | Out-Null |
|
} |
|
} |
|
} |
|
#endregion |