consulを使いながら内部ドメインを参照したい場合の解決法

# はじめに
こんにちわ。@mikitです。
これは [NTTコミュニケーションズ Advent Calendar 2017](https://qiita.com/advent-calendar/2017/nttcom) の 18日目の記事です。
何気に Qiita 初投稿でドキドキしています。。!

[consul](https://www.consul.io/) を使って Docker コンテナでのさまざまな名前解決をしつつ、外部に対して公開していない内部ドメインも検索対象に入れたいと思ったことはありませんか?
consul 単体ではそれを行うことができませんが、[unbound](https://www.unbound.net/) と組み合わせることで TLD 毎の参照先を自由にコントロールすることができるようになります。

ニッチすぎて誰かの役に立つのか不安ではありますが、実際にうちのチームではこの構成で名前解決をしていますので、メモを兼ねて。

# 用語解説
- unbound
  - キャッシュDNSサーバのソフトウェアです。
  - [本家サイト](https://www.unbound.net/) ([日本語サイト](http://unbound.jp/unbound/)) が詳しいのでそちらを参照ください。
  - チューニングや実際の運用については [こちら](https://dnsops.jp/event/20160624/unbound.pdf) の記事が詳しいです。
- 権威DNSサーバ
  - 名前に対する IPアドレスの組み合わせや階層構造などを保持し、公開するためのサーバ。
- キャッシュDNSサーバ
  - 端末に「DNSサーバ」として設定したり、サーバの `/etc/resolv.conf` に設定したりするアレです。
  - 権威DNSサーバの階層を渡り歩き、端末やサーバからの名前解決要求に答えます。

# 前提条件
- 内部ドメイン名
    - `internal.example.` 
    - このドメインの情報を保持する権威DNSサーバの IPアドレス: `192.0.2.53`
- 内部ドメインに設定されているデータ (抜粋)
    - `www.internal.example. IN A 192.0.2.80`
    - `ns.internal.example. IN A 192.0.2.53`
    - `sv.internal.example. IN A 192.0.2.1`
- 今回設定するサーバのIPアドレス: `192.0.2.1`

- consul 起動 yml 例
  - `ports` で port 8600 を port 53 に bind しています。
  - consul 自身で解決できない名前解決要求については `9.9.9.9` で行っています。

```docker-compose.yml
version: '2.1'
services:
  consul:
    container_name: consul
    image: consul:0.9.3
    command: agent -server -bootstrap-expect 3 -recursor 9.9.9.9 -ui -client 0.0.0.0
    ports:
      - "8300:8300"
      - "8301:8301/udp"
      - "8301:8301/tcp"
      - "8302:8302/udp"
      - "8302:8302/tcp"
      - "8400:8400"
      - "8500:8500"
      - "53:8600/udp" # DNS
      - "53:8600/tcp" # DNS
    hostname: consul
    network_mode: "bridge"
    restart: always
```

- サーバ全体での DNS 参照先は 127.0.0.1 となっており、現在は consul が名前解決を担当しています。

```/etc/resolv.conf 
nameserver  127.0.0.1
``` 

- この状態ですと、DNS の階層構造に定義されていない `internal.example.` については `9.9.9.9` は知ることができないため、名前解決できません。

# 解決方法

- unbound の設定をします。

- 参考までに Dockerfile を載せますが、dockerhub の適当なイメージを利用されても構いません。

```Dockerfile:Dockerfile
FROM alpine:latest

MAINTAINER Miki TAKATA "mikit@example.jp"

RUN apk add --update unbound drill openssl \
  && rm -rf /var/cache/apk/*

COPY unbound.conf /etc/unbound/

EXPOSE 53/tcp
EXPOSE 53/udp

CMD ["/usr/sbin/unbound", "-d"]
```

- 最低限必要な unbound の設定
- 解説
  1. `access-control` にて名前解決要求を許可する IPアドレス帯を設定します。
  2. `stub-zone` にて、ドメイン配下の名前解決要求については指定した権威DNSサーバに問い合わせを行い、クライアントに答えを返すように設定します。`.consul` ドメインの場合は `192.0.2.1` の port 8600、`internal.example` の場合は `192.0.2.53`。
  3. `forward-zone` にて、`stub-zone` 以外の名前解決要求が来た時に、問い合わせを転送するよう設定します。
 
```yaml:unbound.conf
server:
        interface: 0.0.0.0
        access-control: 192.0.2.0/24 allow
        use-syslog: no
        log-time-ascii: yes
        log-queries: yes
        val-log-level: 2
        root-hints: /etc/unbound/root.hints
        rrset-roundrobin: yes

stub-zone:
        name: "consul"
        stub-addr: 192.0.2.1@8600
stub-zone:
        name: "internal.example"
        stub-addr: 192.0.2.53

forward-zone:
       name: "."
       forward-addr: 9.9.9.9
```

この状態で適宜 build します。

```
> docker build -t unbound .
```

- docker-compose.yml
  - unbound 起動用の ymlです。
  - なぜか `network_mode: "host"` にしないと意図通り動いてくれなかったのでおまじない的に入れています。ヘタレですみません。。教えてえらい人!

```docker-compose.yml
version: '2.1'
services:
  unbound:
    container_name: unbound
    hostname: unbound
    image: unbound
    ports:
      - "53:53/tcp"
      - "53:53/udp"
    volumes:
      - /etc/localtime:/etc/localtime:ro
    network_mode: "host"
```

- unbound 起動
  - `start of service` を確認

```
> docker-compose up
Dec 17 21:23:01 unbound[1:0] notice: init module 0: validator
Dec 17 21:23:01 unbound[1:0] notice: init module 1: iterator
Dec 17 21:23:02 unbound[1:0] info: start of service (unbound 1.6.7).
```

- consul の DNS ポート番号を変更して起動
  - その他、この時点で、consul へ飛んでくる名前解決要求は `.consul` に限定されるので `-recursor 9.9.9.9` オプションは外してもいいはず。

```docker-compose.yml
    ports:
      - "8600:8600/udp" # DNS
      - "8600:8600/tcp" # DNS
```

# 確認
- `internal.example` 配下の名前解決

```
> dig www.internal.example @192.0.2.1 +short
192.0.2.80
```

- consul の名前解決

```
> dig consul-sv.node.dc1.consul @192.0.2.1 +short
192.0.2.1
```

- その他ドメインの名前解決

```
> dig qiita.com @192.0.2.1 +short
54.249.227.135
46.51.250.143
```

それぞれ名前解決できました :)

# おまけ
- consul は名前解決の結果をキャッシュしない。
- `-recursor 9.9.9.9` を指定している場合、名前解決要求が来るたびに `9.9.9.9` に問い合わせを行い、結果を待つことになり効率が悪い。
- unbound を間に挟むことで、その他ドメインの問い合わせについては結果をキャッシュするようになるので、その他ドメインの問い合わせが多い環境では効率がよくなるはず。
- `9.9.9.9` は `Quad9` といって IBM のやってる Public DNS のサービス。https://www.quad9.net/

# まとめ、雑感
- 今回、consul と unbound の組み合わせを取り上げたけれど、他の DNS を使うサービスディスカバリのソフトウェアとの連携でも同じような手法を使えると思うので、色々やってみてほしいです。
- 自分の中では割と当然にあるものでも、こうやってまとまった形でアウトプットするのは思った以上に大変だった。
- アドベントカレンダーに誘ってくれた @iwashi に感謝