自宅 k8s クラスタのサービスに FQDN で繋がるようにした

自宅の検証用マシン (Deskmini A300) に ESXi を入れて検証環境として利用しています。

最近はそこへ k8s クラスタを構築して色々試しているのですが、クラスタ内に立ち上げたサービスへは IP アドレスでアクセスしていました。 IP アドレスでアクセスするのはとても面倒だったのですが、やっと k8s で動かしているサービスに FQDN で繋がるようになったので投稿します。

システム構成図

完成後のシステム構成図になります。 (構成図描くの下手で分かりにくいと思います…)

f:id:nnstt1:20201113012536p:plain

見てもらって分かる通り、LAN 用の DNS サーバを VM で建てています (k8s クラスタの外です)。 これは、k8s クラスタを構築する前に DNS サーバ (dnsmasq) を構築していて、それを流用しているためです。

使用するプロダクト

今回利用しているプロダクトは以下になります。

名前 バージョン 用途
Kubernetes 1.19.0 コンテナオーケストレーション
Docker 19.03.12 コンテナランタイム
ExternalDNS 0.7.3 DNS プロバイダに DNS を登録
CoreDNS 1.8.0 DNS サーバ
etcd 3.4.13 DNS レコード格納
MetalLB 0.9.3 ベアメタルロードバランサー

構築

以下の手順で環境を構築しました。

  1. DNS サーバ (CoreDNS & etcd) 構築
  2. MetalLB デプロイ
  3. ExternalDNS デプロイ

DNS サーバ (CoreDNS & etcd) 構築

CoreDNS & etcd を使った DNS サーバを構築します。 この DNS サーバに k8s クラスタのサービス用の DNS レコードを格納していきます。 なぜ dnsmasq を流用しないかというと、後述する ExternalDNS が dnsmasq に対応していないからです。

今までは dnsmasq を yum でインストールしていたのですが、今回からは Docker コンテナを使っていきます。 docker のインストールは省略します。 (Docker Compose を使えばよかったかも)

コンテナネットワーク

CoreDNS コンテナから etcdコンテナへ繋がるようにするために dns-network という名前の bridge を作成しておきます。

$ docker network create --driver bridge dns-network

CoreDNS

CoreDNS とは、Go 言語で書かれた DNS サーバで、 CNCF によってホストされているプロジェクトです。 プラグイン形式で DNS レコードの格納/参照先を柔軟に設定できるところが売りなようです。

今回は etcd プラグインと hosts プラグインを使って、「k8s のサービスは etcd、その他は /etc/hosts に直書き」という処理をさせます。

まずは CoreDNS の設定ファイル Corefile を準備します。 仮に example.com をローカルで使用するドメインとして、example.com の問い合わせは以下の順番で確認するようにします。

  1. etcd (k8s サービス用)
  2. /etc/hosts

example.com 以外の問い合わせは /etc/resolv.conf を参照して外部の DNS サーバに回します。

#Corefile
example.com {
    etcd {
        path /skydns
        endpoint http://etcd:2379
        fallthrough
    }
    hosts
    cache
    errors
    log
}

. {
    forward . /etc/resolv.conf
}

次に、CoreDNS コンテナを起動します。 先ほど作成した Corefile/etc/hosts をマウントしています。

$ docker run -d \
    --name coredns \
    --network dns-network \
    -v /home/nnstt1/Corefile:/root/Corefile \
    -v /etc/hosts:/etc/hosts \
    -p 53:53/udp \
    -p 53:53/tcp \
    coredns/coredns:1.8.0 -conf /root/Corefile

etcd

etcd とは、こちらも Go 言語で書かれた分散 KVS (Key Value Store) です。 今回は CoreDNS で管理する DNS レコードの格納先としてシングルノードで動かします。

こちらは設定ファイルの用意はなく、etcd コンテナを起動するだけです。

$ mkdir -p /tmp/etcd-data.tmp

$ docker run -d \
    -p 2379:2379 \
    -p 2380:2380 \
    --mount type=bind,source=/tmp/etcd-data.tmp,destination=/etcd-data \
    --name etcd \
    --network dns-network \
    gcr.io/etcd-development/etcd:v3.4.13 \
    /usr/local/bin/etcd \
    --name s1 \
    --data-dir /etcd-data \
    --listen-client-urls http://0.0.0.0:2379 \
    --advertise-client-urls http://0.0.0.0:2379 \
    --listen-peer-urls http://0.0.0.0:2380 \
    --initial-advertise-peer-urls http://0.0.0.0:2380 \
    --initial-cluster s1=http://0.0.0.0:2380 \
    --initial-cluster-token tkn \
    --initial-cluster-state new \
    --log-level info \
    --logger zap \
    --log-outputs stderr

動作確認

CoreDNSetcd のコンテナが両方とも起動して、DNS サーバとして動作するか確認します。

まずはコンテナが起動しているか。

$ docker ps
CONTAINER ID        IMAGE                                  COMMAND                  CREATED             STATUS              PORTS                                    NAMES
8064ae23c30c        gcr.io/etcd-development/etcd:v3.4.13   "/usr/local/bin/etcd…"   2 hours ago         Up 2 hours          0.0.0.0:2379-2380->2379-2380/tcp         etcd
e0bce2b55549        coredns/coredns:1.8.0                  "/coredns -conf /roo…"   17 hours ago        Up 17 hours         0.0.0.0:53->53/tcp, 0.0.0.0:53->53/udp   coredns

