PowerDNSを用いて冗長化された権威DNSサーバを構築する(さくらのクラウド編)

![logo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/840f9ec4-1509-3ffe-9fc7-e6e88ed588a7.png)

## はじめに

[PowerDNS](https://www.powerdns.com/)を用いて、物理的、地理的に冗長化(※)された権威DNSサーバの構築例を紹介します。本稿では[「さくらのクラウド」](https://cloud.sakura.ad.jp/)上に構築していますが、同等の構成をとれる環境では同じ設定が可能かと思います。

PowerDNS自体の説明は、[Wikipedia](https://ja.wikipedia.org/wiki/PowerDNS)やその他わかりやすいサイトや資料が多数あるため、本稿では割愛しますが、

- 権威DNSサーバとして動作する **PowerDNS Authoritative Server**
- DNSキャッシュサーバとして動作する **PowerDNS Recursor**

の2種類があります。ここでは権威サーバとして使用するため前者を導入します。

:::note info
ところで、冗長化したからといって必ずしも高可用性を実現できるわけではありません。冗長化はその必要条件にすぎないからです。ことDNSの可用性については、DDoS対策、オペレーション、バックアップ体制など、他にも重要な要素が多々存在します。
:::

## インフラ構成

本稿では以下のような構成例を元に設定例を紹介します。

![fig1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/8d1efab1-a3f8-d9e8-8649-1c045153e60f.png)

この構成例では、「プライマリサーバ」と「DNSサーバ」に、役割を明確に分けています。

### プライマリサーバについて

DNSゾーンのリソースレコードなどの情報を一元管理するサーバです。インターネットからのDNSクエリへの応答は行いません。そのため、一般的なDNSのコンテキストにおいて、シャドウマスタやヒドゥンマスタと呼ばれることもあります。

なお、さくらのクラウド上に構築するということで、インフラ部分が冗長化されているという前提の元、サーバ(VM)レベルでの冗長化は行っていません。

- ディスクイメージが格納されるストレージ機器は冗長化されており、ハードウェア故障に対するデータの保護耐性がある
- サーバ(VM)が動作するホストはHA構成となっており、ホストがダウンしても別のホスト上で自動的に再起動される

今回の例では、東京側に1台のみ作成しています。

### DNSサーバについて

インターネット上のレゾルバからのクエリに応答するDNSサーバは、地理的に分散されるよう東京と石狩の両拠点に構築します。

さらに、各拠点内でハードウェア冗長性を確保するため、2台のサーバをVRRPで冗長化し、それぞれのVRRP仮想IPアドレスに対してゾーンの権威をもたせる(上位ゾーンからNSを向ける)ようにします。

### 例示する各サーバのIPアドレス

各サーバのIPアドレス(例示)は以下の通りとなります。お手元の環境のアドレスに適宜読み替えてください。

| ホスト名        | IPアドレス       | 仮想IPアドレス              |
| -------------- | -------------- | --------------------------- |
| is-dns1        | 203.0.113.5    | 203.0.113.4(ns1.example.jp) |
| is-dns2        | 203.0.113.6    | 同上                         |
| tk-dns1        | 192.0.2.5      | 192.0.2.4(ns2.example.jp)   |
| tk-dns2        | 192.0.2.6      | 同上                        |
| tk-dns-primary | 192.0.2.10     | なし                        |

## ミドルウェア構成

PowerDNSでは、様々なバックエンドデータベースを利用可能ですが、ここではMariaDBを使用します。プライマリサーバとDNSサーバ間の関係は下図のとおりです。

![fig2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/2623306b-e8f4-44b6-b53e-a7230a021417.png)

プライマリサーバがもっているゾーン情報は、MariaDBのレプリケーションを用いてDNSサーバに伝搬します。DNSサーバ上で動いているPowerDNSは、自ホスト上のMariaDBを参照しながらDNS応答を行います。したがって、プライマリサーバがダウンしても、ゾーン情報の更新ができなくなるだけで、DNS応答が途絶えるすることはありません。

※ DNSホスティング事業者では、ユーザがゾーン情報の変更できない状態も、サービス障害とみなすことが多いようですが。。

本稿では以下のソフトウェア、バージョンの組み合わせで動作確認を行っております。

| 項目                    | 種別・バージョン         |
| ---------------------- | ---------------------- |
| OS/ディストリビューション | CentOS7.9 Linux 3.10.0 |
| データベース             | MariaDB 10.9.3         |
| DNSサーバ                | PowerDNS 4.6.3        |
| VRRP                    | Keepalived 1.3.5       |

### 余談：ゾーン転送について

従来から存在するBINDなどのDNSサーバソフトウェアでは、DNSサーバ間のゾーン情報の同期には、元々DNSのプロトコルとして定義されている「ゾーン転送」という動作が広く用いられてきました。PowerDNSでは、この「ゾーン転送」を用いずに、上記のようなデータベース(RDBMS)のレプリケーションのような信頼性のある仕組みを利用することが推奨されています。

従来の「ゾーン転送」では、ゾーンのSOAレコード中のシリアル番号を元に更新を検知したり、プライマリサーバからセカンダリサーバに更新を通知するためにDNS Notifyパケットが存在したり、そもそもの仕組みが複雑であることからトラブルや負荷の要因となっていました。PowerDNSとRDBMSのレプリケーションを組み合わせた方法では、このようなサーバ間の情報同期の煩わしさから開放されます。

※ 別途、RDBMSの運用ノウハウが必要になるのはさておき :blush:

## 1. ネットワーク(ルータ＋スイッチ)の作成

ここから具体的な構築手順を説明していきます。まずはサーバを収容するネットワークの準備を行います。

さくらのクラウドでは、専用のネットワークアドレス帯とL2ブロードキャストドメインを提供する「ルータ＋スイッチ」というリソースがあり、これを今回使用します。東京と石狩に1つずつ作成します。

![fig3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/8b84ab4a-4583-5e41-3f5d-c583df7019cf.png)

作成が完了すると、以下のようにネットワークアドレスが確認できます。以降、例示アドレスは割り当てられたIPアドレスに読み替えてください。

![fig4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/3ac58513-8788-86f2-7ed6-013ea5784191.png)

## 2. サーバの作成

コントロールパネルのサーバの画面を開き、サーバの作成を進めます。同じ要領で5台分作成してください。

![fig5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/61b30f29-79d3-3250-68b3-88ad1212bf68.png)

![fig6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/9ebbd073-4344-3a93-176e-22c08dec1f42.png)

![fig7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/dc86c6d9-3e5a-6c7b-cd85-31bf8e83dfe0.png)

![fig8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/6e1741a0-5f37-a112-17b0-daf09e683aec.png)

![fig9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/108990/73335e3c-e10d-22e8-3773-317e8b7d0d57.png)

「サーバの情報」に設定しているタグ(`@group`)は、サーバが起動されるホストが冗長となるように制御するものです。`a`～`d`の4種類の記号を設定することができ、異なる記号が設定されているサーバは、異なるホスト上で起動されます。これで、ホスト障害により、冗長化している2つのサーバが同時にダウンすることを防ぐことができます。

今回の構成では、以下のように1台目にはa、2台目にはbを設定しておくとよいでしょう。

| ホスト名       | グループタグ |
| ------------- | ---------- |
| is-dns1       | `@group=a` |
| is-dns2       | `@group=b` |
| tk-dns1       | `@group=a` |
| tk-dns2       | `@group=b` |

詳しくは、[特殊タグ一覧 | さくらのクラウドドキュメント](https://manual.sakura.ad.jp/cloud/server/special-tags.html)をご確認ください。

## 3. OS初期セットアップ

サーバの作成が完了したら、それぞれにログインし、以下のセットアップを進めます。

### 全サーバにて

アップデートし、基本パッケージをインストールします。

```
yum -y update
yum -y groupinstall base
```

続いて、パケットフィルタ設定を行います。

DNSのトラフィックによりconntrackの負荷が増大しないよう、ここではステートレスなフィルタを設定しています。ここでは手順をシンプルにするため、firewalldを用いず、iptablesのルールを直接叩き込んでいますが、お好みで選択してください。


```
yum -y install iptables-services

cat > /etc/sysconfig/iptables <<_EOF_
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]

-A INPUT -i eth0 -s xx.xx.xx.xx/xx -j ACCEPT       ← SSHアクセスを許可するIPアドレスを記入する
-A INPUT -i eth0 -s 192.0.2.0/28 -j ACCEPT         ← 東京側ネットワークアドレスに書き換える
-A INPUT -i eth0 -s 203.0.113.0/28 -j ACCEPT       ← 石狩側ネットワークアドレスに書き換える

-A INPUT -i eth0 -p tcp --dport 22 -j DROP
-A INPUT -i eth0 -p tcp --dport 3306 -j DROP

COMMIT
_EOF_

systemctl disable firewalld
systemctl enable iptables
```

一度再起動します。

```
reboot
```

## 4. ミドルウェアインストール

### 全サーバにて

MariaDBをインストールします

```
curl -sS https://downloads.mariadb.com/MariaDB/mariadb_repo_setup | sudo bash
yum -y install MariaDB-server
```

PowerDNSをインストールします

```
yum -y install epel-release yum-plugin-priorities
curl -o /etc/yum.repos.d/powerdns-auth-46.repo https://repo.powerdns.com/repo-files/el-auth-46.repo
yum -y install pdns pdns-tools pdns-backend-mysql
```

## 5. MariaDB設定

### レプリケーション用のサーバID割り当て

今回は、GTIDによるレプリケーションを行います。そこで5台でユニークなサーバIDを割り当てる必要があります。ここでは以下のように割り当てました。

| ホスト名       | サーバID |
| -------------- | -------------- |
| tk-dns-primary | 1 |
| is-dns1        | 11 |
| is-dns2        | 12 |
| tk-dns1        | 21 |
| tk-dns2        | 22 |

### プライマリサーバにて

以下の内容で/etc/my.cnfを作成します。

```
[mariadb]
server_id = 1
log_bin = log-bin
```

MariaDBを起動し、PowerDNSが用いる初期データベースの構築を行います。
以降、rootユーザとレプリケーション用ユーザ(repl)のパスワードを `himitsu-0716` として例示していますが、安全なパスワードを設定してください。

```
systemctl enable mariadb
systemctl restart mariadb

# anonymousユーザとtestデータベースを削除します
mysql -e "DELETE FROM mysql.global_priv WHERE User=''"
mysql -e "DROP DATABASE IF EXISTS test"
mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'"

# rootユーザのパスワードを設定します
mysqladmin password "himitsu-0716"
mysql -e "flush privileges"

# レプリケーション用のユーザを作成します
mysql -e "CREATE USER repl IDENTIFIED BY 'himitsu-0716'"
mysql -e "GRANT REPLICATION SLAVE ON *.* TO 'repl'"
mysql -e "flush privileges"

# データベースを作成します
mysql -e "CREATE DATABASE pdns CHARACTER SET utf8"
mysql pdns < /usr/share/doc/pdns-backend-mysql-*/schema.mysql.sql
```

## 6. データベースレプリケーション設定

### DNSサーバにて

以下ような内容で /etc/my.cnf を作成します。
以下はis-dns1の例です。server-idはサーバ毎に書き換えてください。

```
[mariadb]
server_id = 11               ← サーバごとに書き換える
relay_log = relay-bin
replicate_do_db = pdns
skip-networking
```

MariaDBを起動します。

```
systemctl enable mariadb
systemctl restart mariadb
```

testデータベースを削除します

```
mysql -e "DROP DATABASE IF EXISTS test"
mysql -e "DELETE FROM mysql.db WHERE Db='test' OR Db='test\\_%'"
```

プライマリサーバからのレプリケーションを設定します。
192.0.2.10の部分はプライマリサーバのIPアドレスに読み替えてください。

```
ssh 192.0.2.10 mysqldump --all-databases --master-data=1 --gtid --single-transaction --routines > dump.sql

mysql -e 'stop replica'
mysql -e 'reset replica all'

mysql < dump.sql
rm -f dump.sql

mysql -e "CHANGE MASTER TO MASTER_HOST = '192.0.2.10', MASTER_USER = 'repl', MASTER_PASSWORD = 'himitsu-0716', MASTER_USE_GTID = slave_pos"
mysql -e "start replica"
```

レプリケーション動作が正しく行われていることを確認します。

```
mysql -e "show replica status\G"

! 以下となっていればレプリケーションが正しく行われています
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
Gtid_IO_Pos: 0-1-XX
```

:::note warn
DNSサーバは、レプリケーションセカンダリとして動作しているため、データベースに変更を加えてはなりません(pdnsutilコマンドによる更新操作などを含め)。不整合が発生しレプリケーションが停止してしまいます。
:::

## 7. PowerDNS設定

### 全サーバにて

以下の内容で /etc/pdns/pdns.conf を作成します

```
expand-alias=yes
resolver=8.8.8.8
setgid=pdns
setuid=pdns
negquery-cache-ttl=10

launch=gmysql
gmysql-socket=/var/lib/mysql/mysql.sock
gmysql-user=root
gmysql-dbname=pdns
gmysql-password=himitsu-0716
```

/etc/pdns ディレクトリ配下のownerをpdnsユーザに変更します

```
chown -R pdns:pdns /etc/pdns
```

### DNSサーバにて

PowerDNSを有効化し、デーモンを起動します

```
systemctl enable pdns
systemctl start pdns
systemctl status pdns
```

:::note info
プライマリサーバではDNS応答を行わないため、PowerDNSデーモン(pdns_server)を起動しておく必要はありません。pdns_serverを起動せずとも、pdnsutilコマンドを用いてゾーン情報をデータベースに反映することは可能です。
:::

## 8. DNS応答動作確認

プライマリサーバ側でテスト用のDNSゾーンを作成し、DNSサーバ側でDNSが引けるようになるかどうか確認します。

### プライマリサーバにて

テスト用のDNSゾーンを作成します。

```
pdnsutil load-zone example.jp /dev/stdin <<_EOF_
@       259200  IN      NS      ns1
        259200  IN      NS      ns2
        3600    IN      SOA     ns1.example.jp. noc.example.jp. 1 3600 900 2419200 300
ns1     259200  IN      A       203.0.113.4
ns2     259200  IN      A       192.0.2.4
test    3600    IN      A       192.0.2.1
test-alias 3600 IN      ALIAS   a.root-servers.net
_EOF_
```

### DNSサーバにて

ゾーン情報がレプリケーションされているか確認します

```
pdnsutil list-all-zones
! ゾーンのリストを取得し、example.jpゾーンが作成されていればOKです


pdnsutil list-zone example.jp

! 以下のようにゾーンの内容が正しく表示されれば成功です
$ORIGIN .
example.jp      259200  IN      NS      ns1.example.jp.
example.jp      259200  IN      NS      ns2.example.jp.
example.jp      3600    IN      SOA     ns1.example.jp noc.example.jp 1 3600 900 2419200 300
ns1.example.jp  259200  IN      A       203.0.113.4
ns2.example.jp  259200  IN      A       192.0.2.4
test-alias.example.jp   3600    IN      ALIAS   a.root-servers.net
test.example.jp 3600    IN      A       192.0.2.1
```

DNSクエリに応答するか確認します

```
dig @127.0.0.1 test.example.jp a

! 以下のようにレスポンスが返ってくれば成功です
; <<>> DiG 9.11.4-P2-RedHat-9.11.4-26.P2.el7_9.5 <<>> @127.0.0.1 test.example.jp a
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 49992
;; 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:
;test.example.jp.               IN      A

;; ANSWER SECTION:
test.example.jp.        3600    IN      A       192.0.2.1

;; Query time: 2 msec
;; SERVER: 127.0.0.1#53(127.0.0.1)
;; WHEN: 金  7月 16 21:59:46 JST 2021
;; MSG SIZE  rcvd: 60

dig +short @127.0.0.1 test-alias.example.jp a

! 以下のようにレスポンスが返ってくれば成功です(a.root-servers.netのIPアドレス)
198.41.0.4
```

## 9. keepalived(VRRP)設定

### DNSサーバにて

keepalivedをインストールし、VRRPの設定をします。

```
yum -y install keepalived

cat > /etc/keepalived/keepalived.conf <<_EOF_
vrrp_instance V1 {
        state BACKUP
        interface eth0
        virtual_router_id 4          ← VIPの第4オクテットとする
        priority 100                 ← 2号機側は90にする
        advert_int 5
        virtual_ipaddress {
                203.0.113.4          ← 書き換える
        }
}
_EOF_

systemctl start keepalived
systemctl status keepalived
systemctl enable keepalived

! インターフェイスにVIPが付与されることを確認する
ip addr show
```

VIPがDNS応答するか確認します

```
dig @203.0.113.4 test.example.jp
```

## 10. おつかれさまでした

本稿では割愛しますが、以下のようなことを確認、検討しておくと良いでしょう。

- DNSサーバの応答性能を計測する(以下に補足説明)
- DNSサーバのVRRPが想定通り切り替わるか動作確認をする
- プライマリサーバのデータベースを定期にバックアップする
- 死活監視、モニタリングを行う
    - 各サーバのCPU、メモリ、ディスク空き容量など基本的なチェック、傾向把握
    - DNS応答の外形監視(それぞれの実IP、VIPに対してDNSクエリを発行)
    - データベースのレプリケーションが停止していないかチェック("show replica status"の出力などを確認)
- MariaDBレプリケーション用に安全なネットワークを用意する
    - 本稿では、インターネット経由でレプリケーションする構成例としましたが、閉域網を使用するのが好ましいでしょう
    - さくらのクラウドでは[「ブリッジ接続」](https://manual.sakura.ad.jp/cloud/network/bridge.html)や[「ローカルルータ」](https://manual.sakura.ad.jp/cloud/network/localrouter.html)といった機能を利用可能です

### 補足：PowerDNSのDNS応答性能について

前述の通り、PowerDNSはRDBMS(ここではMariaDB)に検索SQL(SELECT文)を発行し、リソースレコードなどの情報を得た上でDNS応答します。一方で、Packet Cacheという高速化機構を備えており、RDBMSから得た情報をしばらくメモリ上にキャッシュします(デフォルトで20秒間)。キャッシュにヒットした場合はSQLを発行することなくDNS応答できます。

そのため、キャッシュにヒットする場合、しない場合で大幅に性能が変化します。安全をみるのであれば、最悪値を元にサイジング、プロビジョニングを行うのがよいでしょう。なお、性能チューニングに関して詳しくは、[PowerDNSのマニュアル(Performance and Tuning)](https://doc.powerdns.com/authoritative/performance.html)を参考にしてみてください。

## 更新履歴

- 2022/09/26
  MariaDBとPowerDNSのバージョンをアップデートし、設定ファイル、コマンドを更新しました