ラズパイ4で作るディスプレイ付きKubernetesクラスター

完成品

まずは完成したクラスターをご紹介します。

コンセプト

ラズパイk8sクラスターなんて先人達が幾度となく構築、記事に残しており、ぶっちゃけ枯れた分野です。
普通に作ってもつまらないので、以下のコンセプトで作成してみました。

コンセプト1. ディスプレイ

見た目を良くしたい、なんとなくラズパイ用小型ディスプレイを試してみたい、といった理由から、ディスプレイを取り付ける方針としました。
最上段に上向きにつけると見づらいので、側面に取り付ける方向で検討しました。

コンセプト2. 電源

Raspberry Pi 4 の電力仕様である5V/3Aを供給することを目的としました。
(ラズパイ3時代はもっと少なくてよかった)
USB給電が少ない場合は2.5Aで十分と公式ドキュメントに書いてあります。
また、よくある構築例だとAnkerの1ポートあたり2.4AのUSB電源を利用している場合が多く、特にこれで問題は発生しないようです。
が、やはり仕様はきちんと満たしたいので、きっちり5V/3Aを供給できる電源を取り付ける方針としました。

コンセプト3. ネットワーク

ノード間はギガビットイーサ(1000BASE-T)で接続することをコンセプトとしました。
こちらもよくある構築例では、USB給電可能なメガビットイーサ(100BASE-T)対応のスイッチングハブが利用されています。
せっかくのギガビットイーサ対応Raspberry Piなので、意図的にボトルネックを作りたくないので、ギガビットイーサにこだわりました。

ここからはハードウェアとソフトウェアに分けて構成パーツや構築の流れを紹介します。
興味のある部分だけ見ていただく形がよいかと思います。

ハードウェア

まずはハードウェアのご紹介から。

構成パーツ一覧

最終的に構築に利用したパーツと料金、リンクを下記の表に示します。

分類名前値段リンク
Raspberry Pi本体Raspberry Pi 4 Model B 4GB * 3台¥7315 * 3Amazon
SDカードSunDisk 32GB UHS-I Class10 * 3個¥1009 * 3Amazon
ケース積層ケース for Raspberry Pi 4¥1889Amazon
ディスプレイOSOYOO 3.5インチ LCD タッチスクリーン¥2380Amazon
ジャンパーブレッドボード ジャンパーワイヤ メス-オス 20cm¥196Amazon
電源Anniber QC3.0 ACアダプター * 3個¥997 * 3Amazon
電源ケーブルUSB type-c 【30cm3本】¥690Amazon
スイッチングハブエレコム スイッチングハブ EHC-G05PA-SB¥2351Amazon
LANケーブルエレコム LANケーブル 0.15m 2個入り * 2個¥579 * 2Amazon
LANケーブルAucas カテゴリ7 ウルトラフラット 1m¥898Amazon
その他金具、ネジ¥638Amazon
その他基盤スタンド¥858Amazon
合計¥39030

ハードウェア詳細

Raspberry Pi本体

Raspberry Pi 4では、RAMが1GB、2GB、4GBのモデルが販売されています。
ベンチマーク結果を見て当初は2GBが最もコストパフォーマンスに優れていそうなので、これにしようかと思っていたのですが、金額を気にしなくて良くなったため4GBモデルを購入することにしました。

台数はクラスターを組むにあたり最低3台必要なので、とりあえずは3台としました。

microSD

Raspberry Pi 4では64GB以上のサイズも利用可能になりました。
しかし、64GB以上のmicroSDはフォーマットの仕方に癖があるようなので、今回は32GBとしました。

ディスプレイ

ディスプレイは上面に設置するタイプがほとんどなので、L字金具 + 基盤スタンドで側面に固定
GPIO端子は直付けではなく、ジャンパーを使って配線しています。

ディスプレイ選びですが、ちょうどよいサイズの3.5インチに関しては数社から販売されております。
価格/性能ともに特に差は見受けられなかったため、背面の基盤配置的にもっとも取り付けやすそうなものを購入しました。

画面に出力している内容に関しては後述します。

ケース

ラズパイクラスターといったら、やはり積層ケースでしょう。
いくつか探してみたのですが、結局は定番の以下のものに落ち着きました。

電源

Raspberry Pi 4の要求電源は5V/3Aです。
(USB Downstreamが500mA以下なら2.5Aで可)

当初は下の画像のような、複数のUSBポートを備えたUSB充電器を探していたのですが、複数ポートの場合は、どうしても「各ポート2.4Aまで」という制限がついています。

(いちおうこの程度の供給でも動作はするという報告は上がっているようです)

1つのUSB充電器で供給することは諦め、1ポートのUSB電源を3個購入しました。

クラスターのケースに取り付けることはできなくなりましたが、もともと可搬性は気にしていなかったので問題なしとしています。

