どうも、やまもとやまです。
権威DNSサーバーの実装には有名どころでもいくつかあります。
BIND、djbdns、NSD、PowerDNS等、、
実装によりゾーンデータの保存方法や利用できるバックエンドも異なります。
今回はEOLになったCentOS7からAlmaLinux9へサーバーをリプレイスし、DNSサーバーソフトウェアも更改するという体で、BINDからPowerDNSへのゾーン情報移行を試してみます。
環境
以下の環境を想定します。
※いずれもプライマリ(マスター)サーバーの想定です
○移行元
CentOS7にBINDを導入し、ゾーンファイルを作成した環境
ホスト名:bindtest.future.local
○移行先
AlmaLinux9にPowerDNSを導入した環境
ホスト名:pdnstest.future.local
移行元環境を準備する
では早速仮想の移行元環境を準備しましょう。
CentOS7を最小インストールした状態から、BINDを導入します。
# yum install bind
テスト用のゾーンファイルを作成します。
example.comとexample.co.jpの2ゾーンを管理しているとします。
# vi /var/named/example.com.zone
----------------------------------------------------------------------
$ORIGIN .
$TTL 600 ; 10 minutes
example.com IN SOA bindtest.future.local. admin.bindtest.future.local. (
2025082901 ; serial
7200 ; refresh (2 hours)
3600 ; retry (1 hour)
604800 ; expire (1 week)
3600 ; minimum (1 hour)
)
NS dns001.future.local.
NS dns002.future.local.
A 192.0.2.100
MX 10 mail.example.com.
$ORIGIN example.com.
mail A 192.0.2.100
www CNAME example.com.
----------------------------------------------------------------------
# chown named:named /var/named/example.com.zone
# vi /var/named/example.co.jp.zone
----------------------------------------------------------------------
$ORIGIN .
$TTL 600 ; 10 minutes
example.co.jp IN SOA bindtest.future.local. admin.bindtest.future.local. (
2025082901 ; serial
7200 ; refresh (2 hours)
3600 ; retry (1 hour)
604800 ; expire (1 week)
3600 ; minimum (1 hour)
)
NS dns001.future.local.
NS dns002.future.local.
A 192.0.2.100
MX 10 mail.example.co.jp.
$ORIGIN example.co.jp.
mail A 192.0.2.100
www CNAME example.co.jp.
----------------------------------------------------------------------
# chown named:named /var/named/example.co.jp.zone
とてもシンプルなゾーンですがとりあえずはこれで!
では、named.confにマスターゾーンとして追加します。
# vi /etc/named.conf
----------------------------------------------------------------------
zone "example.com" IN {
type master;
file "example.com.zone";
};
zone "example.co.jp" IN {
type master;
file "example.co.jp.zone";
};
----------------------------------------------------------------------
ここまでで最低限の準備が整ったので、BINDを起動します。
# systemctl start named.service
# ps -ef | grep named
named 8275 1 0 16:29 ? 00:00:00 /usr/sbin/named -u named -c /etc/named.conf
正常に起動していますね。
名前解決ができるか試しておきましょう。
$ dig @localhost example.com ns +short
dns002.future.local.
dns001.future.local.
$ dig @localhost example.com mx +short
10 mail.example.com.
$ dig @localhost example.co.jp +short
192.0.2.100
$ dig @localhost www.example.co.jp +short
example.co.jp.
192.0.2.100
問題なさそうです!
移行先環境を準備する
続いて、移行先の環境を準備しましょう。
AlmaLinux9を最小インストールした状態から、PowerDNSを導入します。
バックエンドにはMariaDBを利用します。
# dnf install epel-release
# dnf install pdns pdns-backend-mysql pdns-tools
# dnf install mariadb-server
# systemctl enable mariadb.service
# systemctl start mariadb.service
MariaDBを初期設定し、PowerDNS用のデータベースを作成し、テーブルデータをインポートします。
# mysql_secure_installation
# mysql
> create database pdns DEFAULT CHARACTER SET utf8;
> create user 'pdnsuser'@'localhost' IDENTIFIED BY 'PASSWORD';
> GRANT ALL PRIVILEGES ON `pdns`.* TO 'pdnsuser'@'localhost';
> flush privileges;
> exit
# mysql -updnsuser -p pdns < /usr/share/doc/pdns/schema.mysql.sql
PowerDNSの初期設定をします。
# vi /etc/pdns/pdns.conf
----------------------------------------------------------------------
launch=gmysql
gmysql-host=localhost
gmysql-user=pdnsuser
gmysql-dbname=pdns
gmysql-password=PASSWORD
master=yes
----------------------------------------------------------------------
今回はゾーン移行を試すだけなのでとりあえずこれだけあれば大丈夫でしょう。
実際にサービス利用する場合はもう少し細かく設定した方が良いですね。
# systemctl enable pdns.service
# systemctl start pdns.service
これで移行先準備も完了です。
ゾーンを移行してみる
PowerDNSには便利なことに、zone2sqlというBINDのゾーンファイルをPowerDNS用のSQLへ変換してくれるツールが付属しています。
早速試してみましょう。
手っ取り早く試すため、移行先のPowerDNSサーバーに移行元BINDデータを同期してやります。
※移行先からroot権限でrsyncできる設定になっているものとします。設定方法は省略
# rsync -av bindtest.future.local:/etc/named.conf /etc/
# rsync -av bindtest.future.local:/var/named/ /var/named/
named.confの設定の一部は不要で、エラーの原因にもなるのでコメントアウトしておきましょう。
# vi /etc/named.conf
----------------------------------------------------------------------
//logging {
// channel default_debug {
// file "data/named.run";
// severity dynamic;
// };
//};
//zone "." IN {
// type hint;
// file "named.ca";
//};
//include "/etc/named.rfc1912.zones";
//include "/etc/named.root.key";
----------------------------------------------------------------------
それではSQLに変換!
# zone2sql --gmysql=yes --named-conf=/etc/named.conf | tee zones.sql
insert into domains (name,type) values ('example.co.jp','NATIVE');
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'SOA', 'bindtest.future.local admin.bindtest.future.local 2025082901 7200 3600 604800 3600', 600, 0, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'NS', 'dns001.future.local', 600, 0, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'NS', 'dns002.future.local', 600, 0, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'A', '192.0.2.100', 600, 0, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'MX', 'mail.example.co.jp', 600, 10, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'mail.example.co.jp', 'A', '192.0.2.100', 600, 0, 0 from domains where name='example.co.jp';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'www.example.co.jp', 'CNAME', 'example.co.jp', 600, 0, 0 from domains where name='example.co.jp';
0% done (/var/named/example.co.jp.zone)insert into domains (name,type) values ('example.com','NATIVE');
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.com', 'SOA', 'bindtest.future.local admin.bindtest.future.local 2025082901 7200 3600 604800 3600', 600, 0, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.com', 'NS', 'dns001.future.local', 600, 0, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.com', 'NS', 'dns002.future.local', 600, 0, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.com', 'A', '192.0.2.100', 600, 0, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.com', 'MX', 'mail.example.com', 600, 10, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'mail.example.com', 'A', '192.0.2.100', 600, 0, 0 from domains where name='example.com';
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'www.example.com', 'CNAME', 'example.com', 600, 0, 0 from domains where name='example.com';
100% done
2 domains were fully parsed, containing 14 records
良い感じです。問題なく出力されましたね。
teeコマンドで標準出力からファイル(zones.sql)にも保存しておいたので、そちらをデータベースへインポートします。
当然ですが、インポート前にはPowerDNSでの名前解決はできません。
$ dig @localhost example.com
; <<>> DiG 9.16.23-RH <<>> @localhost example.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 4939
;; flags: qr rd; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com. IN A
;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Sep 04 14:31:33 JST 2025
;; MSG SIZE rcvd: 40
ではインポートします。ついでに手っ取り早く反映させるためにPowerDNSのサービス再起動。
# mysql pdns < zones.sql
# systemctl restart pdns.service
行数も少ないのですぐに完了しますね。では名前解決を確認。
$ dig @localhost example.com
; <<>> DiG 9.16.23-RH <<>> @localhost example.com
; (2 servers found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26567
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;example.com. IN A
;; ANSWER SECTION:
example.com. 600 IN A 192.0.2.100
;; Query time: 4 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Sep 04 14:33:34 JST 2025
;; MSG SIZE rcvd: 56
ちゃんと引けています。
$ dig @localhost example.co.jp axfr
; <<>> DiG 9.16.23-RH <<>> @localhost example.co.jp axfr
; (2 servers found)
;; global options: +cmd
example.co.jp. 600 IN SOA bindtest.future.local. admin.bindtest.future.local. 2025082901 7200 3600 604800 3600
example.co.jp. 600 IN A 192.0.2.100
example.co.jp. 600 IN MX 10 mail.example.co.jp.
example.co.jp. 600 IN NS dns001.future.local.
example.co.jp. 600 IN NS dns002.future.local.
mail.example.co.jp. 600 IN A 192.0.2.100
www.example.co.jp. 600 IN CNAME example.co.jp.
example.co.jp. 600 IN SOA bindtest.future.local. admin.bindtest.future.local. 2025082901 7200 3600 604800 3600
;; Query time: 4 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: Thu Sep 04 14:34:46 JST 2025
;; XFR size: 8 records (messages 3, bytes 377)
こちらも問題なし。
簡単に移行できました。素晴らしいですね!
さて、ここで注意点がいくつか。
insert into domains (name,type) values ('example.co.jp','NATIVE');
insert into records (domain_id, name, type,content,ttl,prio,disabled) select id ,'example.co.jp', 'SOA', 'bindtest.future.local admin.bindtest.future.local 2025082901 7200 3600 604800 3600', 600, 0, 0 from domains where name='example.co.jp';
INSERT文を見ると分かりますが、zone2sqlではゾーンタイプはNATIVEに変換されます。
そのため、プライマリ(マスター)として設定する場合はタイプをMASTERに変更しましょう。
また当たり前ではありますが、SOAは移行元の設定そのままなので、必要に応じて修正しましょう。
ついでながら、PowerDNSのAPIを利用する場合は、domainmetadataテーブルで対象ドメイン(ゾーン)に対するSOA-EDIT-APIを追加してやると便利です。
追加していないと、APIでレコード変更したのにシリアルがカウントアップされずに更新されない!となってはまったりします。
insert into domainmetadata (domain_id,kind,content) select id, 'SOA-EDIT-API' ,'DEFAULT' from domains where name='example.com';
こんな感じで追加できますね。
結果
予想以上に簡単にゾーン移行ができました。便利。
今回は省いていますが、実際に移行するような場合は、移行データが正常かどうかもチェックは必要ですね。
ゾーン数にもよりますが、適当なスクリプトを書いて機械的にチェックするのが良さそうです。
それではまた!

