[Mac] ExternalDNS+ローカルCoreDNS/etcdでkind上のServiceを名前解決してみる

こんにちは、(株)日立製作所 研究開発グループ サービスコンピューティング研究部の中村です。

Kubernetesの機能や関連するOSSをローカルPCで試す時、minikubeやkindを利用されている方も多いと思います。
KubernetesのIngress周りの機能や関連するOSSを、ローカルPC(Mac)で試す場合、Public Cloudとは別の設定(Service typeをLoadBalancerではなくNodePort)にしたり、hostsファイルを書き換えたり、使われている方も多いのではないでしょうか。
また、複数のWorkerノードが動作しているKubernetesクラスタで試したい場合、minikubeでは機能不足で実現できないし、VMを立てて構築するにもリソースが足らないという場合に候補になるのが、kind(Kubernetes in Docker)です。
本稿では、kindを使ったローカルKubernetesクラスタ環境で、なるべく同じようなIngress周りの設定でデプロイできるようにする方法をご紹介したいと思います。

## システム構成

今回利用したOS、各OSSのVersionは以下の通りです。

| 分類   |  コンポーネント   |   Version         |
|:------|:----------------|:------------------|
| OS    |  MacOS          | Catalina(10.15.1) |
| OSS   |  Docker for Mac | 2.1.0.5           |
| OSS   |  kind           | v0.6.0            |
| OSS   |  etcd           | 3.4.3             |
| OSS   |  CoreDNS        | 1.6.2             |
| OSS   |  Kubernetes     | 1.16.3            |
| OSS   |  kubectl        | 1.16.3            |
| OSS   |  MetalLB        | 0.8.3             |
| OSS   |  ExternalDNS    | 0.5.17            |

----
#### 注意! (2020/2/12追記)

