RaspberryPiにkubernetesをセットアップ

Qiita内でも何件かRaspberryPiにkubernetesをセットアップする記事が上がっているので、簡単に行くものだと思っていたのですが、非常に苦労したので、備忘を含めて記録しておこうと思います。

# 前提
今回はRaspberry Pi 3B+に2018/06/27版のraspbian-strech-liteを使用しました。RaspberryPiのセットアップ手順は省略します。
kubernetesはkubeadm initを使って初期設定を行う手順になります。
文章内で明示しない限りはpiユーザを使って作業を行っています。

# セットアップ
手順としては

 - dockerのインストール
 - kubeadmを使えるようにする
 - kubernetes環境の初期化

となります。

## dockerのインストール
dockerのインストールは公式の手順に則って行います。
詳細は[このあたり](https://docs.docker.com/install/linux/docker-ce/debian/)を参考に、以下の手順でインストールします。
今回インストール時はdockerのバージョンが18.06.1-ceでした。

```
$ sudo apt-get install software-properties-common
$ curl -fsSL https://download.docker.com/linux/raspbian/gpg | sudo apt-key add -
$ echo "deb [arch=armhf] https://download.docker.com/linux/raspbian stretch stable" |  sudo tee /etc/apt/sources.list.d/docker.list
$ sudo apt-get update
$ sudo apt-get install docker-ce
```
## kubeadmを使えるようにする
kubeadm,kubectl,kubeletをインストールします。
公式な手順は[このあたり](https://kubernetes.io/docs/setup/independent/install-kubeadm/)を参考にします。
執筆時は丁度v1.12.0-rc1やらrc2やらが出始めたところでうまく動かない部分もあったのでv1.11.3に固定します。

```
$ curl -fsSL https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update
$ sudo apt-get install kubelet=1.11.3-00 kubeadm=1.11.3-00 kubectl=1.11.3-00
```

そのままコマンドを実行しようとするとエラーになるため、cgroupの設定とswapの設定をします。
cgroupの設定は、`/boot/cmdline.txt`ファイルに`cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1`を追加します。
swapの設定は以下のコマンドを実行します。

```
$ sudo dphys-swapfile swapoff
$ sudo dphys-swapfile uninstall
$ sudo update-rc.d dphys-swapfile remove
```

カーネルの起動オプション変更があるため再起動しておきましょう。

### kubernetes masterノードの初期化にはまる
ここで公式手順通りに進めると`kubeadm init`でmasterの初期化を行います。
flannelを使うため、`--pod-network-cidr=10.244.0.0/16`のオプションのみ使用します。
やってみました・・・

```
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16
[init] using Kubernetes version: v1.11.3
[preflight] running pre-flight checks
I0918 03:00:29.587034     789 kernel_validator.go:81] Validating kernel version
I0918 03:00:29.587889     789 kernel_validator.go:96] Validating kernel config
        [WARNING SystemVerification]: docker version is greater than the most recently validated version. Docker version: 18.06.1-ce. Max validated version: 17.03
[preflight/images] Pulling images required for setting up a Kubernetes cluster
[preflight/images] This might take a minute or two, depending on the speed of your internet connection
[preflight/images] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[preflight] Activating the kubelet service
[certificates] Generated ca certificate and key.
[certificates] Generated apiserver certificate and key.
[certificates] apiserver serving cert is signed for DNS names [vpod1 kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.0.xx]
[certificates] Generated apiserver-kubelet-client certificate and key.
[certificates] Generated sa key and public key.
[certificates] Generated front-proxy-ca certificate and key.
[certificates] Generated front-proxy-client certificate and key.
[certificates] Generated etcd/ca certificate and key.
[certificates] Generated etcd/server certificate and key.
[certificates] etcd/server serving cert is signed for DNS names [vpod1 localhost] and IPs [127.0.0.1 ::1]
[certificates] Generated etcd/peer certificate and key.
[certificates] etcd/peer serving cert is signed for DNS names [vpod1 localhost] and IPs [192.168.0.xx 127.0.0.1 ::1]
[certificates] Generated etcd/healthcheck-client certificate and key.
[certificates] Generated apiserver-etcd-client certificate and key.
[certificates] valid certificates and keys now exist in "/etc/kubernetes/pki"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/admin.conf"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/kubelet.conf"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/controller-manager.conf"
[kubeconfig] Wrote KubeConfig file to disk: "/etc/kubernetes/scheduler.conf"
[controlplane] wrote Static Pod manifest for component kube-apiserver to "/etc/kubernetes/manifests/kube-apiserver.yaml"
[controlplane] wrote Static Pod manifest for component kube-controller-manager to "/etc/kubernetes/manifests/kube-controller-manager.yaml"
[controlplane] wrote Static Pod manifest for component kube-scheduler to "/etc/kubernetes/manifests/kube-scheduler.yaml"
[etcd] Wrote Static Pod manifest for a local etcd instance to "/etc/kubernetes/manifests/etcd.yaml"
[init] waiting for the kubelet to boot up the control plane as Static Pods from directory "/etc/kubernetes/manifests"
[init] this might take a minute or longer if the control plane images have to be pulled

                Unfortunately, an error has occurred:
                        timed out waiting for the condition

                This error is likely caused by:
                        - The kubelet is not running
                        - The kubelet is unhealthy due to a misconfiguration of the node in some way (required cgroups disabled)
                        - No internet connection is available so the kubelet cannot pull or find the following control plane images:
                                - k8s.gcr.io/kube-apiserver-arm:v1.11.3
                                - k8s.gcr.io/kube-controller-manager-arm:v1.11.3
                                - k8s.gcr.io/kube-scheduler-arm:v1.11.3
                                - k8s.gcr.io/etcd-arm:3.2.18
                                - You can check or miligate this in beforehand with "kubeadm config images pull" to make sure the images
                                  are downloaded locally and cached.

                If you are on a systemd-powered system, you can try to troubleshoot the error with the following commands:
                        - 'systemctl status kubelet'
                        - 'journalctl -xeu kubelet'

                Additionally, a control plane component may have crashed or exited when started by the container runtime.
                To troubleshoot, list all containers using your preferred container runtimes CLI, e.g. docker.
                Here is one example how you may list all Kubernetes containers running in docker:
                        - 'docker ps -a | grep kube | grep -v pause'
                        Once you have found the failing container, you can inspect its logs with:
                        - 'docker logs CONTAINERID'
couldn't initialize a Kubernetes cluster
```
失敗しました。

#### apiserverの再起動問題
言われた通り、`systemctl status kubelet`を確認してみるのですが、Activeで問題ありません。
`docker ps -a | grep kube | grep -v pause`を確認してみると、kube-apiserverが数分で落ちて再起動がかかる状況が繰り返されているようです。
とりあえずこの状況を何とかしなければならなそうなのですが、最初原因がわかりませんでした。そこで`kubeadm init -v 10`で様子を探ってみました。するとどうやらヘルスチェックをしようとしてタイムアウトしているようでした。
検索してみた結果、どうやらヒットしそうなバグ？を[ここで](https://github.com/kubernetes/kubeadm/issues/413)見つけました。
結果、上のログにある`[controlplane] wrote Static Pod manifest for component kube-scheduler to "/etc/kubernetes/manifests/kube-scheduler.yaml"`が出力された直後からapiserverが立ち上がるまでの間に別ターミナルから以下のコマンドで回避しました。

```
$ sudo sed -i 's/initialDelaySeconds: 15/initialDelaySeconds: 300/g' /etc/kubernetes/manifests/kube-apiserver.yaml
```

これでapiserverは落ちなくなったので、セットアップ完了・・・かと思いきや、また同じ止まり方をしました。

#### kubeadm initのヘルスチェックが早すぎる問題
再度`kubeadm init -v 10`で様子を見てみたのですが、ヘルスチェックでタイムアウトしている状況は変わりません。
ヘルスチェックに使用しているコマンド`curl -k -v -XGET  -H "Accept: application/json, */*" -H "User-Agent: kubeadm/v0.0.0 (linux/arm) kubernetes/$Format" 'https://192.168.0.xx:6443/healthz?timeout=32s'`を叩いてみたところ、応答が安定するまで10分近くかかりました。どうやら`kubeadm init`がそこまで待ってくれないようです。正直どうしようもありません。

最終手段として`kubeadm`コマンドを改造することにしました。
とりあえずソースツリーを取ってきます。

```
$ git clone https://github.com/kubernetes/kubernetes.git
$ cd kubernetes
$ git checkout remotes/origin/release-1.11
```

kubeadmコマンドのソースがcmd/kubeadm配下にあります。
今回はその中のapp/cmd/init.goの400行目に無理やり10分間sleepさせるという荒業にでました。もっと良い方法があるはずなのでよい子はマネしないでね。

```
--- init.go.bak 2018-09-22 12:10:02.029874009 +0900
+++ init.go     2018-09-22 09:47:08.546241353 +0900
@@ -396,6 +396,10 @@ func (i *Init) Run(out io.Writer) error
        fmt.Printf("[init] waiting for the kubelet to boot up the control plane as Static Pods from directory %q \n", kubeadmconstants.GetStaticPodDirectory())
        fmt.Println("[init] this might take a minute or longer if the control plane images have to be pulled")

+       //
+       fmt.Println("[init] sleep...")
+       time.Sleep(600*time.Second)
+
        if err := waitForKubeletAndFunc(waiter, waiter.WaitForAPI); err != nil {
                ctx := map[string]string{
                        "Error":                  fmt.Sprintf("%v", err),
```

ファイルを修正したらkubeadmコマンドを作り直します。

```
$ hack/run-in-gopath.sh bash --norc --noprofile
$ cd cmd/kubeadm/
$ go build kubeadm.go
```

これで無駄に10分sleepする`kubeadm`が出来上がりました。
これを使用して再度`sudo ./kubeadm init --pod-network-cidr=10.244.0.0/16 -v 10`を行います。途中kube-apiserver.yamlの編集も忘れずに。
ここまでやってようやく初期化が完了しました。

## kubectlを使えるようにする
`kubeadm init`がうまくいけば、追加でやる事が表示されるはずですのでそのまま実行します。

```
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
```

その後、

```
$ kubectl get node
NAME      STATUS     ROLES     AGE       VERSION
hoge      NotReady   master    35m       v1.11.3
```

のようになれば問題ないようです。

## flanel add-on の追加
[公式のここ](https://kubernetes.io/docs/setup/independent/create-cluster-kubeadm/)にも記載がありますが、今回はarm版でのセットアップですので、以下のようにします。

```
 $ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/c5d10c8/Documentation/kube-flannel.yml
```

待つこと数分、ようやっと動き出します。

```
$ kubectl get po -n kube-system
NAME                            READY     STATUS    RESTARTS   AGE
coredns-78fcdf6894-n8hq5        1/1       Running   0          46m
coredns-78fcdf6894-srq7x        1/1       Running   0          46m
etcd-cpod5                      1/1       Running   1          50m
kube-apiserver-hoge             1/1       Running   0          50m
kube-controller-manager-hoge    1/1       Running   2          50m
kube-flannel-ds-arm-pdzmd       1/1       Running   0          11m
kube-proxy-lmv6v                1/1       Running   0          46m
kube-scheduler-hoge             1/1       Running   1          50m
$ kubectl get node
NAME      STATUS    ROLES     AGE       VERSION
hoge      Ready     master    52m       v1.11.3
```

# 今後
とりあえずマスターノードをRaspberry Pi上で立ち上げるところまでは完了しました。
ノードの追加やアプリケーションのデプロイ等、きちんと動くのか検証していきます。