Raspberry Pi 2 と Jetson nano でHybridおうちKubernetes環境を作ったときの話

Progateの前田です。

エンジニアマネージャーとしていろいろやってる傍、個人ではAWS Community Builder / AWS Startup Community Code Member として活動しています。

本記事はProgate Advent Calendarの1日目です!

今年の9月にEKS Anywhereのリリースがあって、ついに自宅にKubernetes環境を作る機運かと思い、おうちKubernetesに着手しました。 しかし残念なことに、EKS Anywhereはx86/amd64にしか対応していないので、ただのKubernetesクラスタを構築することになりました。

という余談は置いといて、いざおうちKubernetesをやろうとRaspberry Piを調べていたところ、Raspberry Pi 4が異常なまでの品薄状態になっており、出鼻を挫かれる事態に・・

僕の中で昂ったおうちKubernetes熱を消化しきれず、とりあえず家にあったRaspberry Pi 2とJetson nano 2GBを使ってなんとかKubernetesクラスタを組めないか試してみました。

できたもの

最終的に出来上がったのがこれです。

f:id:progate_tech:20211129103001p:plain

順番に解説していきます。

作り方

材料購入

サーバー類の材料で購入したのは主に下記です(元々持っていたものも含む)。

マウントラックをDIYするために、下記も購入しました。

だいたいトータルで4万くらいかな?と思います。

筐体

正直ここが一番時間かかりました。 規格の違うRaspberry Pi と Jetson nanoをいい感じに積み上げられるラックケースのようなものが見当たらず、100均グッズなど活用して自作することにしました。

具体的には、アクリルのフォトフレームの穴に長いボルトを通して、ワッシャーとナットで高さを固定しました。

f:id:progate_tech:20211129103347p:plain f:id:progate_tech:20211129103405p:plain

あと、基本的にsshで入って操作するのでディスプレイとキーボードの必要性はそこまで高くないのですが、なんとなくテンションが上がるのでつけてみました。 こちらの固定は結束バンド一点頼みです。

f:id:kzk_maeda:20211130095752p:plain

これだけでもうほぼ完成したくらいの気持ちですが、ここまで作ったので以降でサーバーとKubernetesの設定を入れていきます。

OSインストール

ここはRaspberry Pi、Jetson nanoそれぞれの公式ドキュメントなど参照し、OSを入れていきます。

Raspberry PiにはUbuntu20.04Raspberry Pi Imager経由で、Jetson nanoはせっかくのGPUデバイスで他になにか使い道がありそうなのでDeveloper KitからOSをインストールしました。

Network設定

次に、各マシンをネットワークに接続できる状態(インターネット接続と、マシン間の相互接続)にします。

構成としては概ねこんな感じです。

f:id:kzk_maeda:20211130101826p:plain

最初にトラベルルーターを起動して、MacをトラベルルーターのNWに参加します。

その後、ルーターの管理画面(今回の機種は192.168.13.1)にアクセスすると、ルーターの設定ができるので、Wireless WANモードを選択し、自宅のNWにトラベルルーターを参加させます。

これにより、トラベルルーターを起点としたL3のローカルネットワーク帯域が構成され、トラベルルーターによりルーティングされて上記のローカルネットワークがインターネットアクセスできる状態になります。

この状態でトラベルルーターとスイッチングハブをLANケーブルで接続し、スイッチングハブからRaspberry PiとJetson nanoにLANケーブルを差すことで、Mac、Raspberry Pi、Jetson nanoがトラベルルーターのDHCPにより払い出されるローカルIPアドレスで相互にアクセスできるようになります。(今回の機種の場合、192.168.13.0/26の帯域から自動でIPが付与され、このCIDRは変更できないようでした)

今回採用したトラベルルーターには固定IP振り機能がないのでDHCP必須となってしまいますが、基本的に再起動などしない限りは一度割り振られたIPアドレスはリリースされないはずなので、コンソールログインして調べたIPアドレスにDNSで名前を振ります。

僕の自宅ルーターにはDNS機能があるので、自宅ルーターをDNSサーバーとして認識させるようにRaspberry PiとJetson nanoのresolved.confを設定し、ルーターに各IPアドレスとローカルホスト名をマッピングさせます。

# Raspberry Pi / Jetson nano の両方で設定
$ sudo apt-get install dnsutils
$ sudo vim /etc/systemd/resolved.conf
$ cat /etc/systemd/resolved.conf | grep DNS
DNS=192.168.1.1

