ラズパイでEKS Distroを動かす

こちらは[AWS Containers Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-containers)の20日目の記事です。
本記事は個人の意見であり、所属する組織の見解とは関係ありません。

1年半程前、私も[ラズパイでKubernetesクラスタを作成](https://qiita.com/sotoiwa/items/e350579d4c81c4a65260)しました。

![pi-k8s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/86449/8efca21c-2346-d98b-2611-0c15c1ad1048.jpeg)


使用したのは3台のRaspberry Pi 3 Model B+で、その頃のKubernetesのバージョンは1.13でした。このラズパイクラスタはその後ほとんど活用されていないので、今回はこのラズパイクラスタで[EKS Distro](https://distro.eks.amazonaws.com/)を動かしてみたいと思います。

EKS Distroとは何か、についてはFAQがわかりやすいです。日本語です。

- [Amazon EKS Distro のよくある質問](https://aws.amazon.com/jp/eks/eks-distro/faqs/)

## 方針

公式ドキュメントには[kopsを使ったデプロイ手順](https://distro.eks.amazonaws.com/users/)があります。[パートナー](https://distro.eks.amazonaws.com/community/partners/)がそれぞれのサービスのKubernetesクラスターでEKS Distroを使う方法を用意しています。例えば、Canonicalの[Snapパッケージ](https://snapcraft.io/eks)を使うと簡単にラズパイでもEKS Distroが動くようです。これは[MicroK8s](https://microk8s.io/)のKubernetes部分をEKS Distroに置き換えているようです。

Snapではなく[kubeadm](https://kubernetes.io/ja/docs/reference/setup-tools/kubeadm/)を使った手順を探していたところ、同僚に以下のブログ記事を教えてもらいました。

- [Running Kubernetes cluster with Amazon EKS Distro across AWS Snowball Edge](https://aws.amazon.com/jp/blogs/storage/running-kubernetes-cluster-with-amazon-eks-distro-across-aws-snowball-edge/)

今回はこのブログ記事を参考にして、ラズパイに「kubeadm」で「EKS Distro」をデプロイし、「ラズパイEKSクラスタ」を作成したいと思います。ブログ記事ではコントロールプレーンを冗長化していますが、私はシングルコントロールプレーンのクラスターを作成します。

## ラズパイクラスタの準備

実は最初、以前作成したラズパイクラスタを流用し、一度Dockerやらkubeadmやらを削除してきれいにした上で、EKS Distroをデプロイしようとしました。しかし私のラズパイは32bitのarmv7互換のOSで動いており、一方、EKS Distroのバイナリや[イメージ](https://gallery.ecr.aws/?searchTerm=eks-distro)はarm64版しか提供されていないため失敗しました。

頑張れば自分でarmv7版のバイナリとイメージをビルドすることも可能かもしれませんが、OSを64bit版に入れ替える方が簡単そうなので、OSを入れ替えました。しかし、64bit OSはメモリー消費量が多いのか、あるいは以前よりKubernetesのバージョンが上がってメモリー消費量が多くなったのか、メモリーが1GBしかないRaspberry Pi 3 Model B+では`kubeadm init`を実行するとハングしてしまうという自体に陥りました。

結局、Raspberry Pi 4 Model B (4GB RAM)を追加で購入し、Masterノードとして利用することにしました。

したがって、新しいラズパイ4Bと既存のラズパイ3B+に64bit版のOSを入れるところからはじめます。RaspbianはRaspberry Pi OSに名前が変わったのですね。64bit版はまだbetaテスト版で、以下にイメージがあります。

- [Raspberry Pi OS (64 bit) beta test version](https://www.raspberrypi.org/forums/viewtopic.php?f=117&t=275370)

Ubuntuも64bit版があるようです。

- https://ubuntu.com/download/raspberry-pi

今回はRaspberry Pi OSの64bit版を使うことにします。イメージをSDカードに焼くツールは、今回は[Raspberry Pi Imager](https://www.raspberrypi.org/software/)を使いました。とても簡単です。

ラズパイクラスタの基本的なセットアップについては、以下の以前の記事を参照下さい。

- [ラズパイでKubernetesクラスタを構築する](https://qiita.com/sotoiwa/items/e350579d4c81c4a65260)

こちらの記事にしたがって、SSHログインができるところまで進めて下さい。

なお、以前の記事ではクラスターの上部に配置した小型のWiFiルーターを使って上流のWiFiネットワークに接続する構成で、ラズパイのIPアドレスは`192.11.13.101`〜`192.11.13.103`でした。しかしこの構成だと小型のWiFiルーターの速度が遅いので、今回は家庭内LANに有線で直接繋がるようにしています。ラズパイのIPアドレスは`192.11.11.101`〜`192.11.11.104`です。

|ホスト名    |IPアドレス       |役割  |ハードウェア               |メモリー|
|----------|--------------|------|-----------------------|-----|
|k8s-master|192.168.11.101|Master|Raspberry Pi 4 Model B |4GB  |
|k8s-node1 |192.168.11.102|Worker|Raspberry Pi 3 Model B+|1GB  |
|k8s-node2 |192.168.11.103|Worker|Raspberry Pi 3 Model B+|1GB  |
|k8s-node3 |192.168.11.104|Worker|Raspberry Pi 3 Model B+|1GB  |

## 手順

では、ここからクラスターの作成に取りかかります。

### cgroupの有効化

起動する前にSDカードの中の`cmdline.txt`を編集しなかった場合は、`/boot/cmdline.txt`を編集して行の末尾に`cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1`を追加して再起動します。

```:/boot/cmdline.txt
console=serial0,115200 console=tty1 root=PARTUUID=af570baa-02 rootfstype=ext4 elevator=deadline fsck.repair=yes rootwait quiet splash plymouth.ignore-serial-consoles cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1
```

`/proc/cgroups`を確認してenabledの列が1になっていればOKです。

```shell-session
pi@k8s-master:~ $ cat /proc/cgroups
#subsys_name	hierarchy	num_cgroups	enabled
cpuset	6	1	1
cpu	7	1	1
cpuacct	7	1	1
blkio	2	1	1
memory	9	92	1
devices	4	77	1
freezer	8	1	1
net_cls	3	1	1
perf_event	5	1	1
net_prio	3	1	1
pids	10	85	1
```

### スワップの無効化

スワップを無効化しておきます。

```shell
sudo dphys-swapfile swapoff
sudo systemctl stop dphys-swapfile
sudo systemctl disable dphys-swapfile
```

`free`コマンドでスワップが使われていないことを確認します。

```shell
pi@k8s-master:~ $ free
              total        used        free      shared  buff/cache   available
Mem:         934532      248860      265572        7636      420100      615608
Swap:             0           0           0
```

### Dockerのインストール

[convenience script](https://docs.docker.com/engine/install/debian/#install-using-the-convenience-script)を使ってインストールします。

```shell
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
```

```shell-session:コマンド出力
pi@k8s-master:~ $ sudo sh get-docker.sh
# Executing docker install script, commit: 3d8fe77c2c46c5b7571f94b42793905e5b3e42e4
+ sh -c apt-get update -qq >/dev/null
+ sh -c DEBIAN_FRONTEND=noninteractive apt-get install -y -qq apt-transport-https ca-certificates curl >/dev/null
+ sh -c curl -fsSL "https://download.docker.com/linux/debian/gpg" | apt-key add -qq - >/dev/null
Warning: apt-key output should not be parsed (stdout is not a terminal)
+ sh -c echo "deb [arch=arm64] https://download.docker.com/linux/debian buster stable" > /etc/apt/sources.list.d/docker.list
+ sh -c apt-get update -qq >/dev/null
+ [ -n  ]
+ sh -c apt-get install -y -qq --no-install-recommends docker-ce >/dev/null
+ sh -c docker version
Client: Docker Engine - Community
 Version:           20.10.1
 API version:       1.41
 Go version:        go1.13.15
 Git commit:        831ebea
 Built:             Tue Dec 15 04:35:39 2020
 OS/Arch:           linux/arm64
 Context:           default
 Experimental:      true

Server: Docker Engine - Community
 Engine:
  Version:          20.10.1
  API version:      1.41 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       f001486
  Built:            Tue Dec 15 04:33:40 2020
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.4.3
  GitCommit:        269548fa27e0089a8b8278fc4fc781d7f65a939b
 runc:
  Version:          1.0.0-rc92
  GitCommit:        ff819c7e9184c13b7c2607fe6c30ae19403a7aff
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
If you would like to use Docker as a non-root user, you should now consider
adding your user to the "docker" group with something like:

  sudo usermod -aG docker your-user

Remember that you will have to log out and back in for this to take effect!

WARNING: Adding a user to the "docker" group will grant the ability to run
         containers which can be used to obtain root privileges on the
         docker host.
         Refer to https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface
         for more information.
```

`20.10`はまだKubernetesではサポートされていないバージョンであるため、`19.03`にダウングレードします。

まず利用可能なバージョンを確認します。

```shell-session
pi@k8s-master:~ $ apt-cache madison docker-ce
 docker-ce | 5:20.10.1~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:20.10.0~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.14~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.13~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.12~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.11~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.10~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.9~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.8~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.7~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.6~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.5~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.4~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.3~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.2~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.1~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:19.03.0~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.9~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.8~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.7~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.6~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.5~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.4~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.3~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.2~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.1~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 5:18.09.0~3-0~debian-buster | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 18.06.3~ce~3-0~debian | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 18.06.2~ce~3-0~debian | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 18.06.1~ce~3-0~debian | https://download.docker.com/linux/debian buster/stable arm64 Packages
 docker-ce | 18.06.0~ce~3-0~debian | https://download.docker.com/linux/debian buster/stable arm64 Packages
```

ダウングレードします。

```shell
sudo apt-get install -y --allow-downgrades --no-install-recommends \
    docker-ce=5:19.03.14~3-0~debian-buster \
    docker-ce-cli=5:19.03.14~3-0~debian-buster
```

バージョンは固定しておきます。

```shell
sudo apt-mark hold docker-ce docker-ce-cli
```

`pi`ユーザーでも`docker`コマンドを実行できるようにしておきます。

```shell
sudo usermod -aG docker pi
```

ここからは`root`ユーザーで作業します。

```shell
sudo -i
```

Dockerのcgroupドライバーを確認します。

```shell-session
root@k8s-master:~# docker info | grep -i cgroup
 Cgroup Driver: cgroupfs
WARNING: No swap limit support
WARNING: No blkio weight support
WARNING: No blkio weight_device support
```

[Kubernetesのドキュメント](https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/#docker)にしたがって、Dockerのcgroupドライバーを`systemd`に変更します。log-driverとstorage-driverは既にこの指定の通りになっていますが、明示的に指定しておきます。

```shell
cat <<EOF > /etc/docker/daemon.json
{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "storage-driver": "overlay2"
}
EOF
```

Dockerを再起動します。

```shell
systemctl daemon-reload
systemctl restart docker
```

Dockerのcgroupドライバーが`systemd`になったことを確認します。

```shell-session
root@k8s-master:~# docker info | grep -i cgroup
 Cgroup Driver: systemd
WARNING: No swap limit support
```

### kubeadm（EKS Distro版）のインストール

`root`ユーザーで作業します。

kubeadmはapt-getではなくバイナリを直接ダウンロードします。まず以下のリンクに記載のkubeadmの前提条件を確認します。

- [kubeadmのインストール](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/)

iptablesがブリッジを通過するトラフィックを処理できるようにするため、`net.bridge.bridge-nf-call-iptables`を`1`に設定します。この時点で既に`1`に設定されていますが、念のため明示的に設定します。

```shell
cat <<EOF > /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
EOF
sysctl --system
```

iptablesがnftablesをバックエンドに使用しないようにします。レガシーバージョンをインストールします。

```shell
apt-get install -y iptables arptables ebtables
```

レガシーバージョンに切り替えます。

```shell
update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
update-alternatives --set arptables /usr/sbin/arptables-legacy
update-alternatives --set ebtables /usr/sbin/ebtables-legacy
```

変更を反映するため、念のためここでラズパイを再起動します。

```shell
reboot
```

EKS DistroのリポジトリからCNIプラグインをダウンロードしてインストールします。

```shell
mkdir -p /opt/cni/bin
wget https://distro.eks.amazonaws.com/kubernetes-1-18/releases/1/artifacts/plugins/v0.8.7/cni-plugins-linux-arm64-v0.8.7.tar.gz
tar zxf cni-plugins-linux-arm64-v0.8.7.tar.gz -C /opt/cni/bin/
```

EKS Distroのリポジトリからkubeadm、kubelet、kubectlをダウンロードしてインストールします。

```shell
wget https://distro.eks.amazonaws.com/kubernetes-1-18/releases/1/artifacts/kubernetes/v1.18.9/bin/linux/arm64/kubeadm
wget https://distro.eks.amazonaws.com/kubernetes-1-18/releases/1/artifacts/kubernetes/v1.18.9/bin/linux/arm64/kubelet
wget https://distro.eks.amazonaws.com/kubernetes-1-18/releases/1/artifacts/kubernetes/v1.18.9/bin/linux/arm64/kubectl
mv kubeadm kubelet kubectl /usr/bin/
chmod +x /usr/bin/kubeadm /usr/bin/kubelet /usr/bin/kubectl
```

kubeletが必要とするパッケージをインストールします。

```shell
apt-get install -y conntrack ebtables socat
```

kubeletに引数を渡して、cgroupドライバーを`systemd`に変更します。Dockerが使用するcgroupドライバーと一致する必要があります。[参考にしているブログ](https://aws.amazon.com/jp/blogs/storage/running-kubernetes-cluster-with-amazon-eks-distro-across-aws-snowball-edge/)はCentOSなので、Debian(Raspberry Pi OS)とはファイルの配置場所が違うところに注意が必要です。

```shell
cat <<EOF > /etc/default/kubelet
KUBELET_EXTRA_ARGS='--cgroup-driver=systemd'
EOF
```

kubeadmとkubeletが使用するディレクトリを作成します。

```shell
mkdir -p /etc/kubernetes/manifests
mkdir -p /etc/systemd/system/kubelet.service.d
```

ブログ記事と以下のリンクも参考にしながら、kubeadmの設定ファイルとkubeletのユニット定義ファイルを作成します。

- [kubelet用のsystemdファイル](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/kubelet-integration/#the-kubelet-drop-in-file-for-systemd)

```shell
cat <<EOF > /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet \$KUBELET_KUBECONFIG_ARGS \$KUBELET_CONFIG_ARGS \$KUBELET_KUBEADM_ARGS \$KUBELET_EXTRA_ARGS
EOF
```

```shell
cat <<EOF > /lib/systemd/system/kubelet.service
[Unit]
Description=kubelet: The Kubernetes Node Agent
Documentation=https://kubernetes.io/docs/
Wants=network-online.target
After=network-online.target

[Service]
ExecStart=/usr/bin/kubelet
Restart=always
StartLimitInterval=0
RestartSec=10

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

システム起動時にkubeletが起動するようにします。

```shell
systemctl enable kubelet
```

### Kubernetesコンポーネント（EKS Distro版）のダウンロード

コントロールプレーンに必要なDockerイメージをAmazon ECR Publicからpullします。

```shell
docker pull public.ecr.aws/eks-distro/etcd-io/etcd:v3.4.14-eks-1-18-1
docker pull public.ecr.aws/eks-distro/kubernetes/pause:v1.18.9-eks-1-18-1
docker pull public.ecr.aws/eks-distro/kubernetes/kube-scheduler:v1.18.9-eks-1-18-1
docker pull public.ecr.aws/eks-distro/kubernetes/kube-proxy:v1.18.9-eks-1-18-1
docker pull public.ecr.aws/eks-distro/kubernetes/kube-apiserver:v1.18.9-eks-1-18-1
docker pull public.ecr.aws/eks-distro/kubernetes/kube-controller-manager:v1.18.9-eks-1-18-1
docker pull public.ecr.aws/eks-distro/coredns/coredns:v1.7.0-eks-1-18-1
```

kubeadmにハードコードされた値に合わせる必要があるため、イメージに追加のタグを付与します。

```shell
docker tag public.ecr.aws/eks-distro/kubernetes/pause:v1.18.9-eks-1-18-1 public.ecr.aws/eks-distro/kubernetes/pause:3.2
docker tag public.ecr.aws/eks-distro/coredns/coredns:v1.7.0-eks-1-18-1 public.ecr.aws/eks-distro/kubernetes/coredns:1.6.7
```

イメージを確認します。

```shell
root@k8s-master:~# docker images
REPOSITORY                                                     TAG                  IMAGE ID       CREATED       SIZE
public.ecr.aws/eks-distro/kubernetes/pause                     3.2                  430398c8f74c   2 weeks ago   630kB
public.ecr.aws/eks-distro/kubernetes/pause                     v1.18.9-eks-1-18-1   430398c8f74c   2 weeks ago   630kB
public.ecr.aws/eks-distro/kubernetes/kube-proxy                v1.18.9-eks-1-18-1   0ff76167f898   2 weeks ago   579MB
public.ecr.aws/eks-distro/kubernetes/kube-scheduler            v1.18.9-eks-1-18-1   2f471234bdca   2 weeks ago   479MB
public.ecr.aws/eks-distro/kubernetes/kube-controller-manager   v1.18.9-eks-1-18-1   2c01694a0619   2 weeks ago   543MB
public.ecr.aws/eks-distro/kubernetes/kube-apiserver            v1.18.9-eks-1-18-1   94f388827354   2 weeks ago   553MB
public.ecr.aws/eks-distro/etcd-io/etcd                         v3.4.14-eks-1-18-1   61b76a81855c   2 weeks ago   473MB
public.ecr.aws/eks-distro/coredns/coredns                      v1.7.0-eks-1-18-1    c04916f93277   2 weeks ago   44.4MB
public.ecr.aws/eks-distro/kubernetes/coredns                   1.6.7                c04916f93277   2 weeks ago   44.4MB
```

etcdのイメージは[このIssue](https://github.com/etcd-io/etcd/issues/12543)と同じエラーで起動しないため、`ETCD_UNSUPPORTED_ARCH=arm64`という環境変数を渡す必要があります。kubeadmの設定オプションで環境変数を渡すことができなそうだったため、環境変数を追加したイメージをビルドします。

Dockerfileを作成します。

```shell
cat <<EOF > Dockerfile
FROM public.ecr.aws/eks-distro/etcd-io/etcd:v3.4.14-eks-1-18-1
ENV ETCD_UNSUPPORTED_ARCH=arm64
EOF
```

イメージをビルドします。

```shell
docker build -t public.ecr.aws/eks-distro/etcd-io/etcd:v3.4.14-eks-1-18-1 .
```

イメージを確認します。

```shell-session
oot@k8s-master:~# docker images
REPOSITORY                                                     TAG                  IMAGE ID       CREATED         SIZE
public.ecr.aws/eks-distro/etcd-io/etcd                         v3.4.14-eks-1-18-1   bcc9e6b4e4d4   4 seconds ago   473MB
public.ecr.aws/eks-distro/kubernetes/pause                     3.2                  430398c8f74c   2 weeks ago     630kB
public.ecr.aws/eks-distro/kubernetes/pause                     v1.18.9-eks-1-18-1   430398c8f74c   2 weeks ago     630kB
public.ecr.aws/eks-distro/kubernetes/kube-proxy                v1.18.9-eks-1-18-1   0ff76167f898   2 weeks ago     579MB
public.ecr.aws/eks-distro/kubernetes/kube-scheduler            v1.18.9-eks-1-18-1   2f471234bdca   2 weeks ago     479MB
public.ecr.aws/eks-distro/kubernetes/kube-controller-manager   v1.18.9-eks-1-18-1   2c01694a0619   2 weeks ago     543MB
public.ecr.aws/eks-distro/kubernetes/kube-apiserver            v1.18.9-eks-1-18-1   94f388827354   2 weeks ago     553MB
public.ecr.aws/eks-distro/etcd-io/etcd                         <none>               61b76a81855c   2 weeks ago     473MB
public.ecr.aws/eks-distro/kubernetes/coredns                   1.6.7                c04916f93277   2 weeks ago     44.4MB
public.ecr.aws/eks-distro/coredns/coredns                      v1.7.0-eks-1-18-1    c04916f93277   2 weeks ago     44.4MB
```

ここまでの操作は全てのラズパイに対して行います。

### Masterノードの初期化

以下のリンクにしたがってクラスターを作成します。

- [kubeadmを使用したシングルコントロールプレーンクラスターの作成](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/)

Masterノード上でkubeadmの構成ファイルを作成します。Kubernetesの各コンポーネントの取得元のイメージリポジトリとして、EKS Distroのリポジトリを指定しています。PodネットワークアドオンはFlannelを使う想定でPodネットワークのCIDRを指定しています。Calicoだと我が家のLANのCIDRと被ってしまうためFlannelを選択しています。

```shell
cat <<EOF > kubeadm-config.yaml
apiVersion: kubeadm.k8s.io/v1beta2
kind: ClusterConfiguration
networking:
  podSubnet: "10.244.0.0/16"
etcd:
  local:
    imageRepository: public.ecr.aws/eks-distro/etcd-io
    imageTag: v3.4.14-eks-1-18-1
    extraArgs:
      listen-peer-urls: "https://0.0.0.0:2380"
      listen-client-urls: "https://0.0.0.0:2379"
imageRepository: public.ecr.aws/eks-distro/kubernetes
kubernetesVersion: v1.18.9-eks-1-18-1
EOF
```

このファイルを指定して`kubeadm init`を実行します。

```shell
kubeadm init --config kubeadm-config.yaml
```

```shell-session:コマンド出力
root@k8s-master:~# kubeadm init --config kubeadm-config.yaml
W1219 02:15:50.059301    1349 configset.go:202] WARNING: kubeadm cannot validate component configs for API groups [kubelet.config.k8s.io kubeproxy.config.k8s.io]
[init] Using Kubernetes version: v1.18.9-eks-1-18-1
[preflight] Running pre-flight checks
[preflight] Pulling images required for setting up a Kubernetes cluster
[preflight] This might take a minute or two, depending on the speed of your internet connection
[preflight] You can also perform this action in beforehand using 'kubeadm config images pull'
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Starting the kubelet
[certs] Using certificateDir folder "/etc/kubernetes/pki"
[certs] Generating "ca" certificate and key
[certs] Generating "apiserver" certificate and key
[certs] apiserver serving cert is signed for DNS names [k8s-master kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local] and IPs [10.96.0.1 192.168.11.101]
[certs] Generating "apiserver-kubelet-client" certificate and key
[certs] Generating "front-proxy-ca" certificate and key
[certs] Generating "front-proxy-client" certificate and key
[certs] Generating "etcd/ca" certificate and key
[certs] Generating "etcd/server" certificate and key
[certs] etcd/server serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.11.101 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [k8s-master localhost] and IPs [192.168.11.101 127.0.0.1 ::1]
[certs] Generating "etcd/healthcheck-client" certificate and key
[certs] Generating "apiserver-etcd-client" certificate and key
[certs] Generating "sa" key and public key
[kubeconfig] Using kubeconfig folder "/etc/kubernetes"
[kubeconfig] Writing "admin.conf" kubeconfig file
[kubeconfig] Writing "kubelet.conf" kubeconfig file
[kubeconfig] Writing "controller-manager.conf" kubeconfig file
[kubeconfig] Writing "scheduler.conf" kubeconfig file
[control-plane] Using manifest folder "/etc/kubernetes/manifests"
[control-plane] Creating static Pod manifest for "kube-apiserver"
[control-plane] Creating static Pod manifest for "kube-controller-manager"
W1219 02:16:09.651280    1349 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[control-plane] Creating static Pod manifest for "kube-scheduler"
W1219 02:16:09.655043    1349 manifests.go:225] the default kube-apiserver authorization-mode is "Node,RBAC"; using "Node,RBAC"
[etcd] Creating static Pod manifest for local etcd in "/etc/kubernetes/manifests"
[wait-control-plane] Waiting for the kubelet to boot up the control plane as static Pods from directory "/etc/kubernetes/manifests". This can take up to 4m0s
[apiclient] All control plane components are healthy after 39.519892 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.18" in namespace kube-system with the configuration for the kubelets in the cluster
[kubelet-check] Initial timeout of 40s passed.
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node k8s-master as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node k8s-master as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: v4aezo.0qqsig2csphtm5dj
[bootstrap-token] Configuring bootstrap tokens, cluster-info ConfigMap, RBAC Roles
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to get nodes
[bootstrap-token] configured RBAC rules to allow Node Bootstrap tokens to post CSRs in order for nodes to get long term certificate credentials
[bootstrap-token] configured RBAC rules to allow the csrapprover controller automatically approve CSRs from a Node Bootstrap Token
[bootstrap-token] configured RBAC rules to allow certificate rotation for all node client certificates in the cluster
[bootstrap-token] Creating the "cluster-info" ConfigMap in the "kube-public" namespace
[kubelet-finalize] Updating "/etc/kubernetes/kubelet.conf" to point to a rotatable kubelet client certificate and key
[addons] Applied essential addon: CoreDNS
[addons] Applied essential addon: kube-proxy

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user:

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

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at:
  https://kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root:

kubeadm join 192.168.11.101:6443 --token v4aezo.0qqsig2csphtm5dj \
    --discovery-token-ca-cert-hash sha256:04da2245c17641aec3cec1edc74e0ae88c7411573cc287a920c42ebba9a5c4e8
```

出力の最後にある`kubeadm join`コマンドを後で使います。

一般ユーザーに戻って、kubeconfigを設定します。

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

`kubectl`が実行できることを確認します。この時点ではまだノードは`NotReady`ですが、`VERSION`はEKSっぽいバージョンになっていますね！

```shell
pi@k8s-master:~ $ kubectl get node
NAME         STATUS     ROLES    AGE    VERSION
k8s-master   NotReady   master   2m3s   v1.18.9-eks-1-18-1
```

### Podネットワークアドオンのインストール

PodネットワークアドオンとしてFlannelをインストールします。

```shell
kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/2140ac876ef134e0ed5af15c65e414cf26827915/Documentation/kube-flannel.yml
```

しばらくするとノードが`Ready`になります。

```shell
pi@k8s-master:~ $ kubectl get node
NAME         STATUS   ROLES    AGE     VERSION
k8s-master   Ready    master   4m27s   v1.18.9-eks-1-18-1
```

コマンドの補完と`k`のエイリアスを有効にしておきます（オプション）。

```shell
sudo apt-get install -y bash-completion
cat <<EOT >> ~/.bashrc

alias k="kubectl"
source <(kubectl completion bash)
complete -o default -F __start_kubectl k
EOT
source ~/.bashrc
```

### Workerノードの追加

全てのWorkerノード上で、先ほどMasterノードで`kubeadm init`を実行した際に出力されていた`kubeadm join`コマンドを`root`ユーザーで実行します。

```shell-session
root@k8s-node3:~# kubeadm join 192.168.11.101:6443 --token v4aezo.0qqsig2csphtm5dj \
>     --discovery-token-ca-cert-hash sha256:04da2245c17641aec3cec1edc74e0ae88c7411573cc287a920c42ebba9a5c4e8
W1219 02:22:56.373017    2477 join.go:346] [preflight] WARNING: JoinControlPane.controlPlane settings will be ignored when control-plane flag is not set.
[preflight] Running pre-flight checks
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -oyaml'
[kubelet-start] Downloading configuration for the kubelet from the "kubelet-config-1.18" ConfigMap in the kube-system namespace
[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

```

Masterノード上で、ノードが追加され`Ready`となっていることを確認します。

```shell
pi@k8s-master:~ $ kubectl get node -o wide
NAME         STATUS   ROLES    AGE     VERSION              INTERNAL-IP      EXTERNAL-IP   OS-IMAGE                       KERNEL-VERSION   CONTAINER-RUNTIME
k8s-master   Ready    master   7m33s   v1.18.9-eks-1-18-1   192.168.11.101   <none>        Debian GNU/Linux 10 (buster)   5.4.79-v8+       docker://19.3.14
k8s-node1    Ready    <none>   66s     v1.18.9-eks-1-18-1   192.168.11.102   <none>        Debian GNU/Linux 10 (buster)   5.4.79-v8+       docker://19.3.14
k8s-node2    Ready    <none>   60s     v1.18.9-eks-1-18-1   192.168.11.103   <none>        Debian GNU/Linux 10 (buster)   5.4.79-v8+       docker://19.3.14
k8s-node3    Ready    <none>   55s     v1.18.9-eks-1-18-1   192.168.11.104   <none>        Debian GNU/Linux 10 (buster)   5.4.79-v8+       docker://19.3.14
```

全てのPodが`Running`であることを確認します。

```shell
pi@k8s-master:~ $ kubectl get pod -A -o wide
NAMESPACE     NAME                                 READY   STATUS    RESTARTS   AGE     IP               NODE         NOMINATED NODE   READINESS GATES
kube-system   coredns-8f7b4cf65-f4dhv              1/1     Running   0          7m40s   10.244.0.2       k8s-master   <none>           <none>
kube-system   coredns-8f7b4cf65-lgg6d              1/1     Running   0          7m40s   10.244.0.3       k8s-master   <none>           <none>
kube-system   etcd-k8s-master                      1/1     Running   0          7m46s   192.168.11.101   k8s-master   <none>           <none>
kube-system   kube-apiserver-k8s-master            1/1     Running   0          7m46s   192.168.11.101   k8s-master   <none>           <none>
kube-system   kube-controller-manager-k8s-master   1/1     Running   0          7m46s   192.168.11.101   k8s-master   <none>           <none>
kube-system   kube-flannel-ds-arm64-cfdxg          1/1     Running   0          87s     192.168.11.103   k8s-node2    <none>           <none>
kube-system   kube-flannel-ds-arm64-d7hgj          1/1     Running   0          93s     192.168.11.102   k8s-node1    <none>           <none>
kube-system   kube-flannel-ds-arm64-frcl2          1/1     Running   0          82s     192.168.11.104   k8s-node3    <none>           <none>
kube-system   kube-flannel-ds-arm64-md7gh          1/1     Running   0          3m44s   192.168.11.101   k8s-master   <none>           <none>
kube-system   kube-proxy-fcbm2                     1/1     Running   0          93s     192.168.11.102   k8s-node1    <none>           <none>
kube-system   kube-proxy-lgvw2                     1/1     Running   0          87s     192.168.11.103   k8s-node2    <none>           <none>
kube-system   kube-proxy-sbh7v                     1/1     Running   0          7m40s   192.168.11.101   k8s-master   <none>           <none>
kube-system   kube-proxy-vpgkg                     1/1     Running   0          82s     192.168.11.104   k8s-node3    <none>           <none>
kube-system   kube-scheduler-k8s-master            1/1     Running   0          7m46s   192.168.11.101   k8s-master   <none>           <none>
```

### 稼働確認

NginxをデプロイしてNodePortで公開し、クラスターが機能していることを確認しましょう。

NginxのDeploymentを作成します。

```shell-session
pi@k8s-master:~ $ kubectl create deployment nginx --image=nginx
deployment.apps/nginx created
```

DeploymentをNodePortタイプのServiceとして公開します。

```shell-session
pi@k8s-master:~ $ kubectl expose deployment nginx --type NodePort --port 80
service/nginx exposed
```

ServiceのNodePortを確認します。

```shell-session
pi@k8s-master:~ $ kubectl get service
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        10m
nginx        NodePort    10.105.150.24   <none>        80:31972/TCP   6s
```

ラズパイではなく手元のマシンから、いずれかのノードのIPアドレス（`192.168.11.101`〜`192.168.11.104`）のNodePort（上記出力の`31972`ポート）にアクセスして確認しましょう。

```shell
$ curl 192.168.11.103:31972
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

無事にNginxにアクセスできました。以上でラズパイEKSクラスタは完成です！

### 補足

クラスターの作成に失敗したり上手く動かない場合に、やり直すときは以下の手順で行います。

Masterノードでノードを削除します。

```shell
kubectl delete node k8s-node1
kubectl delete node k8s-node2
kubectl delete node k8s-node3
```

kubeconfigを削除します。

```shell
rm -rf $HOME/.kube
```

全てのノード上で、`root`ユーザーで`kubeadm reset`コマンドを実行します。

```shell
kubeadm reset -f
```

全てのノードを再起動します。再起動しないと、iptablesのルールや、CNIプラグインが作成したネットワークインターフェースが残っていて、クラスターを再作成した際にハマります。再起動後、以下のコマンドで不要なiptablesルールや、`flannel.1`や`cni0`といったネットワークインターフェースが残っていないことを確認して下さい。

```shell
ip a
iptables -L
```

CNIプラグインをCalicoからFlannelに変更した場合など、以下のディレクトリ配下に設定ファイルが残っていてハマることがあるので、不要なファイルは削除して下さい。

```shell
rm -rf /etc/cni/net.d/*
```

## 参考リンク

- [Unboxing EKS Distro! #ContainersFromTheCouch](https://www.youtube.com/watch?v=YUS_LrlctHI)
- [[Japanese] re:Invent Containers Recap! - Week 3](https://www.youtube.com/watch?v=sJGzU78Pklg)