Docker for Macが2.2の場合、localhost(127.0.0.1)以外のIPアドレスでの[publishができなくなる問題](https://github.com/docker/for-mac/issues/4209)が発生しているようです。
今のところ、2.2での解決策が無いようですので、2.1.0.5を使って進めてください。

----

また、構築するシステム構成は以下の図ような構成です。

![system_structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/c8003457-aede-4185-65d0-cc1276129d8d.png)

## CoreDNS/etcdのインストールと設定

Homebrewを使って、CoreDNS/etcdをMac上にインストールします。

    $ brew tap "coredns/deployment" "https://github.com/coredns/deployment"
    $ brew install coredns etcd

デフォルトではetcdを参照しないため、特定のドメイン(ここではexample.com)のDNSレコードをetcdを参照するようにCoreDNSの設定を書き換えます。以下のファイルを編集します。

    $ sudo vim /usr/local/etc/coredns/Corefile    

以下のようにCorefileを書き換えます。

    example.com {           //  ←追加
        etcd {              //  ←追加
            path /skydns    //  ←追加
        }                   //  ←追加
        cache               //  ←追加
    }                       //  ←追加

    . {
        fallthrough
    }
    ・・・・

そして、CoreDNS/etcdのサービスを起動します。

    $ brew services start etcd
    $ brew services start coredns

CoreDNSが起動しているかチェックします。

    $ dig www.google.com +short @localhost
    172.217.6.36

正しく動いているようであれば、システム設定＞ネットワーク＞拡張のDNSタブからDNSサーバに127.0.0.1を手動でDNSサーバを登録します。

<img width="780" alt="DNS_setting.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/31dc6ec7-bdfc-d836-8285-1f62d4ec3f58.png">


## socatを用いてホストからKubernetesクラスタ内へフォワード設定

socatコンテナをDocker for Mac上で動作させて、ホストネットワークからKubernetesクラスタ内に通信できるようにします。ただし、この後セットアップするMetalLBで管理されるIPアドレスでアクセスがきたら、Kubernetesクラスタ内にフォワードさせたいので、まずはループバックデバイスにエリアス設定をします。

    $ sudo ifconfig lo0 alias 172.17.255.1 netmask 0xffffff00
    $ sudo ifconfig lo0 alias 172.17.255.2 netmask 0xffffff00

エリアス設定ができたらKubernetesクラスタ内に転送するsocatコンテナを起動します。なお、転送するIPアドレス、ポートの組合せ毎に立てる必要がありますので、80, 443ポートの２つを、２つのIPアドレスにフォワードしたい場合は、以下のように合計２つのsocatコンテナを起動します。

    $ docker run -d --name kind-proxy-lb1-80 --publish 172.17.255.1:80:80 alpine/socat -dd tcp-listen:80,fork,reuseaddr tcp-connect:172.17.255.1:80
    $ docker run -d --name kind-proxy-lb1-443 --publish 172.17.255.1:80:80 alpine/socat -dd tcp-listen:80,fork,reuseaddr tcp-connect:172.17.255.1:443
    $ docker run -d --name kind-proxy-lb2-80 --publish 172.17.255.1:80:80 alpine/socat -dd tcp-listen:80,fork,reuseaddr tcp-connect:172.17.255.2:80
    $ docker run -d --name kind-proxy-lb2-443 --publish 172.17.255.1:80:80 alpine/socat -dd tcp-listen:80,fork,reuseaddr tcp-connect:172.17.255.2:443

dockerコマンドで起動されているか確認します。

    $ docker ps -a
    CONTAINER ID        IMAGE                  COMMAND                  CREATED             STATUS              PORTS                       NAMES
    bab6583aa9a1        alpine/socat           "socat -dd tcp-liste…"   2 minutes ago       Up 2 minutes        172.17.255.2:443->443/tcp   kind-proxy-lb2-443
    b900450fe363        alpine/socat           "socat -dd tcp-liste…"   2 minutes ago       Up 2 minutes        172.17.255.2:80->80/tcp     kind-proxy-lb2-80
    3a281fb45acd        alpine/socat           "socat -dd tcp-liste…"   2 minutes ago       Up 2 minutes        172.17.255.1:443->443/tcp   kind-proxy-lb1-443
    9e4b5ae42302        alpine/socat           "socat -dd tcp-liste…"   2 minutes ago       Up 2 minutes        172.17.255.1:80->80/tcp     kind-proxy-lb1-80


## Kubernetesクラスタを構築

次に、kindでKubernetesクラスタを構築します。
まずは、kindを[こちら](https://github.com/kubernetes-sigs/kind#installation-and-usage)を参考にインストールします。Macの場合はHomebrewで簡単にインストールできます。

    $ brew install kind

kindコマンドがインストールされたら直ぐにKubernetesクラスタが構築できるのですが、折角なので、Controlノードが１つWorkerノードが３つのクラスタを構築するために、以下のようなconfig.yamlを作成します。

```yaml:config.yaml
    kind: Cluster
    apiVersion: kind.sigs.k8s.io/v1alpha3
    nodes:
    - role: control-plane
    - role: worker
    - role: worker
    - role: worker
```

もし、Feature Gatesを設定したい場合は、[こちら](https://qiita.com/ryysud/items/eb8f7ff5663c948e59b1)を参考にして上記のconfig.yamlを修正してください。
config.yamlが準備できたら、以下のコマンドを実行します。

    $ kind create cluster --name kind --config config.yaml
    Creating cluster "kind" ...
    ✓ Ensuring node image (kindest/node:v1.16.3) 🖼
    ✓ Preparing nodes 📦 
    ✓ Writing configuration 📜 
    ✓ Starting control-plane 🕹️ 
    ✓ Installing CNI 🔌 
    ✓ Installing StorageClass 💾 
    ✓ Joining worker nodes 🚜 
    Set kubectl context to "kind-kind"
    You can now use your cluster with:

    kubectl cluster-info --context kind-kind

    Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

これでKubernetesクラスタが構築できました。実際に確認してみましょう。

    $ kubectl get nodes
    NAME                 STATUS   ROLES    AGE     VERSION
    kind-control-plane   Ready    master   2m28s   v1.16.3
    kind-worker          Ready    <none>   112s    v1.16.3
    kind-worker2         Ready    <none>   113s    v1.16.3
    kind-worker3         Ready    <none>   113s    v1.16.3

config.yamlでの設定の通り、Controlノードが１つ、Workerノードが３つのKubernetesクラスタができました。

## MetalLBをデプロイ

先程構築したKubernetesクラスタにMetalLBをデプロイします。詳細は[こちら](https://mauilion.dev/posts/kind-metallb/)を参考にして頂けると良いと思います。（この記事ではMacの場合、kubectl proxyを推奨していますが、socatを使ってdocker network "bridge"内にアクセスできるようにしているので、kubectl proxyなしでアクセスできます)

まずはMetalLBをKubernetesクラスタ上にデプロイします。実行するコマンドは以下の通りです。

    $ kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml

現時点でのMetalLBの最新Versionは0.8.3なのでそれを利用しています。

次に、L2モードで動作するような以下のようなConfigMapを作成します。割り当てるIPアドレスは、Docker networkの"bridge"ネットワークと同じネットワーク内のIPアドレスである172.17.255.1-172.17.255.2にします。これは、ループバックデバイスにaliasを設定し、socatでフォワーディング設定しているIPアドレスと一致する必要がありますので、変更している場合は適宜、こちらも変更してください。

```yaml:metallb-config.yaml
    apiVersion: v1
    kind: ConfigMap
    metadata:
    namespace: metallb-system
    name: config
    data:
    config: |
      address-pools:
      - name: default
        protocol: layer2
        addresses:
        - 172.17.255.1-172.17.255.2
```

そして、以下のコマンドでKubernetesクラスタにデプロイします。

    $ kubectl apply -f metallb-config.yaml


## ExternalDNSをデプロイ

次にExternalDNSをデプロイします。
以下のようなmanifestファイルを作成して、Kubernetesクラスタにデプロイします。

```yaml:externaldns.yaml
    ---
    apiVersion: rbac.authorization.k8s.io/v1beta1
    kind: ClusterRole
    metadata:
      name: external-dns
    rules:
    - apiGroups: [""]
      resources: ["services"]
      verbs: ["get","watch","list"]
    - apiGroups: [""]
      resources: ["pods"]
      verbs: ["get","watch","list"]
    - apiGroups: ["extensions"]
      resources: ["ingresses"]
      verbs: ["get","watch","list"]
    - apiGroups: [""]
      resources: ["nodes"]
      verbs: ["list"]
    - apiGroups: ["networking.istio.io"]
      resources: ["gateways"]
      verbs: ["get","watch","list"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1beta1
    kind: ClusterRoleBinding
    metadata:
      name: external-dns-viewer
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: external-dns
    subjects:
    - kind: ServiceAccount
      name: external-dns
      namespace: kube-system
    ---
    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: external-dns
      namespace: kube-system
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: external-dns
      namespace: kube-system
    spec:
    strategy:
      type: Recreate
    selector:
      matchLabels:
      app: external-dns
    template:
      metadata:
      labels:
        app: external-dns
      spec:
        serviceAccountName: external-dns
        containers:
          - name: external-dns
            image: registry.opensource.zalan.do/teapot/external-dns:latest
            args:
              - --source=service
              - --source=ingress
              - --provider=coredns
              - --log-level=debug # debug only
              - --policy=sync
              - --domain-filter=example.com
            env:
              - name: ETCD_URLS
                value: http://host.docker.internal:2379
```


今回は、Mac上で動作しているCoreDNS + etcdをDNSサーバとして利用するので、最後の方に設定されている、providerオプション及び、環境変数ETCD_URLSの設定は忘れないでください。
また、"example.com"というドメインでWebサービスをデプロイする想定で、ExternalDNSのargsにdomain filterの設定をしていますが、適宜変更してください。

以上で環境構築は完了です。

## NGINXをServiceとしてデプロイして動作確認

環境構築ができたら、実際にNGINXをサービスとしてデプロイして動作確認してみましょう。ExternalDNSで自動でDNSレコードがCoreDNSに設定されて、設定したhost名でアクセスできるか確認していきたいと思います。

まずは、NGINXをサービスとしてデプロイするために、以下のようなmanifestファイルを作成し、Kubernetesクラスタにデプロイします。

```yaml:nginx.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: nginx
    spec:
      selector:
        matchLabels:
          app: nginx
      template:
        metadata:
          labels:
            app: nginx
        spec:
          containers:
            - name: nginx
              image: nginx:1
              ports:
                - name: http
                  containerPort: 80
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: nginx
      annotations:
        external-dns.alpha.kubernetes.io/hostname: "nginx.example.com"
    spec:
      ports:
        - name: http
          port: 80
          protocol: TCP
          targetPort: 80
      selector:
        app: nginx
      type: LoadBalancer
```

Serviceリソースを直接できるようにDNSレコードをExternalDNSに登録させるには、typeをLoadBalancerとし、annotationに"external-dns.alpha.kubernetes.io/hostname: \<hostname\>"という形で設定する必要があります。

デプロイできたか確認していきましょう。
まずは、NGINXのPodが作成され、コンテナが起動しているか確認します。

    $ kubectl get pods
    NAME                     READY   STATUS    RESTARTS   AGE
    nginx-5f78746595-jljzl   1/1     Running   0          10s

もし、StatusがRunningになっていなければ、Runningになるまで待ちましょう。コンテナが起動していることが確認できたら、MetalLBによって、デプロイしたServiceにExternal IPが割り当てられたか確認します。

    $ kubectl get svc
    NAME         TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)        AGE
    kubernetes   ClusterIP      10.96.0.1       <none>         443/TCP        11m
    nginx        LoadBalancer   10.104.22.203   172.17.255.1   80:31998/TCP   3m15s

External IPが172.17.255.1が割り当てられているようです。
本当にアクセスできるかcurlを使って確認してみましょう。

    $ curl http://172.17.255.1
    <!DOCTYPE html>
    <html>
    <head>
    <title>Welcome to nginx!</title>
    <style>
        body {
            width: 35em;
            margin: 0 auto;
            font-family: Tahoma, Verdana, Arial, sans-serif;
        }
    </style>
    </head>
    <body>
    <h1>Welcome to nginx!</h1>
    <p>If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.</p>

    <p>For online documentation and support please refer to
    <a href="http://nginx.org/">nginx.org</a>.<br/>
    Commercial support is available at
    <a href="http://nginx.com/">nginx.com</a>.</p>

    <p><em>Thank you for using nginx.</em></p>
    </body>
    </html>

NGINXから応答が返ってきているようです。
次に、ExternalDNSがデプロイされたServiceを検知して、DNSレコードを作成したかログで確認してみましょう。

    $ kubectl -n kube-system logs external-dns-56c79c6b69-ptzsr
    〜〜〜
    time="2019-12-17T18:57:05Z" level=debug msg="service added"
    time="2019-12-17T18:57:06Z" level=debug msg="No endpoints could be generated from service kube-system/kube-dns"
    time="2019-12-17T18:57:06Z" level=debug msg="Endpoints generated from service: default/nginx: [nginx.example.com 0 IN A 172.17.255.1 []]"
    time="2019-12-17T18:57:06Z" level=debug msg="No endpoints could be generated from service default/kubernetes"
    time="2019-12-17T18:57:06Z" level=info msg="Add/set key /skydns/com/example/nginx/3ce624fa to Host=172.17.255.1, Text=\"heritage=external-dns,external-dns/owner=default,external-dns/resource=service/default/nginx\", TTL=0"
    〜〜〜

"nginx.example.com"というDNSレコードをCoreDNSのバックエンドのetcdに登録されたようです。
なお、ExternalDNSは１分おきにリソースの追加・削除をチェックして、そのタイミングでDNSレコードの追加・削除を行います。もし上記のようなログが出ていなかったら、１分程度待ってみてください。

Mac上のCoreDNSで名前解決できるか確認してみましょう。

    $ dig nginx.example.com @localhost

    ; <<>> DiG 9.10.6 <<>> nginx.example.com @localhost
    ;; global options: +cmd
    ;; Got answer:
    ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 17040
    ;; 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: 4096
    ;; QUESTION SECTION:
    ;nginx.example.com.		IN	A

    ;; ANSWER SECTION:
    nginx.example.com.	300	IN	A	172.17.255.1

    ;; Query time: 41 msec
    ;; SERVER: 127.0.0.1#53(127.0.0.1)
    ;; WHEN: Tue Dec 17 11:09:21 PST 2019
    ;; MSG SIZE  rcvd: 79

Aレコードとして登録されていることが確認できました。
それでは、実際にcurlでアクセスしてみましょう。

    $ curl http://nginx.example.com
    <!DOCTYPE html>
    <html>
    <head>
    <title>Welcome to nginx!</title>
    <style>
        body {
            width: 35em;
            margin: 0 auto;
            font-family: Tahoma, Verdana, Arial, sans-serif;
        }
    </style>
    </head>
    <body>
    <h1>Welcome to nginx!</h1>
    <p>If you see this page, the nginx web server is successfully installed and
    working. Further configuration is required.</p>

    <p>For online documentation and support please refer to
    <a href="http://nginx.org/">nginx.org</a>.<br/>
    Commercial support is available at
    <a href="http://nginx.com/">nginx.com</a>.</p>

    <p><em>Thank you for using nginx.</em></p>
    </body>
    </html>

次にブラウザからアクセスできるか確認してみましょう。
ブラウザのアドレスバーに"http\://nginx.example.com"というURLを入力します。

<img width="1792" alt="NGINX_from_browser.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/549683/1c54be3c-880f-223c-92b6-513c73ac4edb.png">


ブラウザからもアクセスできました。


## まとめ

以上のように、ローカルマシン(Mac)上で、kindを用いてKubernetesクラスタを構築し、socatコンテナを使って、ローカルマシンからKubernetesクラスタ内にアクセスできるようにできました。そして、ローカルマシン上で動作させたCoreDNS/etcdと、ExternalDNSを連携させることで、ローカルマシン上でDNSでのKubernetesクラスタ上にデプロイしたサービスにアクセスできるようになりました。

本稿では触れていませんが、これを発展させることで、ローカルマシン上でDNSで振り分けるIngress Controllerのテストなども簡単にできると思います。
これで、Kubernetesを色々試してみてはいかがでしょうか。

## 参考文献
  
  * [Kubernetes ingress on kind (Kubernetes IN Docker)](https://banzaicloud.com/blog/kind-ingress/)
  * [kind（Kubernetes IN Docker）で Feature Gates を設定する方法 - Qiita](https://qiita.com/ryysud/items/eb8f7ff5663c948e59b1)
  * [KIND and Load Balancing with MetalLB on Mac](https://www.thehumblelab.com/kind-and-metallb-on-mac/#_)
  * [Using MetalLb with Kind](https://mauilion.dev/posts/kind-metallb/)