$ sudo vim /etc/resolv.conf
nameserver 192.168.1.1

$ systemctl restart systemd-resolved
$ systemd-resolve --status

f:id:progate_tech:20211129103756p:plain

サーバー設定

次に各サーバーマシンに最低限の設定を入れていきます。

hostnameの設定、OSユーザーの作成

どのマシンに入っているかわかるよう、hostnameを設定しておきます。

# Raspberry Piで設定
$ hostnamectl set-hostname rasp01

# Jetson nanoで設定
$ hostnamectl set-hostname jetson01

Jetson nanoの方は、ubuntuユーザーが作成されていないので、作成してsudo権限を渡します。

$ adduser ubuntu
$ gpasswd -a ubuntu sudo

ssh 接続の許可 / 公開鍵認証の設定

sshで相互アクセスできるように必要なpackageをインストールします。

$ sudo apt update
$ sudo apt install openssh-server
$ sudo systemctl status ssh
$ sudo ufw status
Status: inactive # ufwは無効なので一旦気にしない

お互いに鍵認証でsshできるよう、鍵の交換を実施します。

# Raspberry Piで設定
$ ssh ubuntu@jetson01
$ ssh-copy-id ubuntu@jetson01

# Jetson nanoで設定
$ ssh ubuntu@rasp01
$ ssh-copy-id ubuntu@rasp01

(Mac側からも設定しておくと便利です。)

他にもいろいろ設定したい気持ちはありますが、早くKubernetesのクラスタを構築したいので、一旦最低限のこのくらいにして実際のKubernetes環境構築に移ります。

Kubernetes設定

性能の違う二つのマシンでクラスタを組むので、どっちをmaster / nodeにするか迷ったのですが、Jetson nanoの方が後々追加で入手しやすそうなので、増築するときのことを考えて、Raspberry Pi を master、Jetson nanoをnodeとして構築することにしました。

その際、Raspberry Pi 2 はKubernetesのシステム要件(正確にはkubeadmのシステム要件)を満たしていないので、いくつか気をつけて設定するポイントがありますが、そこは設定時に後述するとして、先に必要なdockerやKubernetes周りのツールをインストールしていきます。

dockerのinstall (Raspberry Piのみ)

Developer KitでOSを入れたJetson nanoには既にdockerが入っているので、Raspberry Pi側にdockerをインストールしていきます。

sudo su -
# 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

# dockerコマンドを実行できるように権限付与
$ sudo usermod pi -aG docker

# このあと一度ターミナルから抜けて再ログインし、コマンド確認
$ docker -v

Kubernetes管理ツール(kubectl / kubeadm)のinstall

Raspberry Pi / Jetson nanoともに、下記サイトを参照して kubectl / kubeadm をインストールします

kubernetes.io

$ sudo apt-get update && sudo apt-get 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-get update
$ sudo apt-get install -y kubelet kubeadm kubectl
$ sudo apt-mark hold kubelet kubeadm kubectl

# Versionの固定
$ apt-mark hold kubelet kubeadm kubectl

Kubernetes Cluster のセットアップ

以降しばらくmasterとして振る舞わせるRaspberry Pi側で作業します。

最初にシステム要件としてswapを無効化しておく必要があるので対応します。

$ sudo swapoff -a
$ sudo sed -i '/ swap / s/^/#/' /etc/fstab

次に kubeadm コマンドでクラスターの初期化を実施します。 ドキュメント通りに実施するには、下記のコマンドを実行することになります。

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16

しかし、kubeadmでクラスタ設定するには、システム要件として2GB以上のメモリが必要なのですが、今回選定したRaspberry Pi 2は1GBしかメモリを積んでいないので、そのまま実行するとメモリエラーで初期化が停止してしまいます。

