【PowerShell】DNSにホストを新規追加するスクリプト

WindowsServerのDNSに新規レコードを追加するPowerShellスクリプトです。

一括登録の際、手動だと手間が掛かるので作成しました。
コマンドには基本的にコメントが付いていますが、詳細は後述しています。

## 実行環境
```
OS: Windows Server 2019
PSVersion: PowerShell 5.1
```
## フロー図
![flow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2088592/10b09027-b582-3901-0e79-1b02e12295da.png)

## 仕様
- CSVに「ホスト名、IPアドレス、サブネット（プレフィックス表記）」を記載し、ps1を実行することで指定のサーバーのDNSに追加する
- CSVの行分だけDNSレコードの追加処理が実行される
## スクリプト
```powershell:addDnsRecord.ps1
#管理者権限に昇格
if (!([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole("Administrators")) { Start-Process powershell.exe "-File `"$PSCommandPath`"" -Verb RunAs; exit }
#################### 環境設定 ####################
#サーバー名
$computerName = "******"
#ドメイン名
$domain = "******"
#CSVのパス
$csvPath = "C:\******\addDns.csv"
#日付、時間（ログ用）
$newFolder = (Get-Date).ToString("yyyyMMdd")
$now = (Get-Date).ToString("HHmmss")
#ログの保存先を指定
$logTarget = "C:\******\" + $newFolder + "\addDns" + $now + ".log"

#################### function ####################

#正引きのホスト名重複があるか確認
function checkHostName() {
    #ホスト名が$hostNameであるレコードを検索し配列として格納
    $aRecord = @(Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName | where-object {$_.hostName -eq $hostName})

    #ホスト名重複があった場合重複しているレコードを正・逆とも削除
    write-host "重複しているホスト名を検索します"

    if($aRecord) {
        #重複あり時の処理
        #正引き・逆引きのレコード削除
        write-host "重複しているホスト名が"$aRecord.Count"個あるため削除します"
        for($i=0;$i -lt $aRecord.Count;$i++) {
            
            Remove-DnsServerResourceRecord -ComputerName $computerName -ZoneName $domain -RRType A -Name $aRecord[$i].HostName -RecordData $aRecord[$i].RecordData.IPv4Address.IPAddressToString -Force

            write-host "ホスト名:" $aRecord[0].HostName "IPアドレス:" $aRecord[0].RecordData.IPv4Address.IPAddressToString "のレコードを削除しました"
        }
    } else {
        #重複なし時の処理
        write-host "重複しているホスト名はありませんでした"
    }
}

#逆引きゾーンがあるか確認
function checkReverceRecordZone() {
    #reverseZoneを初期化
    $reverseZone = $null;
    Clear-Variable -Name "reverseZone"
    #エラーを非表示にに変更（逆引きゾーンを取得できない時にエラーが出るため）
    $ErrorActionPreference = "silentlycontinue"

    $reverseZone = Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone
    write-host "逆引きゾーン $rtpZone があるか確認します"
    #逆引きゾーンが空の場合の処理
    if($reverseZone -eq $null) {
        #ない時の処理
        #逆引きゾーンを追加
        try {
            Add-DnsServerPrimaryZone -ComputerName $computerName -NetworkID $rtpZoneFull -ReplicationScope "Forest"
        sleep 3
            write-host "下記のとおり逆引きゾーン $rtpZone を作成しました"
            Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone
        } catch {
            write-host "エラー：逆引きゾーン作成に失敗しました"
            $ErrorMessage = $_.Exception.Message
            Write-Host "エラーメッセージ : $ErrorMessage 以降の処理を中断します"
            write-host "CSVの $index 行目、ホスト名：$hostName IPアドレス：$ipAdress のレコード追加に失敗しています。ログを確認して作成処理を再度実行してください。"
            #失敗した場合にループ処理をスキップ
            continue
        }
    } else {
        #ある時の処理
        write-host "逆引きゾーン $rtpZone は既に存在します"
    }

    #エラーを表示に変更
    $ErrorActionPreference = "continue"
}

