Kubernetes クラスターを自力で組んでトラブルシューティングしてみる【The Hard Way】【ニフクラ】

この記事は[富士通クラウドテクノロジーズ Advent Calendar 2021](https://qiita.com/advent-calendar/2021/fjct)の8日目の記事です。

7日目は @HanchA の「[Postfix, Dovecot, Zabbixを入れてサーバーを監視してみる](https://qiita.com/HanchA/items/d921ac84fa63fe1d2af1)」でした。同期が配属先で身に着けた運用の知識を使って、ゼロから環境構築をしてアウトプットできていてすごいと思いました。私も負けずに頑張りたいと思います :muscle:

本日は2021年度の新入社員、加藤がお届けします。新人研修期間に [Hatoba](https://pfs.nifcloud.com/service/kubernetes-service-hatoba.htm) (弊社のマネージド Kubernetes サービスの名称) の業務を体験させてもらったときに Kubernetes について色々勉強しました[^1]。CKA 取得に向けまだまだ勉強中ですが、今日はその中の一つである Kubernetes The Hard Way について、ここにアウトプットしていこうと思います！

[^1]: 他には「[ニフクラ Kubernetes Service Hatoba の新機能 LoadBalancer を使って Elasticsearch / Kibana をデプロイしてみた](https://blog.pfs.nifcloud.com/elasticsearch-kibana-with-hatoba-loadbalancer)」という記事も社内ブログに書きました。

## 対象読者

* Kubernetes 聞いたことあるけど全然分かんない！という人
* [CKA (Certified Kubernetes Administrator)](https://training.linuxfoundation.org/ja/certification/certified-kubernetes-administrator-cka/) の資格取得を考えている人
* Kubeadm とかでクラスターは作ったことあるけど中身をちゃんと知りたい人

## Kubernetes The Hard Way とは？

[Kubernetes The Hard Way](https://github.com/kelseyhightower/kubernetes-the-hard-way) とは、自力で Kubernetes のクラスターを作るためのチュートリアルです。初心者向けの学習タスクとして位置づけられており、手順に従って Kubernetes クラスターの構成要素をひとつずつ組み立てていくことで、クラスターの内部の構造を理解することができるようになります。

「Kubernetes The Hard Way をやろう！」と先輩トレーナーに言われた時点では私の Kubernetes の知識はゼロで、非常にハードルが高く感じましたが、やり終えるころには「あー Kubernetes 完全に理解したわ～」という気持ちになれました。

## 事前準備: Kubernetes の構成図を頭に叩き込む

[Kubernetesのコンポーネント](https://kubernetes.io/ja/docs/concepts/overview/components/) というページがあり、事前にこのページを見ておくことでチュートリアルをやっている時の理解度がより深まった気がしました。

コントロールプレーンには etcd や kube-apiserver がいて、ワーカーノードには kubelet がいる……。 kube-apiserver はユーザーが kubectl などのクライアントから送ってきたリクエストを受け付けていて、 kubelet はワーカーノードで実際のコンテナの指揮をしたり、 kube-apiserver との通信をしているんだ……みたいな感じでイメージをつけていきました。

![components-of-kubernetes](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/451958/2ed1d8fd-bcf9-62e2-956c-3501d8df7f0f.png)

## The Hard Way を往く

作業ログはあまりにも長いので、 GitHub のプロジェクトにまとめています。

[kubernetes-the-hard-way-nifcloud](https://github.com/SogoKato/kubernetes-the-hard-way)

### 環境

* ニフクラ VM (e-medium4)
* プライベートLAN
* マルチロードバランサー
* Ubuntu 20.04
* Kubernetes 1.21.1

## 苦労したポイント

恥を忍びつつ、躓きから得た教訓や学びをいくつかご紹介しようと思います。初心者なので間違っていることがありましたらご指摘いただけると嬉しいです。

* ネットワーク構成
* kube-apiserver から kubelet への通信 (kube-apiserver の設定)
* CoreDNS がループしてしまい名前解決に失敗する問題
* Pod が名前解決に失敗する問題 (kube-proxy)

### ネットワーク構成

Kubernetes は複数台のノード (物理サーバーや VM) を協調させて、アプリをデプロイするためのものですので、ネットワーク構成は肝となる部分です。

クラスター構築の終盤、コントロールプレーン上のコンポーネントを起動させ、ワーカーノード上のコンポーネントも起動させた後に、ノードの状態を確認してみると `NotReady` になっていたりします。
kubelet のログを確認してみると、kubelet から kube-apiserver へのノードの登録に失敗していることが分かりました。ファイアウォールやルーティングの設定を見直してみて、ネットワークの疎通に問題がないか確認してみるのが基本のトラブルシューティングのようです。

私の場合は、ワーカーからロードバランサー経由で kube-apiserver に到達した通信が、ルーティングの設定がおかしくなっていたことで、ロードバランサーを経由せずに返ってくることが原因でした[^2]。

私はニフクラ上で構築しましたが、私のネットワーク知識が少なかったことも相まって、 GCP からニフクラへの読み替えに苦労して、何度も試行錯誤を繰り返しました。
しかしそのおかげで、ネットワーク全般においてもニフクラの使い方においても、知識がだいぶ増えたので結果オーライです ^^

[^2]: 当時はマルチロードバランサーをグローバルと Kubernetes 用のプライベートLAN　との 2-arm 構成になっており、プライベートLAN でデフォルトゲートウェイに設定されていたルーターもまたグローバルにつながっていました。ワーカーノードがマルチロードバランサーを介して kube-apiserver にアクセスした時、戻りの通信はロードバランサーに流れず、ルーターを介して応答していたので通信に失敗していたというオチです。解決法としては、マルチロードバランサーを 1-arm 構成（グローバルのみ）にすることで直りました。

### kube-apiserver から kubelet への通信 (kube-apiserver の設定)

kubectl でクラスター操作をしているときは、 kubectl が kube-apiserver にアクセスし、 kube-apiserver が kubelet に対して指示を送っています。なので例えば、 `kubectl  exec` をしたときにうまくコンテナに接続できなければ、まず kube-apiserver と kubelet 間の接続を疑ってみることが必要です。

私の環境の場合は `/etc/hosts` などを設定していなかったので kube-apiserver が `worker0` などのホスト名で kubelet にアクセスしようとすると名前解決ができずに失敗します。
これは kube-apiserver の設定を変更することで回避できます。 Kubernetes The Hard Way では systemd のサービスとして kube-apiserver を起動しているため、[第8章で作成している systemd の unit ファイル](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/08-bootstrapping-kubernetes-controllers.md#configure-the-kubernetes-api-server)に項目を追加することで変更ができます。

`--kubelet-preferred-address-types=InternalIP,ExternalIP` を追加し、名前ではなく IP を使うようにしてあげます。

```kube-apiserver.service
cat <<EOF | sudo tee /etc/systemd/system/kube-apiserver.service
[Unit]
Description=Kubernetes API Server
Documentation=https://github.com/kubernetes/kubernetes

[Service]
ExecStart=/usr/local/bin/kube-apiserver \\
  --advertise-address=${INTERNAL_IP} \\
  --allow-privileged=true \\
  --apiserver-count=3 \\
  --audit-log-maxage=30 \\
  --audit-log-maxbackup=3 \\
  --audit-log-maxsize=100 \\
  --audit-log-path=/var/log/audit.log \\
  --authorization-mode=Node,RBAC \\
  --bind-address=0.0.0.0 \\
  --client-ca-file=/var/lib/kubernetes/ca.pem \\
  --enable-admission-plugins=NamespaceLifecycle,NodeRestriction,LimitRanger,ServiceAccount,DefaultStorageClass,ResourceQuota \\
  --etcd-cafile=/var/lib/kubernetes/ca.pem \\
  --etcd-certfile=/var/lib/kubernetes/kubernetes.pem \\
  --etcd-keyfile=/var/lib/kubernetes/kubernetes-key.pem \\
  --etcd-servers=https://10.240.0.10:2379,https://10.240.0.11:2379,https://10.240.0.12:2379 \\
  --event-ttl=1h \\
  --encryption-provider-config=/var/lib/kubernetes/encryption-config.yaml \\
  --kubelet-certificate-authority=/var/lib/kubernetes/ca.pem \\
  --kubelet-client-certificate=/var/lib/kubernetes/kubernetes.pem \\
  --kubelet-client-key=/var/lib/kubernetes/kubernetes-key.pem \\
  --kubelet-preferred-address-types=InternalIP,ExternalIP \\
  --runtime-config='api/all=true' \\
  --service-account-key-file=/var/lib/kubernetes/service-account.pem \\
  --service-account-signing-key-file=/var/lib/kubernetes/service-account-key.pem \\
  --service-account-issuer=https://${KUBERNETES_PUBLIC_ADDRESS}:6443 \\
  --service-cluster-ip-range=10.32.0.0/24 \\
  --service-node-port-range=30000-32767 \\
  --tls-cert-file=/var/lib/kubernetes/kubernetes.pem \\
  --tls-private-key-file=/var/lib/kubernetes/kubernetes-key.pem \\
  --v=2
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
```

### CoreDNS がループしてしまい名前解決に失敗する問題

私の場合は上の対処で kube-apiserver から kubelet への通信に成功してもなお、[第12章でセットアップした DNS アドオンの検証](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/12-dns-addon.md#verification)がうまくいきませんでした。

CoreDNS の状態を確認すると、 `CrashLoopBackOff` で起動に失敗し続けていることがわかりました。Ubuntu 18.04 以降でローカルの DNS スタブリゾルバ (systemd-resolved) が使われるようになったことがその原因です。

Kubernetes The Hard Way のデフォルトの設定値では、 kubelet が名前解決のために `/run/systemd/resolve/resolv.conf` を見に行くよう設定されています。 kubelet はその設定値に基づき、該当ファイルを CoreDNS のコンテナに渡します。ですが、そのファイルには `127.0.0.1` が指定されているため、結果的にループしてしまいクラッシュします。つまり、 `127.0.0.1` ではなく `8.8.8.8` などの外部 DNS を参照するようにすればループの発生を抑えられそうです。

`/run/systemd/resolve/resolv.conf` は systemd-resolved によって生成されるファイルのため直接編集してはいけないので、 `/etc/systemd/resolved.conf` を編集してから、 systemd-resolved のデーモンを再起動することで `/run/systemd/resolve/resolv.conf` を再生成させます。

```sh
{
  NAMESERVER_ADDRESSES=8.8.8.8
  sed -i "s/^DNS=127.0.0.1$/DNS=${NAMESERVER_ADDRESSES}/" /etc/systemd/resolved.conf
  sudo systemctl restart systemd-resolved
}
```

上記のスクリプトを走らせてから、 `sudo systemctl restart kubelet` で kubelet を再起動させれば CoreDNS が無事 `Running` になると思います。

追記: これについて単独の記事を書きました。  
https://sogo.dev/posts/2022/12/kubernetes-coredns-loop

### Pod が名前解決に失敗する問題 (kube-proxy)

これはチュートリアルの終了後に、作ったクラスターで検証していたときに発見したものですが、私の作ったクラスターはなんと Pod が CoreDNS での名前解決に失敗していたのです！ :dizzy_face: （[第12章でセットアップした DNS アドオンの検証](https://github.com/kelseyhightower/kubernetes-the-hard-way/blob/master/docs/12-dns-addon.md#verification)はパスしたはずなので遊んでいるうちに壊した可能性も……。

検証用の Pod 内で `kubectl exec -ti dnsutils -- dig kubernetes` で名前解決を試していたときに、以下のエラーが返ってきました[^3]。

```
;; reply from unexpected source: 10.200.2.177#53, expected 10.32.0.10#53
```

コンテナ内の `/etc/resolv.conf` に `nameserver 10.32.0.10` が設定されており、これが kube-dns の Cluster IP なのですが、実際に Pod が置かれているノードの IP (`10.200.2.177`) で返ってきているためエラーになっています。

この問題は [GitHub の Issue](https://github.com/kubernetes/kubernetes/issues/21613) にも報告されており、同一のノード上に Pod と KubeDNS があるとうまく動かないという問題のようです。
`modprobe br_netfilter` をすることで直ります。詳しくないのでよくわかりませんが、このモジュールをロードすることでいい感じに NAT してくれるようになるんだと思います。
ちなみに、 kube-proxy のオプションにある `--masquerade-all` をつける方法はうまくいきませんでした。

[^3]: see [Debugging DNS Resolution](https://kubernetes.io/docs/tasks/administer-cluster/dns-debugging-resolution/) `kubectl apply -f https://k8s.io/examples/admin/dns/dnsutils.yaml`

## まとめ

こうしてまとめてみてみると、全部ネットワーク周りの問題ですね。ルーティングとか DNS とか。月並みですが、「 Kubernetes のネットワークむずかし〜〜」という感想です :innocent:
あと、 Kubernetes は証明書を各所で使うので、（諸事情によって）ロードバランサーの IP が変わるたびに証明書を全て再生成 & 再配布しなくてはいけないのが結構辛かった。

結構な長文になってしまいましたが、これから Kubernetes The Hard Way する方々のご参考になればと思います！

明日は @ablengawa の「Blenderでそれなりにフォトリアルな画像を生成する」です。お楽しみに！

## おまけ: Sonobuoy を通したい！編

[Sonobuoy](https://sonobuoy.io/) (ソノブイと読みます) という Kubernetes クラスターの end-to-end テストをするツールがあります。これを通すと、 Certified Kubernetes と同等のクラスターと言うことができるので、今回作ったクラスターもテストにかけてみました。

結果は……、**全然ダメ** :no_good: 

デバッグしたりして色々頑張ったのですが、何も分からずに終わりました。弊社のマネージド Kubernetes の Hatoba で作ったクラスターを同テストにかけてみたら、あっさりクリアしたので、「さすが Certified Kubernetes は違うわ〜」と思いました。

まだまだ道のりは長い……。

## 参考文献

[kelseyhightower/kubernetes-the-hard-way](https://github.com/kelseyhightower/kubernetes-the-hard-way)
[Kubernetesのコンポーネント](https://kubernetes.io/ja/docs/concepts/overview/components/)
[クラスターのネットワーク](https://kubernetes.io/ja/docs/concepts/cluster-administration/networking/)
[kube-apiserver](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/)
[kube-proxy](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/)
[coredns/coredns/plugin/loop](https://github.com/coredns/coredns/blob/master/plugin/loop/README.md)
[KubeDNS not working inside of pod when its containers are on the same node with kube-dns containers.](https://github.com/kubernetes/kubernetes/issues/21613)