スイッチングハブ

以下の2点を満たすものを探しました。

  • USB給電可能
  • 1000BASE-T対応

USB給電に関しては、ラズパイ側の都合で電源に複数ポート備えたUSB充電器を使わないので、実はなんでも良かったりします。
ただ、今後5V/3A対応の複数ポートのUSB充電器が発売されたらそちらに乗り換えたいため、USB給電可能なものを選択しました。


ソフトウェア

続いてソフトウェア面です。

構築手順

構築手順をざっと紹介します。

OSのセットアップ

Raspberry Pi用Debianの最新であるRaspbian Busterのheadless(X11なし)を採用しています。
公式の手順を見ながらインストールを行いました。

# デバイスを確認
$ lsblk -p

# microSDをunmount
$ umount /dev/sda1

# イメージの書き込み
$ sudo dd bs=4M if=2019-09-26-raspbian-buster-lite.img of=/dev/sda status=progress conv=fsync
$ sync

# microSDを取り外し、再度取り付ける

# 起動時にsshを有効にする設定
$ touch /media/reireias/boot/ssh

# Dockerを利用するために、cgroupに関する設定を追加します
$ vi /media/reireias/boot/cmdline.txt
# 下記を末尾に追加します
cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1

ここで一旦ラズパイを起動します。

# authorized_keysの設定
$ scp ~/.ssh/id_rsa.pub pi@ip:/tmp/authorized_keys
$ ssh pi@ip
$ mkdir ~/.ssh
$ cp /tmp/authorized_keys ~/.ssh/authorized_keys

# 初期パスワードを変更
$ sudo passwd pi

# swapを無効化
$ sudo systemctl disable dphys-swapfile.service

# ホスト名を設定
# 今回は pikube01 ~ pikube03 という名前にしました
$ sudo vi /etc/hosts
$ sudo vi /etc/hostname

# 何らかの手段でIPを固定化しておくと便利でしょう
# 私はルーターの機能で固定DHCPを設定しています

# packageの更新
$ sudo apt update
$ sudo apt upgrade

# もろもろを反映させるために再起動
$ sudo reboot

ディスプレイの設定

ディスプレイを接続し、公式のドキュメントに従い設定していきます。

$ sudo apt install git
$ git clone https://github.com/kedei/LCD_driver
$ chmod -R 777 LCD_driver
$ cd LCD_driver
$ ./LCD35_show

デフォルトのフォントは少し読みづらかったので変更します。

$ sudo vi /etc/default/console-setup
# 下記を設定
FONTFACE="Terminus"
FONTSIZE="8x16"

これで無事ディスプレイに表示されました。
あとはキーボードを接続し、ログインすれば任意の画面を表示できるはずです。

dockerのインストール

下記の公式ドキュメント通りに構築を行っています。
https://kubernetes.io/ja/docs/setup/production-environment/container-runtimes/

# dockerのインストール
# Debian busterのarm用パッケージはまだ配信されていないので、debファイルをダウンロードし、インストールしている
$ wget https://download.docker.com/linux/debian/dists/buster/pool/stable/armhf/containerd.io_1.2.6-3_armhf.deb
$ wget https://download.docker.com/linux/debian/dists/buster/pool/stable/armhf/docker-ce-cli_19.03.5~3-0~debian-buster_armhf.deb
$ wget https://download.docker.com/linux/debian/dists/buster/pool/stable/armhf/docker-ce_19.03.5~3-0~debian-buster_armhf.deb
$ sudo dpkg -i containerd.io_1.2.6-3_armhf.deb
$ sudo dpkg -i docker-ce-cli_19.03.5~3-0~debian-buster_armhf.deb
$ sudo dpkg -i docker-ce_19.03.5~3-0~debian-buster_armhf.deb
$ sudo usermod pi -aG docker

kubeadmとkubectlのインストール

Kubernetesクラスターの構築に利用するkubeadminと、Kubernetesを操作するkubectlをインストールします。
公式の手順はこちら。
https://kubernetes.io/ja/docs/setup/production-environment/tools/kubeadm/install-kubeadm/

$ 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
$ sudo update-alternatives --set ebtables /usr/sbin/ebtables-legacy
$ sudo apt update && sudo apt install -y 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 update
$ sudo apt install -y kubelet kubeadm kubectl
$ sudo apt-mark hold kubelet kubeadm kubectl

クラスターの構築

まずはマスターノードとするpikube01上で下記を実行します。

# マスターノードの初期化
# IPは後述のflannelを使う都合でこれで固定
$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

# kubectlを実行できるように設定
$ mkdir -p $HOME/.kube
$ sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
$ sudo chown $(id -u):$(id -g) $HOME/.kube/config

# コンテナ用ネットワークファブリックのflannelを構築
$ kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