それを回避するために、次のオプションを追加して再実行します( ref

$ sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --ignore-preflight-errors=Mem

そうすると、実行ログがたくさん流れた後、次に実行するコマンドを指示されるのでそのまま実行します。

この際、Clusterに参加するためのtoken込みの kubeadm コマンドも表示されているので、控えておいてください。

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

次に、flannelというオーバーレイネットワーク環境を構築します。

通常、各nodeで起動されるdockerコンテナに付与されるIPアドレスはホストの外からアクセスできないInternal IPとなっています。 今回、nodeを跨いでコンテナ同士を通信させるためにオーバーレイネットワークの構成が必要であり、今回はおうちKubernetesでよく利用されるflannelを利用していきます。

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

ここまで、Raspberry Pi側でmasterノードの構築を実施していきましたが、次はJetson nano側からクラスタへの参加を実行します。

kubeadm init コマンドを実行した際に表示されていたクラスター参加コマンドをJetson nano側で実行します。

$ sudo kubeadm join 192.168.13.3:6443 --token l4e0y5.03f7om17hca7utww \
    --discovery-token-ca-cert-hash sha256:76383e0a28ad04b33ff03563059cf495edc287fdf78c4cba527a47ff63e59b4b

また、Jetson nano側で kubectl コマンドが実行できるようにkubectl configをコピーしてきます。

# Raspberry Pi側で実行
$ kubectl config view --raw

# Jetson nano側で実行
$ vi ~/.kube/config # 上のコマンド結果を貼り付け

最後にroleを付与して、 kubectl コマンドでクラスターにnodeが参加していることを確認します。

$ kubectl label node jetson01 node-role.kubernetes.io/worker=

$ kubectl get nodes
NAME       STATUS   ROLES                  AGE    VERSION
jetson01   Ready    worker                 5m8s   v1.22.2
rasp01     Ready    control-plane,master   12m    v1.22.2

これで、Raspberry PiとJetson nanoに跨ったKubernetesクラスターが構成されました!

ちょっとハマったこと

せっかくできたKubernetes環境でいろいろ遊んでいた時に、Pod間がCoreDNSで名前解決できない問題に直面しました。

最善の解決策なのかわかりませんが、下記で回避できたのでついでに書き留めておきます。

まずはCoreDNSとして動いているPodのIPアドレスを確認します。

$ kubectl get ep kube-dns --namespace=kube-system -o yaml
- addresses:
  - ip: 10.244.0.4
    nodeName: rasp01
    targetRef:
      kind: Pod
      name: coredns-78fcd69978-bwpmg
      namespace: kube-system
      resourceVersion: "12518"
      uid: 69b1e20f-5201-4d67-9069-be8620056784
  - ip: 10.244.0.5
    nodeName: rasp01
    targetRef:
      kind: Pod
      name: coredns-78fcd69978-56xqc
      namespace: kube-system
      resourceVersion: "12517"
      uid: ec419d54-b120-4d86-9b3f-3e451469e0b1

ここで二つのPodがCoreDNSとして起動していることがわかるので、Deployment定義のマニフェストファイルにDNS設定を記述します。( 参考

apiVersion: apps/v1
kind: Deployment
metadata:
  creationTimestamp: null
  labels:
    app: sample-app
  name: sample-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: sample-app
  strategy: {}
  template:
    metadata:
      creationTimestamp: null
      labels:
        app: sample-app
    spec:
      containers:
      - image: kazukimaeda/k8s-sampleapp:latest
        name: fastapi-sample
        envFrom:
          - configMapRef:
              name: sample-app
          - secretRef:
              name: sample-app
        resources: {}
      dnsPolicy: "None" #以下DNS設定
      dnsConfig:
        nameservers:
          - 10.244.0.4
          - 10.244.0.5
        searches:
          - default.svc.cluster.local
          - svc.cluster.local
          - cluster.local
        options:
          - name: ndots
            value: "5"
status: {}

最後に

Raspberry Piなどを用いておうちKubernetes環境を構築することは、もう何番煎じなのかわからないくらいたくさんの知見が転がっていますが、今回は(予算と在庫の制約で)手元にあったRaspberry Pi 2とJetson nanoという異なるマシンを用いてKubernetesクラスタを構築した時のメモを記事にしてみました。

改めてここまで構築して、EKSのようなクラウド上のクラスター管理サービスが如何にKubernetesクラスタの構築・運用の面倒な部分をうまく巻き取ってくれているかということを強く感じました。

こういう抽象化されたサービスのありがたみを感じるためにも、たまには非効率な学習を行うのも悪くないな、という無理のある訓示っぽい言葉で締めたいと思います。

明日はProgateデータエンジニアの穴澤さんによる、おうちKubernetes環境の活用事例についてです!まさかの2日連続おうちKubernetes関連ネタ被りですが、是非明日もご覧ください!