コンテナ起動していたら、「etcd へ DNS レコードを登録できるか」「登録した DNS レコードを参照できるか」を確認します。

# etcd への DNS レコード登録 (OK が返ってくれば成功)
$ docker exec -it etcd etcdctl put /skydns/com/example/hoge '{"host":"192.168.1.254","ttl":3600}'
OK

# DNS レコード問い合わせ (上記で設定したアドレスが返ってくれば成功)
$ dig +short hoge.example.com @localhost
192.168.2.254

無事に etcd を使った DNS サービスが動作しているようです。

次に、/etc/hosts が参照できるか確認します。

# /etc/hosts へレコード追加
$ sudo sh -c "echo '192.168.1.253 fuga.example.com' >> /etc/hosts"

# DNS レコード問い合わせ (上記で設定したアドレスが返ってくれば成功)
$ dig +short fuga.example.com @localhost

こちらは期待した結果が返ってきませんでした。 どうやら /etc/hosts の編集内容がコンテナのほうに同期されていないようです。 理由は分かっていないのですが、コンテナを再起動することで反映されました。

$ docker restart coredns
coredns
$ dig +short fuga.example.com @localhost
192.168.1.253

これで DNS サーバの構築が完了しました。

MetalLB

MetalLB とは、AKS や GKE などのマネージド k8s と同じようにベアメタル k8s クラスタでも type: LoadBalancer の Service リソースを使えるようにしてくれるロードバランサーの実装です。 k8s クラスタに MetalLB をデプロイすることで、type: LoadBalancer のリソースに対して自動的に外部 IP アドレスを払い出してくれます。

今回は MetalLB のデプロイ方法は省略します。

ExternalDNS

いよいよ本命の ExternalDNS です。 ExternalDNS を利用することで、Kubernetes の Service や Ingress で公開される IP アドレスを Kubernetes クラスタ外部の DNS プロバイダに自動登録して、FQDNKubernetes クラスタ内のサービスにアクセスできるようになります。

執筆時点の v0.7.3 で選択できる DNS プロバイダ は 27 個あります。 Azure DNS や Route 53 など、対応している DNS プロバイダは他にもあるのですが、オンプレに構築していみたいという目的から CoreDNS を選択しました。

デプロイ

DNS プロバイダ毎にチュートリアルが用意されています。 CoreDNS 用のチュートリアルはこちら。

github.com

CoreDNS と etcd は既に構築しているため ExternalDNS のデプロイのみ参照しました。 以下のマニフェストをデプロイします。

---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: external-dns
rules:
  - apiGroups: [""]
    resources: ["services","endpoints","pods"]
    verbs: ["get","watch","list"]
  - apiGroups: ["extensions","networking.k8s.io"]
    resources: ["ingresses"]
    verbs: ["get","watch","list"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["list"]
  - apiGroups: [""]
    resources: ["endpoints"]
    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: k8s.gcr.io/external-dns/external-dns:v0.7.3
        args:
        - --source=service
        - --provider=coredns
        - --log-level=debug
        env:
        - name: ETCD_URLS
          value: http://192.168.2.3:2379

これにより、namespace: kube-system で ExternalDNS の Pod が動くようになりました。

アノテーション付与

ExternalDNS で対象とする Service リソースにアノテーションを付与します。 これにより、ExternalDNS がアノテーション内のホスト名と Service が持つ 外部 IP アドレスを CoreDNS へ登録してくれるようになります。

$ kubectl annotate svc <svc_name> "external-dns.alpha.kubernetes.io/hostname=<svc_name>.example.com."
service/<svc_name> annotated

動作確認

DNS サーバ構築時と同じく dig で確認します。 サンプルで動かしている WordPress独自ドメインを対象にしてみます。

# Service 確認
$ kubectl get svc -n wordpress
NAME              TYPE           CLUSTER-IP       EXTERNAL-IP     PORT(S)        AGE
wordpress         LoadBalancer   10.110.128.227   192.168.2.247   80:30446/TCP   56d
wordpress-mysql   ClusterIP      None             <none>          3306/TCP       56d

# アノテーション付与
$ kubectl annotate svc wordpress "external-dns.alpha.kubernetes.io/hostname=wordpress.nnstt1.work." -n wordpress
service/wordpress annotated

# dig
$ dig +short wordpress.nnstt1.work
192.168.2.247

無事に外部 IP アドレスを参照することができました。 キャプチャは無いのですが、別マシンのブラウザから WordPress を表示することもできました。

これで、IP アドレスを使わずに FQDNk8s クラスタのサービスにアクセスすることができるようになりました。

おわりに

以上が k8s クラスタのサービスに FQDN で繋がるようにした手順です。

ExternalDNS を使うことがメインの目的だったのですが、CoreDNS と etcd を触る機会も得られました。

ExternalDNS も思ってたより簡単に導入できたので、同じような悩みを持っている方はぜひ検討してみてください。

アノテーション付与を手動でする必要がある、という課題があるので、今後は自動的にアノテーションを付与する仕組みを調べたいと思います。