#正引き・逆引きレコード追加
function addRecord() {
    write-host "正引き：ホスト名「$hostName」IPアドレス「$ipAdress」のレコードを追加します"
    write-host "逆引き：ゾーン名「$rtpZoneFull」にレコードを追加します"
    try {
        #正引き・逆引きレコード追加
        Add-DNSServerResourceRecordA -ComputerName $computerName -ZoneName $domain -Name $hostName -IPv4Address $ipAdress -CreatePtr
    sleep 3
        #追加されたレコードの確認・表示
        #作成した正引きレコードの名前を取得
        $addRecordName = Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName | Foreach-object {$_.HostName}
        #作成した正引きレコードのIPを取得
        $addRecordIp = Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName | Foreach-object {($_.RecordData).IPv4Address.IPAddressToString}
        #正引きレコードの名前・IPがそれぞれ正しいか判定
        if(($addRecordName) -eq $hostName -And ($addRecordIp -eq $ipAdress)) {
            write-host "正引きレコードは下記の通り正しく作成されています"
            #正引きレコード表示
            Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName -RRType "A"
        } else {
            write-host "エラー：正引きレコードが正しく追加できませんでした。内容を確認してください。"
        }
        #作成した逆引きレコードの名前を取得
        $reverseRecordData = Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone -Name $reverseName | Foreach-object {($_.RecordData).PtrDomainName} | Foreach-Object { $_ -replace ".$($domain).","" }
        #作成した逆引きレコードの名前が正しいか判定
        if($reverseRecordData -eq $hostName) {
            write-host "逆引きレコードは下記の通り正しく作成されています"
            #逆引きレコード表示
            Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone -Name $reverseName
        } else {
            write-host "エラー：逆引きレコードが正しく追加できませんでした。内容を確認してください。"
        }

        write-host "正引き・逆引きともレコードを追加しました"
    } catch {
        write-host "エラー：正引き又は逆引きゾーンの作成に失敗しました"
        $ErrorMessage = $_.Exception.Message
        Write-Host "エラーメッセージ : $ErrorMessage"
    }
}

#################### CSV読み込み、ホスト名、IP、プレフィックスの正規表現チェック ####################
#CSV読み込み
$csv = Import-Csv -path $csvPath -Encoding UTF8
#CSVのindex
$index = 0;
$csv | Foreach-Object {
    $hostName = $_.hostName
    $ipAdress = $_.ipAdress
    $prefix = $_.prefix
    $index ++;

    write-host "------------------------- $index 行目 -------------------------"
    if ($ipAdress -match "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$") {
        Write-Host "IP : $ipAdress"
    } else {
        Write-Host "$index 行目のIPアドレス $ipAdress が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のIPアドレス $ipAdress が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    if ($hostName  -match "^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]$") {
        Write-Host "host : $hostName"
    } else {
        Write-Host "$index 行目のホスト名 $hostName が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のホスト名 $hostName が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    if ($prefix  -match "^[0-9]$|^[1-2][0-9]$|^3[0-2]$") {
        Write-Host "prefix : /$prefix"
    } else {
        Write-Host "$index 行目のプレフィックス $prefix が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のプレフィックス $prefix が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    write-host "$index 行目のフォーマットは問題ありません"
}

#################### 処理開始 ####################

