Kubernetes上にCoreDNSでDNSサーバを立ててみた

# CoreDNSとは

https://coredns.io/

Go言語によるDNSサーバ。

その機能のほとんどがプラグインで制御されているため高い柔軟性を持つ。

[Cloud Native Computing Foundation](https://cncf.io/)(CNCF)のGraduated Projectであり、KubernetesにおけるデフォルトのDNSサーバとしても使用されている。
(ただし今回はそれとは別の用途を想定し構築している)

# 実装

最小構成であれば、デプロイする必要があるリソースはPod(Deployment)とConfigMapのみのため、その2種のリソース、合計4マニフェストの例を示す。
(ただしこれだとPodのIPに直接アクセスできないとリクエストを投げられないので、実際には外部公開するためのServiceも必要になりそう)

なお今回は下記3ドメインのゾーン情報を管理し、それ以外をGoogle Public DNSにフォワーディングすることを要件とする。

- example.com
- example.net
- example.org

## Deployment

```jsx
apiVersion: apps/v1
kind: Deployment
metadata:
  name: coredns
  namespace: coredns
spec:
  replicas: 3
  selector:
    matchLabels:
      k8s-app: coredns
  template:
    metadata:
      labels:
        k8s-app: coredns
    spec:
      containers:
        - name: coredns
          image: coredns/coredns:latest
          args:
            - -conf
            - /etc/coredns/Corefile
          volumeMounts:
            - name: coredns-config-volume
              mountPath: /etc/coredns/Corefile
              subPath: Corefile
              readOnly: true
            - name: coredns-example-com-zone-volume
              mountPath: /etc/coredns/example.com
              subPath: example.com
              readOnly: true
            - name: coredns-example-net-zone-volume
              mountPath: /etc/coredns/example.net
              subPath: example.net
              readOnly: true
      volumes:
        - name: coredns-config-volume
          configMap:
            name: coredns-config
        - name: coredns-example-com-zone-volume
          configMap:
            name: coredns-example-com-zone
        - name: coredns-example-net-zone-volume
          configMap:
            name: coredns-example-net-zone
```

/etc/coredns/Corefileがメインの設定ファイル。

/etc/coredns/example.comと/etc/coredns/example.netはゾーンファイルである。

これらのファイルは後述するConfigMapを3種それぞれ、データボリュームとしてマウントする。

(なおexample.orgは別の方式でゾーン情報を持たせてみる)

## Configmap

### Corefile

```jsx
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns-config
  namespace: coredns
data:
  Corefile: |
    example.com:53 {
      file /etc/coredns/example.com
    }
    example.net:53 {
      file /etc/coredns/example.net
    }
    example.org:53 {
      log
      hosts {
        192.168.1.1 hoge.example.org
        192.168.1.2 fuga.example.org
        fallthrough
      }
    }
    .:53 {
      forward . 8.8.8.8 8.8.4.4
    }
```

要件を実現するためのほぼ最小構成。

実運用上ではlogやerrors、healthやreadyといった各種[プラグイン](https://coredns.io/manual/plugins/)を追加で使用することになると思う。

example.comとexample.netへの問い合わせ(それぞれ53番ポート)はfileプラグインによりゾーンファイルを参照させている。

example.orgはhostsプラグインで直書きしてみた。なおfallthroughオプションが無いと、一致するものがない場合にリクエストが下流に流れず打ち切られてしまうので注意。

また、後述するが、hostsプラグインで名前解決を行った場合はAレコードとして返ってくる模様。

### example.com

```jsx
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns-example-com-zone
  namespace: coredns
data:
  example.com: |
    $TTL 3600
    @   IN  SOA ns1.example.com. admin.example.com. (
            2024032101
            7200
            3600
            1209600
            3600
    )
    @       IN  NS  ns1.example.com.
    ns1     IN  A   10.0.0.100
    server1 IN  A   10.0.0.1
    server2 IN  A   10.0.0.2
```

### example.net

```jsx
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns-example-net-zone
  namespace: coredns
data:
  example.net: |
    $TTL 3600
    @   IN  SOA name.example.net. adm.example.net. (
            2024032101
            7200
            3600
            1209600
            3600
    )
    @       IN  NS  name.example.com.
    name    IN  A   172.16.0.200
    web     IN  A   172.16.0.1
    db      IN  A   172.16.0.2
```

# 動作確認

ServiceリソースをデプロイしてないのでPodのIPアドレスを直接指定して問い合わせる。

```jsx
$ kubectl get po -n coredns -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP               NODE          NOMINATED NODE   READINESS GATES
coredns-5775847dc8-4lp7q   1/1     Running   0          41m   10.192.173.113   test-node03   <none>           <none>
coredns-5775847dc8-9vf7x   1/1     Running   0          41m   10.192.184.113   test-node02   <none>           <none>
coredns-5775847dc8-vtpl5   1/1     Running   0          41m   10.192.130.106   test-node01   <none>           <none>
```

以下、確認結果を示す。

```jsx
$ dig A server1.example.com @10.192.173.113 +short
10.0.0.1

$ dig A server2.example.com @10.192.173.113 +short
10.0.0.2

$ dig A name.example.net @10.192.173.113 +short
172.16.0.200

$ dig A db.example.net @10.192.173.113 +short
172.16.0.2

### example.orgについてはAレコードで返ることを分かりやすく示すため+shortを外している
$ dig A hoge.example.org @10.192.173.113

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> A hoge.example.org @10.192.173.113
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6808
;; 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
; COOKIE: ae23e97670ce4991 (echoed)
;; QUESTION SECTION:
;hoge.example.org.              IN      A

;; ANSWER SECTION:
hoge.example.org.       3600    IN      A       192.168.1.1

;; Query time: 0 msec
;; SERVER: 10.192.173.113#53(10.192.173.113) (UDP)
;; WHEN: Fri Mar 21 15:30:28 JST 2025
;; MSG SIZE  rcvd: 89

$ dig A fuga.example.org @10.192.173.113

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> A fuga.example.org @10.192.173.113
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 4067
;; 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
; COOKIE: b67f04c7c08f658d (echoed)
;; QUESTION SECTION:
;fuga.example.org.              IN      A

;; ANSWER SECTION:
fuga.example.org.       3600    IN      A       192.168.1.2

;; Query time: 4 msec
;; SERVER: 10.192.173.113#53(10.192.173.113) (UDP)
;; WHEN: Fri Mar 21 15:30:43 JST 2025
;; MSG SIZE  rcvd: 89
```

管理外のゾーン情報をフォワーディングして問題なく名前解決できることも確認する。

```jsx
$ dig A qiita.com @10.192.173.113

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> A qiita.com @10.192.173.113
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 12650
;; flags: qr rd ra; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;qiita.com.                     IN      A

;; ANSWER SECTION:
qiita.com.              60      IN      A       13.249.146.69
qiita.com.              60      IN      A       13.249.146.22
qiita.com.              60      IN      A       13.249.146.65
qiita.com.              60      IN      A       13.249.146.98

;; Query time: 24 msec
;; SERVER: 10.192.173.113#53(10.192.173.113) (UDP)
;; WHEN: Fri Mar 21 15:33:31 JST 2025
;; MSG SIZE  rcvd: 138
```

# おまけ

```jsx
    .:53 {
      forward . 8.8.8.8 8.8.4.4
    }
```

ここ消してDeploymentとConfigMapとも再デプロイすると、管理外のゾーン情報については名前解決できなくなる。
(reloadプラグイン使えば再デプロイ不要なはず)
```jsx
$ dig A qiita.com @10.192.130.107 # デプロイし直したのでIPアドレス変わってる

; <<>> DiG 9.18.30-0ubuntu0.22.04.2-Ubuntu <<>> A qiita.com @10.192.130.107
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: REFUSED, id: 40859
;; 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
; COOKIE: e8c5afbb1f5024ac (echoed)
;; QUESTION SECTION:
;qiita.com.                     IN      A

;; Query time: 0 msec
;; SERVER: 10.192.130.107#53(10.192.130.107) (UDP)
;; WHEN: Fri Mar 21 15:36:27 JST 2025
;; MSG SIZE  rcvd: 50
```