Raspberry Pi Zeroで動くKubernetesのARMバイナリを何とかする

#はじめに
[ClusterHATを使って世界最小Kubernetesクラスタを構築する - インストール編その2]でRaspberry Pi Zero上にノード構築を始めたが、いきなりarmの配布バイナリが動かない（Illegal instructionが発生）という事態に遭遇した。調べてみたがPi ZeroをKubernetesクラスタに参加させるという事を試す人がいないのかなかなか解決策が見つからない。試行錯誤の後に実際にPi Zeroで動くバイナリのクロスコンパイルをすることが出来た。

今回はその方法と経緯を共有したい。
#環境
Kubernetes: 10.2
Go:19.4
Build Environment: wsl
#Pi Zeroはオフィシャルサポート対象外
Raspberry PiはARMという種類のRISC CPUを採用している。
KubernetesのARMのオフィシャルサポートはv1.3.0からあり、他のプラットフォーム向けバイナリと同様にリリース時にARM用のバイナリも提供されるようになった。このバイナリを使えばRaspberry Piに持っていくだけで環境を構築する事が出来る。

しかしながら、Pi Zeroでは動かない。
Pi ZeroはPi3 Model Bと同じARMのではあるんだがバージョンが異なる。最近良くKubernetesクラスタのサーバーとして使われているPi 3 Model BはARM v7だがPi Zeroは組み込みで使われるARM v6のCPUが使われている。

```bash
# Raspberry Pi3 B
 $ uname -a
Linux controller 4.14.34-v7+ #1110 SMP Mon Apr 16 15:18:51 BST 2018 armv7l GNU/Linux

# Raspberry Pi Zero
$ uname -a
Linux p1 4.14.34+ #1110 Mon Apr 16 14:51:42 BST 2018 armv6l GNU/Linux
```

このバージョンの差は全体としてはさほど大きくはないが、決定的な違いとして浮動小数点の演算方法が異なる。具体的にはV7以降で導入された[VFPというコプロセッサ](http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0205ij/CJAGBDFI.html)で、浮動小数点の演算をハードウェアによって最適化している。この為全く互換性が無い。
#昔はARM v6で動いていたが、v.1.6.0でサポート終了
v.1.5.xまではPi ZeroでもPi3 Bでも動くARM用バイナリがオフィシャルのリリースで提供されていたが、[ここでIssueとして挙がっている](https://github.com/kubernetes/kubeadm/issues/253)ようにv.1.6.0からARM v7用に最適化された提供となった。このバージョンではPi Zeroで実行するとIllegal instructionが発生する。

[Multi-architecture plan for Kubernetes #38067](https://github.com/kubernetes/kubernetes/issues/38067)

実際の変更はこの[PR 38926](https://github.com/kubernetes/kubernetes/pull/38926)に含まれる。見ていくと、さほどニーズの無いARM v6のサポートはコードのクリーンアップの名目で対象外となった。
#ビルド
[Advanced Standalone Kubelet tutorial for Raspberry Pi](https://povilasv.me/raspberrypi-kubelet/)を参考にさせてもらった。今回ターゲットとしてるKubernetes 10.2をビルドする為に多少変更を加えて進める。

まずはソースを取得する。

```bash
# 使用する環境変数の設定
$ KUBEREPO="k8s.io/kubernetes"
# gitでマスタを取得
$ go get -d $KUBEREPO
# ソースのルートに移動
$ cd $GOPATH/src/$KUBEREPO
# 10.2をチェックアウト
$ git checkout tags/v1.10.2
```

ARMの場合、クロスコンパイルするにもまずは現在のプラットフォーム上でビルドした上でツールチェーンを用いてARM向けにビルドしなければいけない。まずは現環境向けに単純に：

```bash
$ make
```

としてビルドする。

次にGNU ARMツールチェーンをインストールする。

```bash
$ sudo apt-get install gcc-arm-linux-gnueabi
```

冒頭に紹介したチュートリアルではgunueabiではなくgnueabihfをインストールしているが、こちらはARMv7向けである為VFPを使用しない下位互換のgnueabiものをインストールする。

次に、ビルドスクリプトの一部に変更を加える。これは強制的にARMv6向けにコンパイルするためにGoコンパイラに渡す引数を細工している。この為手を加えたものはARMv7でも稼働するがVFPを含めたエンハンスメントが利用できない下位互換のバイナリとなる。ただ、ARMv7用（例えばRaspberry Pi 3 Model B）であれば公式にバイナリが提供されているので、そちらをそのまま使うことが出来る。

ファイルは/hack/libディレクトリ配下にあるgolang.sh。2箇所変更、いずれもset_platform_envs()に加える：

```bash:golang.sh
...
-  export GOOS=${platform%/*}
-  export GOARCH=${platform##*/}
+  export GOOS=${platform%/*}
+  export GOARCH=${platform##*/}
+  export GOARM=5
...
-    case "${platform}" in
-      "linux/arm")
-        export CGO_ENABLED=1
-        export CC=arm-linux-gnueabihf-gcc
+    case "${platform}" in
+      "linux/arm")
+        export CGO_ENABLED=1
+        export CC=arm-linux-gnueabi-gcc
...
```

ここで注意したいのが、指定するGOの環境変数GOARMのバージョンを5としている点である。これは6を指定した場合にはビルド時にリンカーエラーが発生し上手くビルド出来なかった。色々試した結果、下位互換の5だと上手くコンパイル&動くバイナリが用意できたのでここはあえて5を指定している。6を指定して発生するリンカーエラーの原因はVFP関連であることまでは察しがついたが、結局のところ唯一上手くいった対策が5に指定する事だった。

この後、必要となるバイナリ（kubelet、kubectl、kube-proxy）を個々にビルドする。

```bash:
make all WHAT=cmd/kube-proxy KUBE_VERBOSE=5 KUBE_BUILD_PLATFORMS=linux/arm
make all WHAT=cmd/kubelet KUBE_VERBOSE=5 KUBE_BUILD_PLATFORMS=linux/arm
make all WHAT=cmd/kubectl KUBE_VERBOSE=5 KUBE_BUILD_PLATFORMS=linux/arm
```

生成されたバイナリは_output/local/bin/linux/arm/に生成される。これを該当Nodeの/usr/local/binに配置すれば完了。
#おわりに
今回は英語でも回答が見つからず困っている人が他にもいたので、GitHubの[Kubeadm broken on armv6l #253](https://github.com/kubernetes/kubeadm/issues/253)にも同様の回答を記載した。あまりPi ZeroでKubernetesのNodeを動かそうという人はいないんだろうが、ClusterHATでKubernetesで構築する上では必須の工程となる。何かの参考になれば幸いである。