#ログ開始
Start-Transcript -Path $logTarget -NoClobber
#CSVの値を出力
$csv | Format-Table
write-host "--------------------------------------------------------------"
$index = 0;
#CSV１行ごとに関数を実行
$csv | Foreach-Object {
    $hostName = $_.hostName
    $ipAdress = $_.ipAdress
    $prefix = $_.prefix

    $index ++;
    ############# CSVの値から逆引き関連を定義 #############
    # IPから逆引き用のゾーンを抽出
    $extraction = [regex]::Matches($ipAdress, "[0-9]*\.")
    $extractionJoin = $extraction.Value[2] +$extraction.Value[1] + $extraction.Value[0]
    $rtpAdress = $extractionJoin.TrimEnd(".")
    #逆引きゾーン名
    $rtpZone = $rtpAdress + ".in-addr.arpa"
    #逆引きゾーン
    $rtpZoneFull = $ipAdress + "/" + $prefix
    #逆引きレコード名
    $reverseName = [regex]::Replace($ipAdress,"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}","")

    ############# functionの実行 #############
    #正引きのホスト名重複があるか確認
    checkHostName
    #逆引きゾーンがあるか確認
    checkReverceRecordZone
    #正引き・逆引きレコード追加
    addRecord
    write-host "--------------------------------------------------------------"
    sleep 3
}
#ログ終了
Stop-Transcript