# 作成できたか確認
$ kubectl get pod --all-namespaces

続いてワーカーにするpikube02pikube03上で下記を実行します。

# master node作成時に出力されたコマンドを入力
$ kubeadm join 192.168.xx.yy:6443 --token xxxxxxxxxxxxxxxxxxx \
    --discovery-token-ca-cert-hash sha256:zzzzzzzzzzzzzzzzz

master側でクラスターが作成できたか確認します。

$ kubectl get nodes
# 下記のように表示されればOK
NAME       STATUS     ROLES    AGE     VERSION
pikube01   Ready      master   4m29s   v1.17.2
pikube02   Ready      <none>   47s     v1.17.2
pikube03   Ready      <none>   38s     v1.17.2

# ROLESがnoneなので、labelをつける
$ kubectl label node pikube02  node-role.kubernetes.io/worker=
$ kubectl label node pikube03  node-role.kubernetes.io/worker=

# 再確認
# ROLESが意図通りになっている
$ kubectl get nodes
NAME       STATUS   ROLES    AGE     VERSION
pikube01   Ready    master   8m38s   v1.17.2
pikube02   Ready    worker   4m56s   v1.17.2
pikube03   Ready    worker   4m47s   v1.17.2

別マシンからkubectlコマンドを利用できるようにする

# master node(pikube01)上で下記を実行し、出力をコピー
$ kubectl config view --raw

# kubectlを実行できるようにしたいマシン上で下記を実施
$ vi ~/.kube/config
# 先程コピーした設定を書き込む

# 動作確認
$ kubectl get nodes

MetalLBのインストール

ベアメタル環境Kubernetes用のL2ロードバランサー機能を提供するMetalLBをクラスターへ追加します。
MetalLBを追加することで、指定したレンジでIPアドレスを取得し、そのIPをエンドポイントとしてLAN内にServiceを公開できるようになります。kubedns container cannot connect to apiserver 揃 Issue #193 揃 kubernete…https://github.com

# 全てのnode上で下記を実施し、MetalLBで利用する通信が疎通できるようにする
$ sudo sysctl net.ipv4.conf.all.forwarding=1
$ sudo iptables -P FORWARD ACCEPT

# MetalLBの構築
kubectl apply -f https://raw.githubusercontent.com/google/metallb/v0.8.3/manifests/metallb.yaml

下記内容でConfigMap用のyamlファイルを作成します。
adressesにはDHCP等で取得できるIPアドレスのレンジを指定してください。
筆者の場合は、pikube01pikube03192.168.99.100-102となっているので、192.168.99.200-220を割り当てています。metallb-config.yml

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
      - name: default
        protocol: layer2
        addresses:
          - 192.168.99.200-192.168.99.220

構築ができたら動作確認を行います。
みんな大好きNginxで簡単なServiceとDeploymentを作成し、MetalLBによってtype: LoadBalancerなServiceにIPが割り振られることを確認します。nginx.deployment.yml

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 1
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  name: nginx-service
spec:
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80
  type: LoadBalancer
# 上記ファイルをapply
$ kubectl apply -f nginx.deployment.yml

# serviceを取得し、ExternalIPにIPが割り当てられていることを確認
$ kubectl get service
NAME            TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
kubernetes      ClusterIP      10.96.0.1       <none>           443/TCP        5d2h
nginx-service   LoadBalancer   10.97.205.133   192.168.99.200   80:32198/TCP   4d2h

# curlで上記ExternalIPへアクセスし、Welcome to Nginx! が返ることを確認する 

metrics-serverのクラスターへの追加

kubectl topコマンドを利用できるようにするために、metrics-serverを追加します。
下記を参考にさせていただきました。
https://qiita.com/yyojiro/items/febfaeadabd2fe8eed08

下記リポジトリをcloneします。
https://github.com/kubernetes-sigs/metrics-server

deploy/1.8+/metrics-server-deployment.yamlファイルをarm1用に修正します。
差分は下記のようになります。

diff --git a/deploy/1.8+/metrics-server-deployment.yaml b/deploy/1.8+/metrics-server-deployment.yaml
index e4bfeaf..f7c72f1 100644
--- a/deploy/1.8+/metrics-server-deployment.yaml
+++ b/deploy/1.8+/metrics-server-deployment.yaml
@@ -29,10 +29,12 @@ spec:
         emptyDir: {}
       containers:
       - name: metrics-server
-        image: k8s.gcr.io/metrics-server-amd64:v0.3.6
+        image: k8s.gcr.io/metrics-server-arm:v0.3.6
         args:
           - --cert-dir=/tmp
           - --secure-port=4443
+          - --kubelet-insecure-tls
+          - --kubelet-preferred-address-types=InternalIP,ExternalIP,Hostname
         ports:
         - name: main-port
           containerPort: 4443
