Raspberry Pi4 3台でKubernetesクラスタ構築


**[ALHアドベントカレンダー2020](https://qiita.com/advent-calendar/2020/alhinc)** 
12/12(土)はきくりんが担当いたします！！
サーバー、スイッチ、電源、筐体の選定から、組み込み、構築、ナレッジ化とインフラエンジニア総合格闘技的な内容となりました。
![IMG_7989.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/b942bfdd-38fa-0a90-84cb-c872e6d10436.jpeg)


# 目的と背景
ずっとやろうと思っていたラズパイのクラスタ構成を使ったKubernetesの構築ですがこの度ついに着手出来たので情報まとめていきます。
とりあえずクラスタ構成の構築と、webサーバが上がることをゴールとします。
~~今回クラスタケースの収容数が4つのため、既に持っていたラズパイ3も収容したいと思います。~~
スイッチングハブと充電器も収容することを考えると3台がMAXだよね、ということに組んでから気付いたので3台構成とします。
一部写真で同居していますがまあ気にしないでください。

#構築・ハードウェア編
### 準備したもの
・新規購入
![IMG_7944.JPG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/d79310f6-c1c7-1f6f-71fa-f6c1dae8b207.jpeg)
※主張の激しいマウスは無関係です。
・後で追加＋家に転がってたもの
![IMG_7965.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/f753cfdd-5a2e-4e31-299d-ae7cf00fcd48.jpeg)

###一覧(工具類は割愛)
| パーツ名 | 詳細 | 備考 |
|:-:|:-:|:-:|
| Raspberry Pi4  | Raspberry Pi 4 Model B/4GB x 3台  | k8sクラスタ用 |
| ~~Raspberry Pi3~~  | ~~Raspberry Pi 3 Model B?/1GB x 1台~~  | 検証用、3台構成に変更したので、結局使わなかった |
| Raspberry Pi用ヒートシンク  | Akuoly Raspberry Piアルミ製ヒートシンクセット ラズベリーパイ冷却キットクーラーheatsink 15点セット x2 | k8sクラスタ用、どうやら3向けだったみたいで1セット |
| SDカード | Samsung micro SDXC 64GB MB-MC64GA x 3枚  | k8sクラスタ用 |
| スイッチングハブ | BUFFALO Giga対応 プラスチック筐体 AC電源 5ポート LSW6-GT-5EPL/NBK ブラック スイッチングハブ ローコストモデル 簡易  | |
| ~~スイッチングハブ~~ | ~~ロジテック 1000BASE-T対応 スイッチングハブ LAN-GSW05PSBE~~  |廃盤のため中古。買ってから3ポートしか無いことに気づき、結局使わなかった |
| ケース | GeeekPi Raspberry Piクラスタケース（4層）  |  |
| 冷却ファン | EASYDIY 120mm PWM 高風量静音デュアルフレーム白色LEDケースファン  | ケース付属が虹色に光るのが嫌なので |
| ファンコントローラ | サイズ ファンコン搭載 ファン用USB変換ケーブル AS-71G2  | 風量とLEDの光量調節用 |
| USB充電器 | Anker PowerPort Speed 5  |  |
| 電源ケーブル | SANWA SUPPLY(サンワサプライ) 電源コード KB-DM2L-2  | ケースに充電器を収容するためにL字プラグのもの|
| USBケーブル | SUNGUY type-A & type-c 30cm 2本セット x 2セット(4本)  | ラズパイ給電用 |
| ~~USBケーブル~~ | ~~Anker PowerLine+ Micro USBケーブル~~  | 検証ラズパイを載せるのはやめたので、付属のケーブルタイだけ使用 |
| USBケーブル | USB 電源ケーブル 変換プラグ付き DC充電コード  | スイッチングハブ給電用 |
| LANケーブル | ミヨシMCO カテゴリ-6スリムLANケ-ブル 15cm x 4本  | ハブ～ラズパイ間接続 |
| ~~ケーブル用ベルト~~ | ~~面ファスナー配線ベルト,面ファスナー配線ベルト固定ベルト付き~~  | DAISOで調達、結局使わなかった |
| HDMI変換 | ホーリック HDMIマイクロ変換アダプタ 7cm シルバー HDM07-042ADS  | 初期構築時に使用 |

### キッティング
ラズパイを箱から出してヒートシンクを貼っていきます
![IMG_7955.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/41c62ba4-8ed2-22a4-353b-16afc78f2b52.jpeg)
![IMG_7956.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/7d59c614-8383-71d4-8a8a-3cc768a2b7c1.jpeg)
GeekPi クラスタケースに収容していきます。
ラズパイにmicrosdの付属のライザーカードをつけたあとアクリルボードの保護シートを剥がして固定します。
このカードをかませることで分解せずともケース後方からmicrosdの抜き差しが可能になります。(単一障害点が増えるけど)
最初からネジをすべて締めるとなかなか入って行かず基板を破損させてしまいそうになるので、緩めの状態ですべてネジを通してから固定させると付けやすいです。
冷却ファンにもアクリルボードを取り付けます。
![IMG_7957.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/f19dafac-1315-e491-826d-941805ca745a.jpeg)
サイドパネルに差し込みます。方向が違うと刺さらないようになってますね。※写真では4つラズパイ使ってますが後で1台外しています。
![IMG_7958.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/26435457-e9e9-d80b-d63f-98bf7ab9c91c.jpeg)

### 全体図
全て取り付けるとこんな感じです。
今回はubuntuを使うのでcanonicalの中の人に貰ったステッカーでも貼っておきます。
![IMG_7986.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/4cc9259f-1a5a-c55f-b40e-8c88489e4946.jpeg)
![IMG_7985.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/3b3911da-7a1b-e51f-5abf-0c2328d2dd9c.jpeg)
充電器からの給電は左から冷却ファン、スイッチングハブ、ラズパイx3　です。
![IMG_7984.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/494fbeb1-9315-2f27-67d4-42a5abee81a9.jpeg)

クラスタの構成はこんな感じ
![IMG_7983_キャプション付き.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/51f931c0-f5d6-b2e5-c9b6-8e276e003bb3.png)
#構築・ソフトウェア編

###OSインストール **(全台で実行)**
今回はUbntu Server 20.04 LTS(arm64)を使っていきます。

```shell
$ cat /etc/os-release
NAME="Ubuntu"
VERSION="20.04.1 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.1 LTS"
VERSION_ID="20.04"
#(以下省略)
```

事前にubuntu desktop 20.04 LTS(amd64)で検証したのですが、k8sの導入でコケました。
また、今後8GBモデルを買った際にmicrosdを差し替えて移行することも考えられるので、raspberry pi OSではなく64bit版があるubuntuにしました。
純正の書き込みツールでmicroSDにOSを書き込んでいきます。microsdはPCに挿しておきます。
[Raspberry Pi OS – Raspberry Pi Imager](https://www.raspberrypi.org/software/) から環境に合ったものをダウンロードします。
※この記事ではWindowsでやっています。
起動すると以下の画面になります。Operating System>[CHOOSE OS]からmicrosdに書き込むOSを選択します。
![2020-12-05 (8).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/5eec7f4a-23dc-6838-3ed4-9a0fbb16783d.png)
Ubuntuを選択
![2020-12-05 (9).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/0c82a4a6-d032-7c6a-aa60-ffbed4b28ad4.png)
Ubuntu Server 20.04.1 LTS(RPi3/4)を選択
![2020-12-05 (11).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/95600f11-a3cf-9f69-b5bf-f13e38fc089b.png)
OSが選択されましたね。次はSD Card>[CHOOSE SD CARD]からmicrosdを選択します。
![2020-12-05 (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/c3df2534-8adf-eaf7-a425-7a6575fbef7f.png)
挿しておいたmicrosdを選択します。名前はmicrosdの種類やロットで変わると思いますので容量で判断かなあ。
余談ですがpanasonic製microsdフォーマットツールとかだと**microsd以外のストレージ**も選択出来てしまうので誤ってフォーマットしてしまうことがありました。
まあ余計なものは取り外ししておいたほうがいいですね。
![2020-12-05 (15).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/45cc1983-8201-ce72-cc12-776d8c38810f.png)
[WRITE]を選択
![2020-12-05 (16).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/86bbdf76-594e-e6ac-fc3c-4500a76706af.png)
中身全部消えるけどええんか？という確認が出ます。確認して[YES]を選択
![2020-12-05 (14).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/c02f6e80-e349-25ae-bcc2-d4b47d919b59.png)
10分～20分待ちます。microssdの書き込み速度で変わると思います。
![2020-12-05 (5).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/ca4886b5-53d3-931f-258e-260d6ed3ea8b.png)
書き込みが終わると。「書き込みが終わったんで抜いていいっす」というダイアログが出るので[CONTINUE]を選択して閉じます。
![2020-12-05 (17).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/95869b91-de24-4ab6-50a6-2f642200ad15.png)
3台分やったら終わりです。

###OS初期設定 **(全台で実行)**
前段作業で書き込んだmicrosdをrasberry piへ差し込み、起動させます。
raspberry piは給電されると自動的に電源が入ります。自分の場合は1台ずつ電源を入れて設定していきました。
設定対象のraspberry piにmicro HDMI端子↔HDMI変換ケーブルを挿してモニタで作業します。(今回は検証用のケーブルを引き回してあるTV使いました。)
ubuntu serverでは初期設定でGUIは無効化、SSHが有効化してあります。ubuntu desktopではgnomeが起動、sshはインストールされていない
コマンドラインが走り、```ubuntu login:```と出れば起動完了です。
![IMG_7972.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/409537/5c0fe8af-889f-88a5-4c85-bdb30b7465bc.jpeg)

#####初期パスワード変更
ubuntuユーザーの初期パスワードはubuntuで、ログインするとパスワード変更を即されるので変更します。
**キーボードが英字設定**なので記号などを含めるのは気をつけたほうがいいです。
後で日本語設定入れてから```password```コマンド使えばいいので、仮でいい気がします。

####ipアドレスを確認

```shell
$ ip a
#(中省略)
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether XX:XX:XX:XX:XX:ac brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.5/24 brd 192.168.0.255 scope global eth0
#(以下省略)
```
eth0の結果を確認すればいいので結果が上記の場合```192.168.0.5```がDHCPで自動的に割あたったIPですね。

###★以下の手順以降はPCからSSHクライアント(Teraterm等)から操作します。★

#####【全ノードで実行】ビルトインアカウント無効化と作業アカウント(きくりんユーザー)作成

```shell
## きくりんユーザー作成
$ sudo useradd -m -s /usr/bin/bash kickling
## きくりんユーザーパスワード設定
$ sudo passwd kickling
## きくりんユーザーをsudo権限付与
$ sudo adduser kickling sudo
## きくりんユーザーをsudo権限付与確認
$ cat /etc/group | grep sudo
sudo:x:27:ubuntu,kickling
## ビルトイン(ubuntu)をログイン無効化
$ sudo usermod -s /usr/sbin/nologin ubuntu
## 無効化(nologin)確認
$ cat /etc/passwd | grep ubuntu
```

#####【管理ノードで実行】管理ノードのネットワーク設定
ホスト名設定

```shell
## ホスト名設定
$ sudo hostnamectl set-hostname ras-k8s-master1
## ホスト名確認
$ hostname
ras-k8s-master1
```

ipアドレス(ネットワーク)設定

```shell
## ホスト名設定
$ sudo nano /etc/netplan/99-network.yaml
##--ここから--
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: false
      dhcp6: false
      addresses:
        - 192.168.0.71/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
          - 192.168.0.1
##--ここまでコピーアンドペースト--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## 設定適用
$ sudo netplan apply
##SSHが切断されるので設定したIPでログインし直す
```

#####【ワーカーノード1で実行】ワーカーノード1のネットワーク設定
ホスト名設定

```shell
## ホスト名設定
$ sudo hostnamectl set-hostname ras-k8s-worker1
## ホスト名確認
$ hostname
ras-k8s-worker1
```

ipアドレス(ネットワーク)設定

```shell
## ホスト名設定
$ sudo nano /etc/netplan/99-network.yaml
##--ここから--
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: false
      dhcp6: false
      addresses:
        - 192.168.0.72/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
          - 192.168.0.1
##--ここまでコピーアンドペースト--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## 設定適用
$ sudo netplan apply
##SSHが切断されるので設定したIPでログインし直す
```

#####【ワーカーノード2で実行】ワーカーノード2のネットワーク設定
ホスト名設定

```shell
## ホスト名設定
$ sudo hostnamectl set-hostname ras-k8s-worker2
## ホスト名確認
$ hostname
ras-k8s-worker2
```

ipアドレス(ネットワーク)設定

```shell
## ホスト名設定
$ sudo nano /etc/netplan/99-network.yaml
##--ここから--
network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: false
      dhcp6: false
      addresses:
        - 192.168.0.73/24
      gateway4: 192.168.0.1
      nameservers:
        addresses:
          - 192.168.0.1
##--ここまでコピーアンドペースト--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## 設定適用
$ sudo netplan apply
##SSHが切断されるので設定したIPでログインし直す
```

###【全ノードで実行】共通OS設定
一通りネットワーク設定が終わったので接続し直します。

| ホスト名 | IPアドレス | ログインユーザ |
|:-:|:-:|:-:|
|ras-k8s-master1 | 192.168.0.71 | kickling |
|ras-k8s-worker1 | 192.168.0.72 | kickling |
|ras-k8s-worker2 | 192.168.0.73 | kickling |

####hostsファイルへ追記

```shell
## hosts編集
$ sudo nano /etc/hosts
##--ここから--
# Kubernetes Clusters
192.168.0.71       k8s-master1 ras-k8s-master1
192.168.0.72       k8s-worker1 ras-k8s-worker1
192.168.0.73       k8s-worker2 ras-k8s-worker2
##--ここまで下に追記--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## hosts確認
$ cat /etc/hosts
```
####swapの無効化
Docker及びKubernetesの実行時はswapの無効化が必要になるのですが

```shell
$ free
              total        used        free      shared  buff/cache   available
Mem:        3884348      945272     1677080        5184     1261996     3001004
Swap:             0           0           0 # ←全て0
```
最初から無効化されていました。大抵の場合有効になってるはずなので```swap off```して```reboot```
してあげる必要があると思います。

####time zone変更

```shell
$ sudo timedatectl set-timezone Asia/Tokyo
$ timedatectl | grep Time
Time zone: Asia/Tokyo (JST, +0900)
```

####key map変更

```shell
$ sudo localectl set-keymap jp106
$ localectl
   System Locale: LANG=C.UTF-8
       VC Keymap: jp106
      X11 Layout: jp
       X11 Model: jp106
     X11 Options: terminate:ctrl_alt_bksp
```

####IPv6の無効化(任意)
```shell
## sysctl.conf編集
$ sudo nano /etc/sysctl.conf
##--ここから--
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.eth0.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
##--ここまで下に追記--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## カーネルパラメータ反映
$ sudo sysctl -p
## ipv6無効化確認(inet6が表示されないこと)
$ ip a
```

####パッケージの更新

```shell
$ sudo apt update
$ sudo apt -y upgrade
```

####cgruop でmemoryの有効化
Ubuntu Server 20.04.1 LTS(RPi3/4)ではデフォルトで無効化されているようです。
↓3列目が0 = enableが無効

```shell
$ cat /proc/cgroups | grep memory
memory  0       105     0
```
/boot/firmware/cmdline.txt に追記します。
1行目に追記します。全一行のファイルなので改行して行追加とかでは無いです。
**記法間違えると再起動後に上がってこないかも**

```shell
## cmdline.txt編集
$ sudo nano /boot/firmware/cmdline.txt
##--ここから--(1文字目はスペース)
 cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory
##--ここまで1行目の最後に追記--
#ctrl + xで保存、yでファイル名指定(変えなくていい)、Enterで閉じる
## cmdline.txt確認
$ cat /boot/firmware/cmdline.txt
net.ifnames=0 dwc_otg.lpm_enable=0 console=serial0,115200 console=tty1 root=LABEL=writable rootfstype=ext4 elevator=deadline rootwait fixrtc cgroup_enable=cpuset cgroup_memory=1 cgroup_enable=memory
## 設定反映のため、再起動
$ sudo reboot
```

再起動後、確認
↓3列目が1 = enableが1なので有効になってますね。

```shell
$ cat /proc/cgroups | grep memory
memory  x       x     1
```

####iptableがのnftablesバックエンドを参照しないようにする

```shell
$ sudo apt-get -y install iptables arptables ebtables
$ sudo update-alternatives --set iptables /usr/sbin/iptables-legacy
$ sudo update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy
$ sudo update-alternatives --set arptables /usr/sbin/arptables-legacy
update-alternatives: using /usr/sbin/arptables-legacy to provide /usr/sbin/arptables (arptables) in manual mode
$ sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy
update-alternatives: using /usr/sbin/ebtables-legacy to provide /usr/sbin/ebtables (ebtables) in manual mode
```

####Dockerのインストール

```shell
## 6行まとめてコピーアンドペーストで貼り付けて実行する
$ sudo apt-get -y install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
## リポジトリキー追加
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
## arm64向けDockerリポジトリ追加
$ sudo add-apt-repository "deb [arch=arm64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get -y install docker-ce docker-ce-cli containerd.io
## Docker関連バージョン固定設定
$ sudo apt-mark hold docker-ce docker-ce-cli containerd.io
docker-ce set on hold.
docker-ce-cli set on hold.
containerd.io set on hold.
```

作業ユーザーをDockerグループに追加

```shell
$ sudo adduser kickling docker
Adding user `kickling' to group `docker' ...
Adding user kickling to group docker
Done.
$ cat /etc/group | grep docker    # 確認する
docker:x:998:kickling
```
グループ割当を反映させるため一旦```exit```で抜けてログインし直します。

Dockerのバージョン確認(任意)

```shell
$ sudo docker version
Client: Docker Engine - Community
 Version:           19.03.14
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        5eb3275
 Built:             Tue Dec  1 19:20:49 2020
 OS/Arch:           linux/arm64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.14
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       5eb3275
  Built:            Tue Dec  1 19:19:19 2020
  OS/Arch:          linux/arm64
  Experimental:     false
 containerd:
  Version:          1.3.9
  GitCommit:        ea765aba0d05254012b0b9e595e995c09186427f
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
```

Dockerの動作確認(Hello worldコンテナの起動)
""Hello from Docker!""と表示されればOKです。

```shell
$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world

256ab8fe8778: Pulling fs layer 256ab8fe8778: Downloading     424B/3.367kB256ab8fe8778: Downloading  3.367kB/3.367kB256ab8fe8778: Download complete 256ab8fe8778: Extracting  3.367kB/3.367kB256ab8fe8778: Extracting  3.367kB/3.367kB256ab8fe8778: Pull complete Digest: sha256:e7c70bb24b462baa86c102610182e3efcb12a04854e8c582838d92970a09f323
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (arm64v8)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/
```

####Kubernetesのインストール
kubeadm、kubectl、kubeletをインストールします。

```shell
$ sudo apt-get update && sudo apt-get -y install apt-transport-https curl
$ curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
$ cat <<EOF | sudo tee /etc/apt/sources.list.d/kubernetes.list
deb https://apt.kubernetes.io/ kubernetes-xenial main
EOF
$ sudo apt-get update
$ sudo apt-get -y install kubelet kubeadm kubectl


## 2021/03/29追記 失敗する場合は以下を追加入力後sudo apt-get -y install kubelet kubeadm kubectl　
$ sudo curl -fsSLo /usr/share/keyrings/kubernetes-archive-keyring.gpg https://packages.cloud.google.com/apt/doc/apt-key.gpg
$ echo "deb [signed-by=/usr/share/keyrings/kubernetes-archive-keyring.gpg] https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list
$ sudo apt-get update
## 追記ここまで

## kubelet kubeadm kubectlのバージョン固定
$ sudo apt-mark hold kubelet kubeadm kubectl
kubelet set on hold.
kubeadm set on hold.
kubectl set on hold.
```

各モジュールのバージョン確認

```shell
$ kubeadm version -o json
{
  "clientVersion": {
    "major": "1",
    "minor": "19",
    "gitVersion": "v1.19.4",
    "gitCommit": "d360454c9bcd1634cf4cc52d1867af5491dc9c5f",
    "gitTreeState": "clean",
    "buildDate": "2020-11-11T13:15:05Z",
    "goVersion": "go1.15.2",
    "compiler": "gc",
    "platform": "linux/arm64"
  }
}

$ kubectl version -o json
{
  "clientVersion": {
    "major": "1",
    "minor": "19",
    "gitVersion": "v1.19.4",
    "gitCommit": "d360454c9bcd1634cf4cc52d1867af5491dc9c5f",
    "gitTreeState": "clean",
    "buildDate": "2020-11-11T13:17:17Z",
    "goVersion": "go1.15.2",
    "compiler": "gc",
    "platform": "linux/arm64"
  }
}
The connection to the server localhost:8080 was refused - did you specify the right host or port?

$ kubelet --version
Kubernetes v1.19.4
```

###Kubernetes クラスタの設定
#####【管理ノードで実行】Master Nodeの初期化
k8sクラスタの初期化作業を実行します。

| オプション名 | 値 | 備考 |
|:-:|:-:|:-:|
|apiserver-advertise-address | 192.168.0.71 | 管理ノードのIP |
|pod-network-cidr | 10.244.0.0/16 | ポッド(flannel)のアドレス帯域指定 |

```shell
$ sudo kubeadm init --apiserver-advertise-address=192.168.0.71 --pod-network-cidr=10.244.0.0/16
[sudo] password for kickling: 
W1205 09:15:19.362924    2871 configset.go:348] WARNING: kubeadm cannot validate component configs for API groups [kubelet.config.k8s.io kubeproxy.config.k8s.io]
[init] Using Kubernetes version: v1.19.4
[preflight] Running pre-flight checks
	[WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". Please follow the guide at https://kubernetes.io/docs/setup/cri/
	[WARNING SystemVerification]: missing optional cgroups: hugetlb
[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'
[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 [kubernetes kubernetes.default kubernetes.default.svc kubernetes.default.svc.cluster.local ras-k8s-master1] and IPs [10.96.0.1 192.168.0.71]
[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 [localhost ras-k8s-master1] and IPs [192.168.0.71 127.0.0.1 ::1]
[certs] Generating "etcd/peer" certificate and key
[certs] etcd/peer serving cert is signed for DNS names [localhost ras-k8s-master1] and IPs [192.168.0.71 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
[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
[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"
[control-plane] Creating static Pod manifest for "kube-scheduler"
[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 35.013277 seconds
[upload-config] Storing the configuration used in ConfigMap "kubeadm-config" in the "kube-system" Namespace
[kubelet] Creating a ConfigMap "kubelet-config-1.19" in namespace kube-system with the configuration for the kubelets in the cluster
[upload-certs] Skipping phase. Please see --upload-certs
[mark-control-plane] Marking the node ras-k8s-master1 as control-plane by adding the label "node-role.kubernetes.io/master=''"
[mark-control-plane] Marking the node ras-k8s-master1 as control-plane by adding the taints [node-role.kubernetes.io/master:NoSchedule]
[bootstrap-token] Using token: g9yaoj.xs04rxh4avt5mikd
[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.0.71:6443 --token g9yaoj.xs04rxh4avt5mikd \
    --discovery-token-ca-cert-hash sha256:52488ac2af018a489aae35d55ead8ad784b0593a0db5c8ad79a16505cb30e1d3
```

最後の2行の
```kubeadm join 192.168.0.71:6443 --token g9yaoj.xs04rxh4avt5mikd \
    --discovery-token-ca-cert-hash sha256:52488ac2af018a489aae35d55ead8ad784b0593a0db5c8ad79a16505cb30e1d3```は
Worker Node追加時に必要なコマンドになるのでメモっておきましょう。

トークンが有効なのは24時間なので、過ぎた場合はコマンドで再発行が必要です。
**※この時点で後述の```kubeadm token list```及び```kubeadm token create```打つとエラーになります**
ので**次に環境変数設定と入力補完設定を済ませます。**

残り時間を確認する場合は```kubeadm token list```コマンドを実行してTTLの列を確認します。
何も表示されない場合は有効なトークンが無いので```kubeadm token create```コマンドで生成します。



#####【管理ノードで実行】環境変数と入力補完の設定
環境変数設定

```shell
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config
$ echo 'KUBECONFIG=$HOME/.kube/config' >> $HOME/.bashrc
$ source $HOME/.bashrc
# kubectlコマンド実行時「The connection to the server ・・・」が表示されないことを確認する
$ kubectl version -o json
```

入力補完設定

```shell
$ source <(kubectl completion bash)
$ echo "source <(kubectl completion bash)" >> $HOME/.bashrc
```

#####【管理ノードで実行】Podネットワークアドオンのインストール
Pod間の通信を行うために[flannel](https://github.com/coreos/flannel#flannel)を設定します。

```shell
$ curl https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml -O
$ kubectl apply -f kube-flannel.yml
podsecuritypolicy.policy/psp.flannel.unprivileged created
clusterrole.rbac.authorization.k8s.io/flannel created
clusterrolebinding.rbac.authorization.k8s.io/flannel created
serviceaccount/flannel created
configmap/kube-flannel-cfg created
daemonset.apps/kube-flannel-ds created
```

flannelの起動を確認します。
kube-flannel-ds-xxxが確認できればOKです。数秒かかりました。


```shell
$ kubectl get pods -n kube-system
 NAME                                      READY   STATUS    RESTARTS   AGE
 coredns-f9fd979d6-lhddv                   0/1     Running   0          7m12s
 coredns-f9fd979d6-qxpkl                   1/1     Running   0          7m12s
 etcd-ras-k8s-master1                      1/1     Running   0          7m17s
 kube-apiserver-ras-k8s-master1            1/1     Running   0          7m17s
 kube-controller-manager-ras-k8s-master1   1/1     Running   0          7m17s
 kube-flannel-ds-gr226                     1/1     Running   0          47s
 kube-proxy-r2lsh                          1/1     Running   0          7m12s
 kube-scheduler-ras-k8s-master1            1/1     Running   0          7m17s
```

#####【管理ノードで実行】LoadBalancer([MetalLB](https://metallb.universe.tf/)) のインストール
MetalLBはControllerとSpeakerの2種類のPodで構成されてます。

```shell
$ curl https://raw.githubusercontent.com/metallb/metallb/v0.9.4/manifests/namespace.yaml -o namespace.yaml
$ curl https://raw.githubusercontent.com/metallb/metallb/v0.9.4/manifests/metallb.yaml -o metallb.yaml
$ kubectl apply -f namespace.yaml
namespace/metallb-system created
$ kubectl apply -f metallb.yaml
podsecuritypolicy.policy/controller created
podsecuritypolicy.policy/speaker created
serviceaccount/controller created
serviceaccount/speaker created
clusterrole.rbac.authorization.k8s.io/metallb-system:controller created
clusterrole.rbac.authorization.k8s.io/metallb-system:speaker created
role.rbac.authorization.k8s.io/config-watcher created
role.rbac.authorization.k8s.io/pod-lister created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:controller created
clusterrolebinding.rbac.authorization.k8s.io/metallb-system:speaker created
rolebinding.rbac.authorization.k8s.io/config-watcher created
rolebinding.rbac.authorization.k8s.io/pod-lister created
daemonset.apps/speaker created
deployment.apps/controller created
$ kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"
secret/memberlist created
```

MetalLBの起動を確認します。
controller-xxx-xxx と speaker-xxxが確認できればOKです。
controllerはPendingになっていますが、worker nodeを登録後にRunningになります。

```shell
$ kubectl get pod -n metallb-system
NAME                         READY   STATUS    RESTARTS   AGE
controller-8687cdc65-s77qq   0/1     Pending   0          55s
speaker-cd548                1/1     Running   0          55s
```

#####【ワーカーノード1,ワーカーノード2で実行】Worker nodeをクラスタに追加する
ワーカーノードを登録してクラスタ化します。
"【管理ノードで実行】Master Nodeの初期化"でメモっておいた、
```kubeadm join 192.168.0.71:6443 --token g9yaoj.xs04rxh4avt5mikd \
    --discovery-token-ca-cert-hash sha256:52488ac2af018a489aae35d55ead8ad784b0593a0db5c8ad79a16505cb30e1d3```
の先頭に```sudo ```を付けてワーカーノードで実行します。

```shell
$ sudo kubeadm join 192.168.0.71:6443 --token g9yaoj.xs04rxh4avt5mikd \
    --discovery-token-ca-cert-hash sha256:52488ac2af018a489aae35d55ead8ad784b0593a0db5c8ad79a16505cb30e1d3

[sudo] password for kickling: 
[preflight] Running pre-flight checks
	[WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". Please follow the guide at https://kubernetes.io/docs/setup/cri/
	[WARNING SystemVerification]: missing optional cgroups: hugetlb
[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] 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.
```
プロンプトが返ってくるまでしばらく待ちます。

#####【管理ノードで実行】Worker nodeのクラスタ追加を確認
Master nodeで確認します。
ras-k8s-worker1とras-k8s-worker2がReadyになってれば大丈夫ですね。
また、MetalLBのcontrollerがPendingからRunningになりました。

```shell
$ kubectl get nodes
NAME              STATUS   ROLES    AGE    VERSION
ras-k8s-master1   Ready    master   12m    v1.19.4
ras-k8s-worker1   Ready    <none>   100s   v1.19.4
ras-k8s-worker2   Ready    <none>   75s    v1.19.4
$ kubectl get pods -A
NAMESPACE        NAME                                      READY   STATUS    RESTARTS   AGE
kube-system      coredns-f9fd979d6-lhddv                   1/1     Running   0          12m
kube-system      coredns-f9fd979d6-qxpkl                   1/1     Running   0          12m
kube-system      etcd-ras-k8s-master1                      1/1     Running   0          12m
kube-system      kube-apiserver-ras-k8s-master1            1/1     Running   0          12m
kube-system      kube-controller-manager-ras-k8s-master1   1/1     Running   0          12m
kube-system      kube-flannel-ds-bbgrn                     1/1     Running   0          2m2s
kube-system      kube-flannel-ds-gr226                     1/1     Running   0          5m42s
kube-system      kube-flannel-ds-mcfw9                     1/1     Running   0          97s
kube-system      kube-proxy-gsmwm                          1/1     Running   0          97s
kube-system      kube-proxy-p6szf                          1/1     Running   0          2m2s
kube-system      kube-proxy-r2lsh                          1/1     Running   0          12m
kube-system      kube-scheduler-ras-k8s-master1            1/1     Running   0          12m
metallb-system   controller-8687cdc65-s77qq                1/1     Running   0          3m40s
metallb-system   speaker-cd548                             1/1     Running   0          3m40s
metallb-system   speaker-cs6p4                             1/1     Running   0          61s
metallb-system   speaker-z75xl                             1/1     Running   0          46s
```

#####【管理ノードで実行】Worker Nodeにラベルを設定する
```shell
$ kubectl label node ras-k8s-worker1 node-role.kubernetes.io/worker=worker
node/ras-k8s-worker1 labeled
$ kubectl label node ras-k8s-worker2 node-role.kubernetes.io/worker=worker
node/ras-k8s-worker2 labelednode/ras-k8s-worker2 labeled
#ROLESを確認する
$ kubectl get nodes
NAME              STATUS   ROLES    AGE   VERSION
ras-k8s-master1   Ready    master   21m   v1.19.4
ras-k8s-worker1   Ready    worker   11m   v1.19.4
ras-k8s-worker2   Ready    worker   11m   v1.19.4
```

####Kubernetes で コンテナを動かす
[Docker が動作しているホストのHostnameを表示するNginxコンテナ - Qiita](https://qiita.com/yasthon/items/6a4627f249bb7fa52eb9)
を参考に、どのノードと接続しているのか確認していきます。
yamlをエディタで作成します。

```yaml:display-hostname.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: nginx-prod
---
apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: pool-ips  # MetallbのIPプール名
      protocol: layer2
      addresses:
      - 192.168.0.211-192.168.0.215
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service-lb # Service(LoadBalancer) の名前
  namespace: nginx-prod
  annotations:
    metallb.universe.tf/address-pool: pool-ips # MetallbのIPプール名
spec:
  type: LoadBalancer
  ports:
    - name: nginx-service-lb
      protocol: TCP
      port: 8080 # ServiceのIPでlistenするポート
      nodePort: 30080 # nodeのIPでlistenするポート（30000-32767）
      targetPort: 80 # 転送先(コンテナ)でlistenしているPort番号のポート
  selector: # service のselctorは、matchLabels 扱いになる
    app: nginx-pod # 転送先の Pod のラベル
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment # Deployment の名前(ReplicaSetの名前もこれになる)
  namespace: nginx-prod
spec:
  selector:
    matchLabels: # ラベルがマッチしたPodを対象とするReplicaSetの作成
      app: nginx-pod
  replicas: 2
  template: # Pod のテンプレート
    metadata:
      name: nginx-pod # Pod の名前
      namespace: nginx-prod
      labels: # Pod のラベル
        app: nginx-pod
    spec:
      containers: # コンテナの設定
        - name: nginx-container # コンテナの名前
          image: yasthon/nginx-display-hostname # イメージの名前
          env:
            - name: nginx-container
          ports:
            - containerPort: 80 # コンテナのポート
          volumeMounts:
            - name: file-hostname
              mountPath: /usr/share/nginx/html/hostname
      volumes:
        - name: file-hostname
          hostPath:
            path: /etc/hostname
```

リソースを作成します。
Pod、Service、Deployment、ReplicaSet、ConfigMapの作成を確認します。
ServiceのEXTERNAL-IPは、IPプールから割り当たっています。

```shell
$ kubectl apply -f display-hostname.yaml
namespace/nginx-prod created
configmap/config created
service/nginx-service-lb created
deployment.apps/nginx-deployment created

$ kubectl get all -n nginx-prod
NAME                                    READY   STATUS              RESTARTS   AGE
pod/nginx-deployment-7ff4cc65cd-nvchj   0/1     ContainerCreating   0          19s
pod/nginx-deployment-7ff4cc65cd-zlwnv   0/1     ContainerCreating   0          19s

NAME                       TYPE           CLUSTER-IP       EXTERNAL-IP       PORT(S)          AGE
service/nginx-service-lb   LoadBalancer   10.102.181.109   192.168.100.211   8080:30080/TCP   19s

NAME                               READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/nginx-deployment   0/2     2            0           19s

NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/nginx-deployment-7ff4cc65cd   2         2         0       19s
#確認する
$ kubectl get configmap -n metallb-system
NAME     DATA   AGE
config   1      47s
```

Serviceの詳細情報から、LoadBalancer Ingress とPortの値を確認します。

```shell
$ kubectl describe svc nginx-service-lb -n nginx-prod
Name:                     nginx-service-lb
Namespace:                nginx-prod
Labels:                   <none>
Annotations:              metallb.universe.tf/address-pool: pool-ips
Selector:                 app=nginx-pod
Type:                     LoadBalancer
IP:                       10.102.181.109
LoadBalancer Ingress:     192.168.100.211
Port:                     nginx-service-lb  8080/TCP
TargetPort:               80/TCP
NodePort:                 nginx-service-lb  30080/TCP
Endpoints:                10.244.1.3:80,10.244.2.2:80
Session Affinity:         None
External Traffic Policy:  Cluster
Events:
  Type    Reason        Age                From                Message
  ----    ------        ----               ----                -------
  Normal  IPAllocated   58s                metallb-controller  Assigned IP "192.168.100.211"
  Normal  nodeAssigned  42s                metallb-speaker     announcing from node "ras-k8s-worker1"
  Normal  nodeAssigned  32s                metallb-speaker     announcing from node "ras-k8s-worker2"
```

####接続確認
LoadBalancer IngressのIPアドレスとPortのポート番号へcurlして接続確認します。

```shell
$ curl 192.168.0.211:8080/index.sh
<html><head>
<title>ras-k8s-worker2</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
</head><body>
HOSTNAME : ras-k8s-worker1
</body></html>
```

何度か繰り返すと接続先がras-k8s-worker2になったりras-k8s-worker1に戻ったりして
LBで振り分けされていることがわかります。

####抜線確認
ワーカーノード2のLANケーブルを抜きます。
ras-k8s-worker2の振り分けは停止して、ras-k8s-worker1へ接続のみになることが確認できます。

```shell
$ curl 192.168.0.211:8080/index.sh
<html><head>
<title>ras-k8s-worker2</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
</head><body>
HOSTNAME : ras-k8s-worker1
</body></html>
$ curl 192.168.0.211:8080/index.sh
<html><head>
<title>ras-k8s-worker2</title>
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
</head><body>
HOSTNAME : ras-k8s-worker1
</body></html>

##ノード一覧を確認するとras-k8s-worker2はNotReadyになっています
$ kubectl get nodes
NAME              STATUS     ROLES    AGE   VERSION
ras-k8s-master1   Ready      master   44m   v1.19.4
ras-k8s-worker1   Ready      worker   34m   v1.19.4
ras-k8s-worker2   NotReady   worker   34m   v1.19.4
```

結線し直すと戻ります。

ブラウザからは
```http://192.168.0.211:8080/index.sh```で確認できます。

##所感
後述する先駆者様の写経がベースとなりましたが、環境差分やマークダウン記法が初めてなこともあり
結構書き直しの部分が大半で勉強になりました。
また、Kubernetesの構築は何度も挑戦するもkubeletの初期化エラーにぶつかり挫折しかけていたので
今回ついにKubernetesの実機環境ができあがって非常に嬉しいです。
今はなんとなく**よくわからんがなんか出来た**状態なので少しづつ検証して仕組みを勉強したいと思います。

以上、k8sクラスタ構築とwebサーバの動確まで　でした。

#参考
[kubeadmを使ってクラスターを構築する | Kubernetes](https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/)
[RaspberryPi 4 にUbuntu20.04 をインストールして、Kubernetes を構築してコンテナを動かす - Qiita](https://qiita.com/yasthon/items/c29d0b9ce34d66eab3ec)
[3台のRaspberry Pi 4でKubernetesクラスターを構築する（Ubuntu Server 20.04 LTS 64bit） | kimama.cloud](https://kimama.cloud/2020/08/02/raspi-de-k8s/)
[Docker が動作しているホストのHostnameを表示するNginxコンテナ - Qiita](https://qiita.com/yasthon/items/6a4627f249bb7fa52eb9)
[MetalLB, bare metal load-balancer for Kubernetes](https://metallb.universe.tf/)
[KubernetesロードバランサーのMetalLBを導入した話(Necoプロジェクト体験入部) - Cybozu Inside Out | サイボウズエンジニアのブログ](https://blog.cybozu.io/entry/2019/03/25/093000)
[Markdown記法 サンプル集 - Qiita](https://qiita.com/tbpgr/items/989c6badefff69377da7)
[Raspberry Piでkubernetesクラスタを組む | SIOSDX | コンテナの窓口](https://container.sios.jp/tech/raspberry-pi-kubernetes/)