```
## スクリプト説明
### 環境設定
```powershell
#サーバー名
$computerName = "******"
#ドメイン名
$domain = "******"
#CSVのパス
$csvPath = "C:\******\addDns.csv"
#日付、時間（ログ用）
$newFolder = (Get-Date).ToString("yyyyMMdd")
$now = (Get-Date).ToString("HHmmss")
#ログの保存先を指定
$logTarget = "C:\******\" + $newFolder + "\addDns" + $now + ".log"
```
- $computerName
DNSを登録するサーバーの名前を入れてください
- ログ設定
1日に複数回スクリプトを実行することを想定して、ログファイル名にスクリプト実行時間を入れるようにしています
### Function
#### checkHostName
正引きホストに重複があってもDNSレコードは追加できてしまうので、事前に重複があるか確認します
```powershell
#正引きのホスト名重複があるか確認
function checkHostName() {
    #ホスト名が$hostNameであるレコードを検索し配列として格納
    $aRecord = @(Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName | where-object {$_.hostName -eq $hostName})

    #ホスト名重複があった場合重複しているレコードを正・逆とも削除
    write-host "重複しているホスト名を検索します"

    if($aRecord) {
        #重複あり時の処理
        #正引き・逆引きのレコード削除
        write-host "重複しているホスト名が"$aRecord.Count"個あるため削除します"
        for($i=0;$i -lt $aRecord.Count;$i++) {
            
            Remove-DnsServerResourceRecord -ComputerName $computerName -ZoneName $domain -RRType A -Name $aRecord[$i].HostName -RecordData $aRecord[$i].RecordData.IPv4Address.IPAddressToString -Force

            write-host "ホスト名:" $aRecord[0].HostName "IPアドレス:" $aRecord[0].RecordData.IPv4Address.IPAddressToString "のレコードを削除しました"
        }
    } else {
        #重複なし時の処理
        write-host "重複しているホスト名はありませんでした"
    }
}
```
- $aRecord
今回追加するホスト名と同じ名前のレコードがDNSサーバー、ドメイン名にある場合、変数に格納する
- if文で$aRecordの真偽で処理を変えています
- 重複がある場合
重複登録されているレコードを削除します。この場合、更新のイメージで追加していると思うので削除処理としています。
- 重複がない場合
何もしない
#### checkReverceRecordZone
逆引きゾーンが作成されていないと逆引きレコードが作成できないので確認し、なければ新規追加します
```powershell
#逆引きゾーンがあるか確認
function checkReverceRecordZone() {
    #reverseZoneを初期化
    $reverseZone = $null;
    Clear-Variable -Name "reverseZone"
    #エラーを非表示にに変更（逆引きゾーンを取得できない時にエラーが出るため）
    $ErrorActionPreference = "silentlycontinue"

    $reverseZone = Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone
    write-host "逆引きゾーン $rtpZone があるか確認します"
    #逆引きゾーンが空の場合の処理
    if($reverseZone -eq $null) {
        #ない時の処理
        #逆引きゾーンを追加
        try {
            Add-DnsServerPrimaryZone -ComputerName $computerName -NetworkID $rtpZoneFull -ReplicationScope "Forest"
        sleep 3
            write-host "下記のとおり逆引きゾーン $rtpZone を作成しました"
            Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone
        } catch {
            write-host "エラー：逆引きゾーン作成に失敗しました"
            $ErrorMessage = $_.Exception.Message
            Write-Host "エラーメッセージ : $ErrorMessage 以降の処理を中断します"
            write-host "CSVの $index 行目、ホスト名：$hostName IPアドレス：$ipAdress のレコード追加に失敗しています。ログを確認して作成処理を再度実行してください。"
            #失敗した場合にループ処理をスキップ
            continue
        }
    } else {
        #ある時の処理
        write-host "逆引きゾーン $rtpZone は既に存在します"
    }

    #エラーを表示に変更
    $ErrorActionPreference = "continue"
}
```
- $reverseZone
今回追加するレコードに対応する逆引きゾーンがあれば変数に格納する
- $ErrorActionPreference = "silentlycontinue"
reverseZoneがnullの際、Get-DnsServerResourceRecordコマンドでエラーが出力されるがログに表示したくないので、このコマンドでエラー出力を非表示にします
- $reverseZoneがnullの時
逆引きゾーンの作成
- $reverseZoneがnullでない時
何もしない
#### addRecord
正引き・逆引きレコードを作成します
```powershell
#正引き・逆引きレコード追加
function addRecord() {
    write-host "正引き：ホスト名「$hostName」IPアドレス「$ipAdress」のレコードを追加します"
    write-host "逆引き：ゾーン名「$rtpZoneFull」にレコードを追加します"
    try {
        #正引き・逆引きレコード追加
        Add-DNSServerResourceRecordA -ComputerName $computerName -ZoneName $domain -Name $hostName -IPv4Address $ipAdress -CreatePtr
    sleep 3
        #追加されたレコードの確認・表示
        #作成した正引きレコードの名前を取得
        $addRecordName = Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName | Foreach-object {$_.HostName}
        #作成した正引きレコードのIPを取得
        $addRecordIp = Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName | Foreach-object {($_.RecordData).IPv4Address.IPAddressToString}
        #正引きレコードの名前・IPがそれぞれ正しいか判定
        if(($addRecordName) -eq $hostName -And ($addRecordIp -eq $ipAdress)) {
            write-host "正引きレコードは下記の通り正しく作成されています"
            #正引きレコード表示
            Get-DnsServerResourceRecord -ZoneName $domain -ComputerName $computerName -Name $hostName -RRType "A"
        } else {
            write-host "エラー：正引きレコードが正しく追加できませんでした。内容を確認してください。"
        }
        #作成した逆引きレコードの名前を取得
        $reverseRecordData = Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone -Name $reverseName | Foreach-object {($_.RecordData).PtrDomainName} | Foreach-Object { $_ -replace ".$($domain).","" }
        #作成した逆引きレコードの名前が正しいか判定
        if($reverseRecordData -eq $hostName) {
            write-host "逆引きレコードは下記の通り正しく作成されています"
            #逆引きレコード表示
            Get-DnsServerResourceRecord -ComputerName $computerName -ZoneName $rtpZone -Name $reverseName
        } else {
            write-host "エラー：逆引きレコードが正しく追加できませんでした。内容を確認してください。"
        }

        write-host "正引き・逆引きともレコードを追加しました"
    } catch {
        write-host "エラー：正引き又は逆引きゾーンの作成に失敗しました"
        $ErrorMessage = $_.Exception.Message
        Write-Host "エラーメッセージ : $ErrorMessage"
    }
}
```
- Add-DNSServerResourceRecordA
指定した正引き・逆引きレコードを追加します
- Get-DnsServerResourceRecord
Addした正引きレコードが正しく作成されている場合、表示させます
- Get-DnsServerResourceRecord
逆引きレコードも同様に表示して確認します
### CSV読み込み、ホスト名、IP、プレフィックスの正規表現チェック
CSVに必要事項を記載し、ps1を実行する際に記載事項がフォーマットに合っているかチェックし、間違っている場合にポップアップを表示してスクリプトを強制終了させる
```powershell
#CSV読み込み
$csv = Import-Csv -path $csvPath -Encoding UTF8
#CSVのindex
$index = 0;
$csv | Foreach-Object {
    $hostName = $_.hostName
    $ipAdress = $_.ipAdress
    $prefix = $_.prefix
    $index ++;

    write-host "------------------------- $index 行目 -------------------------"
    if ($ipAdress -match "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$") {
        Write-Host "IP : $ipAdress"
    } else {
        Write-Host "$index 行目のIPアドレス $ipAdress が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のIPアドレス $ipAdress が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    if ($hostName  -match "^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9]$") {
        Write-Host "host : $hostName"
    } else {
        Write-Host "$index 行目のホスト名 $hostName が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のホスト名 $hostName が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    if ($prefix  -match "^[0-9]$|^[1-2][0-9]$|^3[0-2]$") {
        Write-Host "prefix : /$prefix"
    } else {
        Write-Host "$index 行目のプレフィックス $prefix が間違っています。修正してください"
        $wsobj = new-object -comobject wscript.shell
        $result = $wsobj.popup("CSV $index 行目のプレフィックス $prefix が間違っています。修正してください",0,"CSVフォーマットエラー",16)
        continue
    }

    write-host "$index 行目のフォーマットは問題ありません"
}
```
- IPアドレス、ホスト名、プレフィックスの正規表現チェック
IPアドレス・・・「０〜255.」を3回繰り返し、最後が「０〜255」で終わっている数値にmatchしているか
ホスト名・・・「英数字（半角・全角）」＋「英数字（半角・全角）or-（ハイフン）」を６１文字以内＋「英数字（半角・全角）」の合計６３文字以内にmatchするか
サブネット（プレフィックス）・・・０〜３２の数値にmatchするか
上記どれかにmatchしない場合はポップアップ警告を表示してスクリプト処理自体を終了させます
### 処理開始〜終了
CSVの正規表現チェックが問題なければ関数を走らせ処理を開始します
```powershell
#ログ開始
Start-Transcript -Path $logTarget -NoClobber
#CSVの値を出力
$csv | Format-Table
write-host "--------------------------------------------------------------"
$index = 0;
#CSV１行ごとに関数を実行
$csv | Foreach-Object {
    $hostName = $_.hostName
    $ipAdress = $_.ipAdress
    $prefix = $_.prefix

    $index ++;
    ############# CSVの値から逆引き関連を定義 #############
    # IPから逆引き用のゾーンを抽出
    $extraction = [regex]::Matches($ipAdress, "[0-9]*\.")
    $extractionJoin = $extraction.Value[2] +$extraction.Value[1] + $extraction.Value[0]
    $rtpAdress = $extractionJoin.TrimEnd(".")
    #逆引きゾーン名
    $rtpZone = $rtpAdress + ".in-addr.arpa"
    #逆引きゾーン
    $rtpZoneFull = $ipAdress + "/" + $prefix
    #逆引きレコード名
    $reverseName = [regex]::Replace($ipAdress,"^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}","")

    ############# functionの実行 #############
    #正引きのホスト名重複があるか確認
    checkHostName
    #逆引きゾーンがあるか確認
    checkReverceRecordZone
    #正引き・逆引きレコード追加
    addRecord
    write-host "--------------------------------------------------------------"
    sleep 3
}
#ログ終了
Stop-Transcript
```
- 逆引き指定関連
ユーザー（追加するためにCSV追記する人）の負担軽減のために、追加するホストのIPアドレスから逆引きゾーン、逆引きレコードの変数を文字列操作を駆使して用意します
- 関数の実行
checkHostName、checkReverceRecordZone、addRecordを順番に走査させ、CSVの行分だけループ処理してDNSレコードを追加していきます
## おわりに
- DNSは何となく分かっていたが、各レコードや何が登録されているか等知るきっかけになってよかった
- なるべくユーザーの負担を減らすようなスクリプトをがしがし作っていきたい