@@ -47,4 +49,4 @@ spec:
           mountPath: /tmp
       nodeSelector:
         beta.kubernetes.io/os: linux
-        kubernetes.io/arch: "amd64"
+        kubernetes.io/arch: "arm"
# ディレクトリ以下をapplyする
$ kubectl  apply -f deploy/1.8+/

# コマンドの確認
$ kubectl top node
NAME       CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%   
pikube01   385m         9%     649Mi           17%       
pikube02   167m         4%     448Mi           11%       
pikube03   175m         4%     435Mi           11%

samplerのビルド

ディスプレイには下記のような表示を行っています。

これは、samplerというツールで表示をしているのですが、このツールがarm用ビルドに対応できないライブラリを利用しているため、一部コードを改変し、arm用にビルド可能にする必要があります。

それを実施したリポジトリがこちらです。
https://github.com/sqshq/sampler

# clone後、下記コマンドでarm[^1]用バイナリをビルド
GOOS=linux GOARCH=arm GOARM=7 go build

# scpでディスプレイを接続しているノード = マスターに転送
# パスが通っている場所へ移動
$ sudo mv /tmp/sampler /usr/bin

これでラズパイ上でsamplerコマンドが実行できるようになりました。
samplerコマンドの設定ファイルは下記のように実装しています。config.yml

gauges:
  - title: pikube01 CPU
    position: [[0, 0], [40, 6]]
    rate-ms: 30000
    color: 10
    percent-only: true
    cur:
        sample: cat /tmp/kube-node | grep pikube01 | awk '{print $3}' | tr -d "%"
    max:
        sample: echo 100
    min:
        sample: echo 0
  - title: pikube02 CPU
    position: [[0, 7], [40, 6]]
    rate-ms: 30000
    color: 13
    percent-only: true
    cur:
        sample: cat /tmp/kube-node | grep pikube02 | awk '{print $3}' | tr -d "%"
    max:
        sample: echo 100
    min:
        sample: echo 0
  - title: pikube03 CPU
    position: [[0, 13], [40, 6]]
    rate-ms: 30000
    color: 14
    percent-only: true
    cur:
        sample: cat /tmp/kube-node | grep pikube03 | awk '{print $3}' | tr -d "%"
    max:
        sample: echo 100
    min:
        sample: echo 0
  - title: pikube01 Mem
    position: [[40, 0], [40, 6]]
    rate-ms: 30000
    color: 10
    cur:
        sample: cat /tmp/kube-node | grep pikube01 | awk '{print $4}' | tr -d "Mi"
    max:
        sample: echo 4096
    min:
        sample: echo 0
  - title: pikube02 Mem
    position: [[40, 7], [40, 6]]
    rate-ms: 30000
    color: 13
    cur:
        sample: cat /tmp/kube-node | grep pikube02 | awk '{print $4}' | tr -d "Mi"
    max:
        sample: echo 4096
    min:
        sample: echo 0
  - title: pikube03 Mem
    position: [[40, 13], [40, 6]]
    rate-ms: 30000
    color: 14
    cur:
        sample: cat /tmp/kube-node | grep pikube03 | awk '{print $4}' | tr -d "Mi"
    max:
        sample: echo 4096
    min:
        sample: echo 0
textboxes:
  - title: Status
    position: [[0, 19], [80, 23]]
    rate-ms: 30000
    sample: >-
      kubectl top node > /tmp/kube-node;
      kubectl get all --all-namespaces > /tmp/kube-all;
      echo "Pod:$(cat /tmp/kube-all | grep pod/ | grep 'Running' | wc -l)"
      "Service:$(cat /tmp/kube-all | grep service/ | wc -l)"
      "Daemonset:$(cat /tmp/kube-all | grep daemonset.apps/ | wc -l)"
      "Deployment:$(cat /tmp/kube-all | grep deployment.apps/ | wc -l)"
      "Replicaset:$(cat /tmp/kube-all | grep replicaset.apps/ | wc -l)";
      echo "";
      echo "Service";
      kubectl get svc --no-headers | grep -v ClusterIP | awk '{print $1, $4, $5}' | column -t;

工夫点としては、同じコマンドはいったんファイルに保存し、catで読み込むことで、無駄にKubernetesクラスター側に負荷がかかるのを抑制している点です。

あとは、pikube01ノードに接続したキーボードからsampler -c config.ymlを実行することで、下記がディスプレイに表示されるはずです。(※キャプチャの都合で、別マシンの同サイズウィンドウで撮影しています

Screenshot from 2020-02-09 00-35-26.png

所感

主にサイズ面で不要なパーツもいくつか購入したため、結局5万円くらい使ってしまいました。
この後は、もともと別のラズパイで動いていたサービスをk8sへ乗せ換えたり、監視周りを構築したり、いろいろやりたいことを試していこうと思います。