KubernetesのIPv6シングルスタックのクラスタにマイクロサービスをデプロイしようとしているが結構大変

この記事は [Kubernetes Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kubernetes) の3日目の記事です。
今日の分が空いていることに気がついたので前日夜から深夜テンションで執筆しています。


## はじめに

はじめまして、大学院でクラウド技術やネットワークなどについて研究している[@eppppi](https://twitter.com/eppppi_)です。
IPv6シングルスタックのKubernetesクラスタがあったら検証環境として嬉しいよね、という話が研究室で出てきたため、有志の勉強会([@daisuke_k](https://twitter.com/daisuke_k), [@PiBVT](https://twitter.com/PiBVT),   [@hiroyanoe](https://twitter.com/hiroyanoe)ほか)にて取り組むことになりました。

そこで、現在進行形でIPv6 onlyなKubernetesクラスタ構築やアプリケーションのIPv6対応にあたって経験していることまとめました。

※IPv6シングルスタッククラスタ構築のための詳しい手順は述べていません
※記事中の意見・感想などは筆者個人のものです


## 環境・使用ツール
- VM 3台構成（それぞれ Ubuntu22.04）
    - Master Node x 1
    - Worker Node x 2
- kubeadm: v1.25.2
- CNI: Calico


## 背景
弊研究室ではクラスタ周りのネットワークに興味がある人が多く、その中で検証用にIPv6シングルスタックのKubernetesクラスタがあると嬉しいということになりました。そこで、「IPv6シングルスタックでマイクロサービスアプリケーションを正常に動かす」をことを当面の目標として有志の勉強会で取り組んでいます。


## クラスタ構築
早速IPv6シングルスタッククラスタ作成に取り掛かります。
なお以下ではクラスタ構築処理の一部のみを記したもののため、コピペして構築できる感じではありません。参考程度に留めていただければと思います。
詳しい構築方法は機会があればまた別の記事にしたいと考えています。

また、前提条件として、各ノードがIPv6で疎通可能であり、有効なIPv6サブネットを有している必要があります。

### kubeadm init, join
今回はVMを３ノード準備し、kubeadmを用いて手元でクラスタを構築しました。単純に`kubeadm init`するとデフォルトではAPI serverのエンドポイントやPodCIDRなどにIPv4アドレスが利用されてしまうため、IPv6対応にするためにこれらを手動で与える必要があります。
```bash
# on master node (e.g.)
kubeadm init --pod-network-cidr=XX:XX:XX::/AA --service-cidr=YY:YY:YY::/BB --apiserver-advertise-address=ZZ:ZZ:ZZ::ZZ
```

`kubeadm init`時に出力されたコマンドをWorkerノードで実行
```bash
# on worker nodes
kubeadm join 〜〜〜
```


### Calicoをデプロイ
CNIプラグインとしてはじめ[Cilium](https://cilium.io/)を利用しようとしましたが、うまくいかなかったためIPv6 onlyの設定方法がドキュメントに記載されていた[Calico](https://www.tigera.io/project-calico/)を用います。

[Configure dual stack or IPv6 only](https://projectcalico.docs.tigera.io/networking/ipv6) の"Enable IPv6 only"→"Manifest"に沿って進めていきます。
- CalicoのバイナリとKubernetesマニフェスト`calico.yaml`を取得
- `calico.yaml`にてIPv4を無効化、IPv6を有効化
  ```json
  "ipam": {
      "type": "calico-ipam",
      "assign_ipv4": "false",
      "assign_ipv6": "true"
  },
  ```
- このマニフェストをapply

このほかにも、v4アドレスが割り当てられているオブジェクトを手動でv6アドレスに書き換えるなどの処理を行うことでなんとかIPv6でK8sのコンポーネントが立ち上がってきました。
kube-apiserverやetcd, 各Pod, ServiceなどがIPv6のみで立ち上がっていれば成功です👏


## 各種マイクロサービスを動かしてみる
無事にIPv6 onlyのクラスタを構築できたため、次にその上でマイクロサービスアプリケーションが正常に動作するか確認していきます。


### Sock Shop
[Sock Shop : A Microservice Demo Application](https://github.com/microservices-demo/microservices-demo)
デモ用のマイクロサービスアプリケーションとして広く用いられているものです。しかし、しばらく開発がストップしている様子で、利用されている各サービスのイメージも数年前にビルドされたものが多いです。


```bash
kubectl apply -f deploy/kubernetes/complete-demo.yaml
```
用意されているマニフェストを単純にデプロイすると、いくつかエラーに遭遇します。

`user` PodがCrashLoopBackOffとなっているのでログを確認すると、以下のエラーが出ています。
```
caller=main.go:67 err="dial udp 8.8.8.8:80: connect: network is unreachable"
```

Userサービスの実装を読むと、以下のようになっています。(https://github.com/microservices-demo/user/blob/ea7bc23723af8452c0c7c4c8f21f33a568c431a2/main.go#L65)
```go
// Find service local IP.
conn, err := net.Dial("udp", "8.8.8.8:80") // ここでエラー
(snip)
localAddr := conn.LocalAddr().(*net.UDPAddr)
host := strings.Split(localAddr.String(), ":")[0]
```
どうやら外に到達できるローカルアドレスを取得するために`8.8.8.8`に接続しに行こうとするようです。
とりあえずの回避策として、v4,v6両方で解決できるできるようにクラスタ内のドメイン名を指定するよう変更し、userイメージをビルドし直しました。
```diff
-conn, err := net.Dial("udp", "8.8.8.8:80")
+conn, err := net.Dial("udp", "kubernetes.default.svc.cluster.local:80")
```
また、user-db内で動いているmongodがデフォルトではIPv6をリッスンしないということでこちらもビルドし直しました。

これによって全PodがRunningとなりましたが、今度はページにアクセスしてユーザ登録処理するとエラーを吐いてPodが落ちるという現象に遭遇し、現在も取り組んでいます。


### Online Boutique

Sock Shopがうまく動かないため、並行して他のサンプル用マイクロサービスアプリケーションも試してみることになりました。

[GoogleCloudPlatform/microservices-demo](https://github.com/GoogleCloudPlatform/microservices-demo)
こちらはGCPチームのリポジトリで開発されているマイクロサービスのデモアプリです。他のデモアプリと異なり、現在も継続的にメンテナンスされている様子です。

こちらも同様にデプロイして確認してみますが、
```bash
kubectl apply -f ./release/kubernetes-manifests.yaml
```
ページにアクセスすると、`HTTP/1.1 500 Internal Server Error`とエラーが返ってきます。

ログを確認して原因を探ると、どうやらこちらもアプリケーションがIPv4アドレス前提でコーディングされていることが原因のようです。

原因箇所：[https://github.com/GoogleCloudPlatform/microservices-demo/blob/573f4db73306b779426fa47a7df7ab682188abea/src/currencyservice/server.js#L172](https://github.com/GoogleCloudPlatform/microservices-demo/blob/573f4db73306b779426fa47a7df7ab682188abea/src/currencyservice/server.js#L172)など複数のサービスのサーバ
```js
server.bindAsync(
  `0.0.0.0:${PORT}`, // <- ここ
  grpc.ServerCredentials.createInsecure(),
  function() {
    logger.info(`CurrencyService gRPC server started on port ${PORT}`);
    server.start();
  },
); 
```

この問題を[issue](https://github.com/GoogleCloudPlatform/microservices-demo/issues/1221)として立てたところ(issuerは[@PiBVT](https://twitter.com/PiBVT)さん)、「IPv6シングルスタックは一般的じゃないし優先度は低いかな」（意訳）とコメントをいただき「確かに...」と納得するしかなかったのですが、つい昨日進展があり、修正のPRが出されていました。(https://github.com/GoogleCloudPlatform/microservices-demo/issues/1221#issuecomment-1334481172 )。

なお、`0.0.0.0`を`[::]`に書き換えるなどしてこの問題を解決した後も、新たにIPv6を用いた際のASP.NET CoreとRedis周りでエラーが出ており、それを修正すべく[PR](https://github.com/GoogleCloudPlatform/microservices-demo/pull/1340)に取り組んでいます。



## まとめ・個人の感想

IPv6シングルスタックのKubernetesクラスタを実験的に構築し、マイクロサービスアプリケーションをそのクラスタ上で動作するように試行錯誤しました（しています）。
クラスタ構築自体は思っていたより簡単に完了したのですが、その上で動くマイクロサービスアプリケーションをIPv6(シングルスタック)に対応させるのが想定していたよりも煩雑で、エラーの連続に悩まされています。今回記載していないことでもつまづいた点が多くありました。
思っていたよりも世の中はまだIPv4前提なのだな、と色々と学びのある経験となっています。


## おわりに

Work in Progressな内容とはなってしまいましたが、IPv6(シングルスタック)対応した経験について記しました。
少しでも今後KubernetesでIPv6シングルスタックやってみようというという方(いるのかな？)の参考になれば幸いです。


## 参考
- [Calico](https://www.tigera.io/project-calico/)
- [Configure dual stack or IPv6 only](https://projectcalico.docs.tigera.io/networking/ipv6)
- [Sock Shop : A Microservice Demo Application](https://github.com/microservices-demo/microservices-demo)
- [Online Boutique](https://github.com/GoogleCloudPlatform/microservices-demo)