Raspberry Pi で【リアル☆Kubernetes】を作る!!

2021/09/11に公開

※注意)Qiitaからの移転で、2021年08月25日に投稿した記事です
※旧タイトル:「モバイル『コンテナ・クラスタ』を作る!」

🌞夏!もほぼ終わりかけてますが、8月の残り数日で、夏休みの自由研究がてら
コンテナをもっと勉強したい」あるいは「RaspberryPiを使って何か作ってみたい
そんなニーズはありますかね?

今回は、その両方を実現する『モバイル・コンテナ・クラスター』のご紹介です。

目的 (やりたいこと)

楽に 持ち運べる『コンテナ・クラスタ』 を作る!

背景・動機

業務ではコンテナでサービスを組むことが増えているのに、プライベートでは未だにVMばっか触ってる。
そろそろプライベートでも、がっつり「コンテナ・オーケストレーション」していきたい。

せっかくだから、持ち運べるようにして、みんなに自慢したい☆

そんな思いから、作りました。

作ったもの

さっそくですが、まずは完成品を見てもらいましょう!

Raspberry Pi Kubernetes

コンテナだけに~♪

…って、すみません。

要はこいつに、DockerやらKubernetesやら、なんならAmazon ECSを搭載して、持ち歩こうって話です。

そもそも「コンテナクラスタを持ち歩く必要があるのか?」と問われると「ない」と即答できますが、意味とか必要性とか、その辺は一旦忘れます。

ただ、今回作る『コンテナ船クラスタ』の魅力みたいなモノはあると思っていて、その辺を整理すると、以下のような感じになるかと思います。

  • コンテナ船クラスタの魅力
    • キッズもワクワクするビジュアル!
    • 海でも山でも、どこにでも持って行ける
    • 水に浮かべて、遊ぶこともできる! [1]
    • 水上は涼しいので「水冷式」とか言い張れる(かも)! [1:1]
    • ノードが死んでも、K8s(あるいはAmazon ECS)がサービス復旧してくれる! [1:2]

などなど。

とにかく見た目が楽しい。
個人的インスタ映え

なんなら、楽に持ち運べるので、どこでもSaaSが提供でき、目の前に筐体があるので、いつでもオンサイト対応できます(💀)。
オンサイト対応

仕組み

ではそろそろ、
この最先端のIT技術を搭載した持ち歩けるコンテナクラスタの仕組みの話に移ります。

  • 実現すべきことは、以下の4点
    • 持ち運ぶ (モバイルバッテリー)
    • インターネットに繋げる (セットアップ時:WiFi、運用時:SORACOM)
    • LANを組む (スイッチングハブ + RaspberryPi 1台をNAT化)
    • コンテナ・クラスターを組む (Docker、Kubernetes、Amazon ECS Anywhere)
    • あわよくばラジコン化 (Bluetooth + Mabeee) [1:3]

コンテナ船の概要

このコンテナ船(おもちゃ)には、3台の RaspberryPi (以下ラズパイ) が積載されています。
ざっくりの構成
このラズパイたちに Kubernetes (以下K8s) や Amazon ECS Anywhere でクラスタを組んで、コンテナ( Docker )を管理します。
まずは、K8sで実現したパターンから説明します。

クラスタ用にラズパイ3台で小さなネットワークを組み、内1台がインターネットへのNATゲートウェイになる構成にします。

セットアップ時は上図の通り、家庭のWiFi環境でセットアップする想定ですが、実運用時は「単体で、どこにでも持ち歩ける」ようにする必要があるので、

↑このように、最終的には SORACOM(3GのUSBドングル) 経由で、インターネットへ接続するようにします。

ラズパイ3台の、それぞれのセットアップ内容は、概ね以下の通りです。

  • 1台目:Master (hostname: k8s-master)
    • 有線(eth0):固定IPを振る
    • 無線(wlan0 ⇒ ppp0):インターネットへの接続 (セットアップ時:WiFi ⇒ 運用時:SORACOM)
    • NATゲートウェイ化 (NAPT)
    • Docker、K8sをインストールし、クラスタを構築
  • 2台目:Node1 (hostname: k8s-node1)
    • 有線(eth0):固定IPを振り、NATゲートウェイへ向ける
    • Docker、K8sをインストールし、Masterにぶら下げる
  • 3台目:Node2 (hostname: k8s-node2)
    • 有線(eth0):固定IPを振り、NATゲートウェイへ向ける
    • Docker、K8sをインストールし、Masterにぶら下げる

Amazon ECS Anywhereで実現する場合は、ラズパイ側に載せるOSが異なったり(RaspiOSは不可)、クラスタの構築や管理はラズパイ側ではなくAWS側でやるなど、わりとやる事が異なるので、別の記事にまとめようかと思います。(とはいえ、ラズパイ側の作業はコマンド一発なので、めちゃくちゃ簡単です)
本記事では、K8sでの実現のみに絞って記載します。

作り方

それではさっそく作っていきます。

ハードウェアの準備

基本セット

ハードウェア 数量 参考URL
おもちゃのコンテナ船 1隻 https://www.amazon.co.jp/gp/product/B07P9T8V2J/
RaspberryPi 3B
(or RaspberryPi 4B [2])
3台 https://www.amazon.co.jp/gp/product/B087R57WJX
( https://www.switch-science.com/catalog/5681/ )
SORACOM
スターターキット
1セット https://www.amazon.co.jp/gp/product/B01G1GSYHW
microSD 32GB 3枚 https://www.amazon.co.jp/gp/product/B06XSV23T1/
クラスターケース 1台 https://www.amazon.co.jp/gp/product/B07TJ15YL1/
USBケーブル
(L字micro/35cm)
1本 https://www.amazon.co.jp/gp/product/B089VKDT89/
USBケーブル
(micro/20cm) [2:1]
3本 https://www.amazon.co.jp/gp/product/B07768P7B3/
LANケーブル
(15cm)
3本 https://www.amazon.co.jp/gp/product/B08143HR4H/
小型スイッチングハブ 1台 https://www.amazon.co.jp/gp/product/B00D5Q7V1M/
小型モバイルバッテリー 3個 https://www.amazon.co.jp/gp/product/B07WGKDYKF/

あと、作業用のパソコンやら、ラズパイに繋ぎ込むためのディスプレイ・マウス・キーボード・USB電源(最低3口)は、別途適宜必要です。
ラズパイは、ヘッドレス(ディスプレイ等なし)でも作業できますが、今回は特に(ネットワーク周りでハマる可能性があるので)、ディスプレイ等はあった方が無難です。

おもちゃの船以外の構成は以下の通りです。これらを船に積み込みます。

各パーツの積載手順に関しては、以下をご参照ください。

各パーツの積載手順

コンテナ船に各パーツを載せるイメージは、以下のような感じです。

コンテナ船の中には、3Dプリンターで出力した、各パーツがぐらつかないように固定するための中敷きを設置してあります。[3]

その中敷きの上に、ラズパイクラスタケースを置き⇒バッテリー3つを収め⇒スイッチングハブをいい感じに載せます。

USBケーブルでラズパイとバッテリーを繋ぎ⇒SORACOMドングルを挿して⇒キャビン付き乗船ゲートを取り付けて、ハードウェアのセットアップは完成です。

オプション「Webラジコンにするぞ!」セット

こちらはオプションです。
「水に浮かべてWebアプリ経由で航行させてみたい!」という場合に必要になります。

ハードウェア 数量 参考URL
Mabeee 2個 https://www.amazon.co.jp/dp/B074KBG9Z7
楽しい『水中モーター』 2基 https://www.amazon.co.jp/gp/product/B002DR3H9E

ただし、水に浮かべる場合は、くれぐれも自己責任でお願いします[1:4]
バッテリー等が地味に重いので、ラズパイともども、おそらく沈没(水没)するかと思います。

あと、ラジコン化は結構ハマリポイントが多く、心の余裕と時間の余裕がある時に、覚悟を持って臨まないと、安くない出費を無駄にしてしまう可能性が高いです。[4]

ソフトウェア関連

次に、ソフトウェア周りの説明をします。

■ SDカードの準備 - 3枚

https://www.raspberrypi.org/software/ から Raspberry Pi Imager をダウンロードして、インストールします。

Imagerを起動し「隠しコマンド」である Ctrl + Shift + X 押して、オプション画面を開きます。

hostname を上図の通り3枚それぞれ別に設定してください。SSH認証設定やWiFi設定も適宜変更してください。

OSは、Recommendedなデスクトップ付きの奴で良いかと思います。
※other内のLiteとかでも良いかもですが、動作確認していません

OSとSDカードを選択したら「Write」します。
これを3回繰り返し、SDカードが3枚作成できたら、実際にラズパイに挿して電源を入れます。

■ ラズパイでの作業について

ラズパイでセットアップ(コマンドライン)作業する方法は、ディスプレイとキーボードを直接つなぐ方法と、SSHする方法がありますが、この辺の説明は割愛します。

SSHする際は、上記で設定した hostname を利用して ssh pi@k8s-master.local な感じで接続すればよいかと思います。[5] [6]

以下、具体的な作業内容です。

基本、コマンドラインでの作業です。
大半が初っ端に root になってしまっていますが、気になる方は sudo での実行に読み替えて下さい。
※sudoでの実行の場合、リダイレクトが上手く動くように sudo sh -c 'コマンド' な感じにする必要があるかもしれません

■ ラズパイ1(1台目): Master (hostname: k8s-master)

基本設定

まずは root になる。※以下、 コマンド(#root) となっているコマンドはrootで実行する前提です

コマンド($pi)
sudo -i

swapの無効化。(kubeletが対応してない為)

コマンド(#root)
systemctl stop dphys-swapfile
systemctl disable dphys-swapfile

cgroupsの有効化。

コマンド(#root)
sed -i -e "1 s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1/g" /boot/cmdline.txt

NAT周りの設定。

コマンド(#root)
cat << EOM >> /etc/dhcpcd.conf
interface eth0
static ip_address=192.168.100.1/24
EOM

sysctl -w net.ipv4.ip_forward=1 >> /etc/sysctl.conf

cat << EOM >> /root/ipv4-napt.sh
iptables -t nat -A POSTROUTING -o wlan0 -j MASQUERADE
iptables -A FORWARD -i wlan0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT
EOM
chmod +x /root/ipv4-napt.sh

vi /etc/rc.local
  # exit 0 の前の行に /root/ipv4-napt.sh を追加
実行結果
#  tail -4 /etc/rc.local

/root/ipv4-napt.sh

exit 0
#

ひとしきり更新して、再起動。

コマンド(#root)
apt-get update
apt-get upgrade -y

reboot
Docker と K8s のインストール

再起動後、再度 root になって以下。

まずは、Dockerのインストールと設定。

コマンド(#root)
apt-get install -y apt-transport-https ca-certificates curl software-properties-common gnupg2

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | apt-key add -

echo "deb [arch=armhf] https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" | \
    sudo tee /etc/apt/sources.list.d/docker.list

apt-get update && apt-get install -y \
    containerd.io=1.2.13-2 \
    docker-ce=5:19.03.15~3-0~debian-buster \
    docker-ce-cli=5:19.03.15~3-0~debian-buster

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

mkdir -p /etc/systemd/system/docker.service.d
systemctl daemon-reload
systemctl restart docker
usermod -aG docker pi

続いて、K8s のインストール。

コマンド(#root)
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

apt-get update
apt-get install -y kubelet kubeadm kubectl
apt-mark hold kubelet kubeadm kubectl

念の為、再起動。

コマンド(#root)
reboot
K8s の設定

まずは、マスターで K8s を初期化。

root でも良いですが、これは pi ユーザーでやりました。
※ラズパイ4B以降の場合 --ignore-preflight-errors=Mem オプションは、メモリ2GB以上あるので不要

コマンド($pi)
sudo kubeadm init --pod-network-cidr=10.244.0.0/16 --ignore-preflight-errors=Mem --apiserver-advertise-address=192.168.100.1

うまく行くと、実行結果の最後に kubeadm join 192.168.100.1:6443 --token xxxxxxxxxxxxxxxxx …略 のようなコマンドが記載される。
これはこの後、ノードを追加する際に使うので、コピってメモしておく。

また、以下のような作業をしろとも言われてるので、しておく。

コマンド($pi)
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config
sudo sh -c 'echo "export KUBECONFIG=/etc/kubernetes/admin.conf" >> ~/.bashrc'

ついでに、bashの補完設定もしておく。

コマンド($pi)
source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> ~/.bashrc

CNIプラグインのFlannelをインストール。

コマンド($pi)
sudo KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

ノードの一覧を確認。

実行例
$ kubectl get node
NAME         STATUS   ROLES                  AGE  VERSION
k8s-master   Ready    control-plane,master   1h   v1.22.0

動いてますね。
Ready になるまでにいくらか時間がかかるので、Not Ready の時はしばらくお待ちください。

■ ラズパイ2(2台目): Node1 (hostname: k8s-node1)

基本設定

まずは root になる。

コマンド($pi)
sudo -i

swapの無効化。

コマンド(#root)
systemctl stop dphys-swapfile
systemctl disable dphys-swapfile

cgroupsの有効化。

コマンド(#root)
sed -i -e "1 s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1/g" /boot/cmdline.txt

NAT周りの設定。

コマンド(#root)
cat << EOM >> /etc/dhcpcd.conf
interface eth0
static ip_address=192.168.100.2/24
static routers=192.168.100.1
static domain_name_servers=8.8.8.8
EOM

ひとしきり更新して、再起動。

コマンド(#root)
apt-get update
apt-get upgrade -y

reboot
Docker と K8s のインストール (Masterと同じ)

Masterの時と同じやり方でインストールしてください。

K8s へノードの追加
コマンド(#root)
sudo kubeadm join 192.168.100.1:6443 --token …略 (上記でメモったコマンドを実行)

■ ラズパイ3(3台目): Node2 (hostname: k8s-node2)

基本設定 (ラズパイ2=Node1とほぼ同じ作業)

Node1と同じ作業をします。

ただ、固定IPアドレスは 192.168.100.2 ではなく 192.168.100.3 にする必要があるので、 /etc/dhcpcd.conf の設定だけ、以下のようにします。

コマンド(#root)
cat << EOM >> /etc/dhcpcd.conf
interface eth0
static ip_address=192.168.100.3/24
static routers=192.168.100.1
static domain_name_servers=8.8.8.8
EOM
Docker と K8s のインストール (Masterと同じ)

Masterの時と同じやり方でインストールしてください。

K8s へノードの追加 (Node1と同じ)

Node1の時と同じようにメモっておいた kubeadm join をrootで実行してください。

■ 動作確認

ラズパイ1 のコンソールへ入り、まずはノードの一覧を確認してみる。

実行例
$ kubectl get node
NAME         STATUS   ROLES                  AGE  VERSION
k8s-master   Ready    control-plane,master   1h   v1.22.0
k8s-node1    Ready    <none>                 1h   v1.22.0
k8s-node2    Ready    <none>                 1h   v1.22.0

動いてますね。
<none> になってるROLESに対するラベリング等の作業は割愛します

以上で、RaspberryPi への K8s 環境構築 は 完了 です。

サービスを載せてみる

では実際に、K8sクラスタにサービスを乗っけてみましょう。

以下、ラズパイ1 で作業します。

Nginxを動かしてみる (設定方法)
コマンド($pi)
mkdir -p /home/pi/container-ship/kubernetes
cd /home/pi/container-ship/kubernetes

# 各種YAMLの作成

cat << EOM >> nginx-test-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: nginx-test
EOM

cat << EOM >> nginx-test-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-test-deployment
  labels:
    app: nginx-test
spec:
  replicas: 4
  selector:
    matchLabels:
      app: nginx-test
  template:
    metadata:
      labels:
        app: nginx-test
    spec:
      containers:
      - name: nginx-test
        image: nginx
        ports:
        - containerPort: 80
EOM

cat << EOM >> nginx-test-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: nginx-test-svc
spec:
  selector:
    app: nginx-test
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
EOM

# ネームスペースYAMLのapply
kubectl apply -f nginx-test-namespace.yaml

# 作成したネームスペースに切り替え
kubectl config set-context $(kubectl config current-context) --namespace=nginx-test

# デプロイとサービスのapply
kubectl apply -f nginx-test-deploy.yaml -f nginx-test-service.yaml
実行例
$ # 既存のNameSpace一覧
$ kubectl get ns
NAME              STATUS   AGE
default           Active   21m
kube-node-lease   Active   21m
kube-public       Active   21m
kube-system       Active   21m
$ 
$ # NameSpaceを追加
$ kubectl apply -f nginx-test-namespace.yaml
namespace/nginx-test created
$ 
$ # NameSpace一覧を再確認
$ kubectl get ns
NAME              STATUS   AGE
default           Active   21m
kube-node-lease   Active   21m
kube-public       Active   21m
kube-system       Active   21m
nginx-test        Active   7s   # ←追加されてる
$ 
$ # 現在のコンテキスト
$ kubectl config get-contexts
CURRENT   NAME                          CLUSTER      AUTHINFO           NAMESPACE
*         kubernetes-admin@kubernetes   kubernetes   kubernetes-admin
$ 
$ # NameSpaceを追加した nginx-test に切り替える
$ kubectl config set-context $(kubectl config current-context) --namespace=nginx-test
$ 
$ # 更新後のコンテキストを確認
$ kubectl config get-contexts
CURRENT   NAME                          CLUSTER      AUTHINFO           NAMESPACE
*         kubernetes-admin@kubernetes   kubernetes   kubernetes-admin   nginx-test # ←切り替わった
$ 
$ # 既存のDeployment一覧を確認 (NameSpace:nginx-test)
$ kubectl get deploy
No resources found in nginx-test namespace.
$ 
$ # 既存のService一覧を確認 (NameSpace:nginx-test)
$ kubectl get svc
No resources found in nginx-test namespace.
$ 
$ # 参考までに既存のDeployment一覧を確認 (全NameSpace)
$ kubectl get svc --all-namespaces
NAMESPACE     NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
default       kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP                  25m
kube-system   kube-dns     ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   25m
$
$ # 参考までに既存のService一覧を確認 (全NameSpace)
$ kubectl get deploy --all-namespaces
NAMESPACE     NAME      READY   UP-TO-DATE   AVAILABLE   AGE
kube-system   coredns   2/2     2            2           25m
$ 
$ # 新しいDeploymentとServiceを追加
$ kubectl apply -f nginx-test-deploy.yaml -f nginx-test-service.yaml
deployment.apps/nginx-test-deployment created
service/nginx-test-svc created
$ 
$ # Deployment一覧を再度確認
$ kubectl get deploy
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
nginx-test-deployment   0/4     4            0           17s # ←増えてる。進捗0/4なので作り中。
$ 
$ # Service一覧を再度確認
$ kubectl get svc
NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
nginx-test-svc   ClusterIP   10.107.115.162   <none>        80/TCP    31s # ←増えてる
$ 
$ # Pod一覧も確認 ※作り中(STATUS:ContainerCreating)だけど、増えてる
$ kubectl get po
NAME                                     READY   STATUS              RESTARTS   AGE
nginx-test-deployment-7f8f66d685-8g6lp   0/1     ContainerCreating   0          46s
nginx-test-deployment-7f8f66d685-glsn2   0/1     ContainerCreating   0          46s
nginx-test-deployment-7f8f66d685-pz7pl   0/1     ContainerCreating   0          45s
nginx-test-deployment-7f8f66d685-zhs7b   0/1     ContainerCreating   0          46s
$ 

apply後、しばらく待ってから再度確認してみます。

実行例
$ kubectl get deploy -o wide
NAME                    READY   UP-TO-DATE   AVAILABLE   AGE   CONTAINERS   IMAGES   SELECTOR
nginx-test-deployment   4/4     4            4           25m   nginx-test   nginx    app=nginx-test
$ 
$ kubectl get po -o wide
NAME                                     READY   STATUS    RESTARTS   AGE   IP           NODE        NOMINATED NODE   READINESS GATES
nginx-test-deployment-7f8f66d685-8g6lp   1/1     Running   0          25m   10.244.2.3   k8s-node2   <none>           <none>
nginx-test-deployment-7f8f66d685-glsn2   1/1     Running   0          25m   10.244.1.3   k8s-node1   <none>           <none>
nginx-test-deployment-7f8f66d685-pz7pl   1/1     Running   0          25m   10.244.1.2   k8s-node1   <none>           <none>
nginx-test-deployment-7f8f66d685-zhs7b   1/1     Running   0          25m   10.244.2.2   k8s-node2   <none>           <none>
$ 
$ kubectl get svc -o wide
NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE   SELECTOR
nginx-test-svc   ClusterIP   10.107.115.162   <none>        80/TCP    25m   app=nginx-test
$ 
$ # ↑サービスの ClusterIP に Curl してみると・・・
$ curl 10.107.115.162
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

..略..

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

新しく起動したサービスの ClusterIP に Curl してみます。

表示されました!!(HTMLソースが!!!)

無事、Nginx-TEST サービスにアクセスできました☆

ブラウザからアクセスする

ただ現状、HTMLソースが見えてるだけで、これだと使い物にならないので、
次に、ブラウザからアクセスできるようにしてみます。

クラスタの外からアクセスする方法は色々なやり方があるようですが、とりあえずnginx経由でプロキシする方法で行きます。

ブラウザからアクセスする (設定方法)
コマンド($pi)
cd /home/pi/container-ship/
mkdir /home/pi/container-ship/conf

cat << EOM > /home/pi/container-ship/conf/nginx.conf
server {
    listen  80;
    server_name  _;
    location / {
        proxy_pass http://10.107.115.162; # 上記のClusterIPをまた使います
    }
}
EOM

docker run -d --restart=always -p 80:80 --name proxy -v /home/pi/container-ship/conf/nginx.conf:/etc/nginx/conf.d/default.conf:ro nginx

ブラウザから、ラズパイ1(Master)のWiFi側のIPアドレス(192.168.100.1ではないSSHした時のIPアドレス)にアクセスしてみてください。

表示されました!

K8s上のサービスに、ブラウザからアクセスできました!

自作のコンテナでサービスを提供する

ブラウザからラズパイK8s上のサービスにアクセスできるようになりましたが、Nginxのデフォルトページが見えたところでどうしようもないので、次に自作のコンテナイメージで、サービス提供をしてみようと思います。

「Docker Hub等のアカウントを持っているので、そこからPullしてくる」とかでも良いですが、今回はラズパイ側にプライベートレジストリを立てて、そこでPush/Pullする方法で行きます。

プライベートレジストリを立てて、自作コンテナを使う (設定方法)

引き続き、ラズパイ1 で作業します。

まずは docker-registry をインストール。

コマンド($pi)
sudo apt-get install docker-registry -y

このレジストリへpush/pullする設定が必要ですが、前述のDockerインストールの項目で /etc/docker/daemon.json"insecure-registries": ["192.168.100.1:5000"] を仕込んでおいたので、特に設定要らずで、このまま使えます。

コマンド($pi)
mkdir /home/pi/container-ship/my-server
cd /home/pi/container-ship/my-server

cat << EOM > Dockerfile
FROM balenalib/rpi-raspbian
RUN apt-get update && apt-get install -y python3-dev
WORKDIR /app
COPY . /app
CMD ["python3", "server.py"]
EOM

cat << EOM > server.py
#!/usr/bin/env python3
import http.server
import socketserver
from urllib.parse import urlparse
from urllib.parse import parse_qs
LISTEN_PORT = 80
class ServerHandler(http.server.SimpleHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header('Content-type', 'text/html')
        self.end_headers()
        self.wfile.write(b"<h1>Hello K8s!</h1>")
if __name__ == "__main__":
    HOST, PORT = '', LISTEN_PORT
    with socketserver.TCPServer((HOST, PORT), ServerHandler) as server:
        server.serve_forever()
EOM

docker build -t 192.168.100.1:5000/my-server:0.1 .
docker push 192.168.100.1:5000/my-server:0.1

cd /home/pi/container-ship/kubernetes

# Nginx-TEST の方は削除しておく
kubectl delete -f nginx-test-deploy.yaml -f nginx-test-service.yaml

# 自作サーバーの準備
cat << EOM > my-server-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: my-server
EOM

cat << EOM > my-server-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-server-deployment
  labels:
    app: my-server
spec:
  replicas: 4
  selector:
    matchLabels:
      app: my-server
  template:
    metadata:
      labels:
        app: my-server
    spec:
      containers:
      - name: my-server
        image: 192.168.100.1:5000/my-server:0.1
        ports:
        - containerPort: 80
EOM

# 新しいサービス作るついでに↓ clusterIP を 10.96.100.1 に固定してる
cat << EOM > my-server-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: my-server-svc
spec:
  selector:
    app: my-server
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
  clusterIP: 10.96.100.1
EOM

# ネームスペースYAMLのapply
kubectl apply -f my-server-namespace.yaml

# 作成したネームスペースに切り替え
kubectl config set-context $(kubectl config current-context) --namespace=my-server

# デプロイとサービスのapply
kubectl apply -f my-server-deploy.yaml -f my-server-service.yaml

# Nginxの転送先も自作コンテナ(サービス)のClusuterIPに変更
cat << EOM > /home/pi/container-ship/conf/nginx.conf
server {
    listen  80;
    server_name  _;
    location / {
        proxy_pass http://10.96.100.1;
    }
}
EOM
docker restart proxy

ブラウザから再度アクセス。

自作っぽいのが表示されました!!

無事、自作コンテナをK8s上に載せて、表示させることが出来ました。

物理デバイスを操作してみる

で、ここまではクラウド上でも出来る事なので、せっかく船の上なので「IoT」っぽく、物理デバイスを動かしてみましょう。

動かす物理デバイスは、船なので『水中モーター』 で決まりでしょう。

どうですか、この見た目。ワクワクしませんか☆

で、この水中モーターを「どうやって操作するのか」ですが、今回はBluetoothで単三電池を操ることが出来る「MaBeeeを使って制御します。

MaBeeeを使えば、水中モーター(左右2基)の
ON/OFFと強弱を制御
して、前進・旋回が可能になります。

水中モーターを制御する準備

MaBeeeを制御するスクリプトは、Sunaga-Laboさんのスクリプトを利用させてもらいました。感謝。

とりあえず、自作コンテナを止めてしまいます。
その後、例によって、rootで作業していきます。

コマンド($pi)
cd /home/pi/container-ship/kubernetes
kubectl delete -f my-server-deploy.yaml -f my-server-service.yaml
sudo -i
コマンド(#root)
cd
git clone https://github.com/sunaga-lab/mabeee-python
cd mabeee-python
apt-get install bluez -y
pip3 install setuptools bluepy datetime

MaBeee制御の下準備は以上で、以下、実際にモーターを制御してみます。
そのまま root で python3 mabeee_ctrl.py scan を実行すると、上手く検知できればMabeeeのMACアドレスをゲットできます。

コマンド(#root)
python3 mabeee_ctrl.py scan
実行例
# python3 mabeee_ctrl.py scan
- Device MaBeeeA12001 addr ab:12:cd:34:ef:56 # ← 水中モーター1
- Device MaBeeeA12002 addr 9a:8b:7c:65:de:43 # ← 水中モーター2
#

今度は、このMACアドレスを引数にして、1台ずつ制御してみます。

コマンド(#root)
python3 mabeee_ctrl.py <MACアドレス>
実行例
# python3 mabeee_ctrl.py ab:12:cd:34:ef:56
PWM 0-100: 100 # ← 片方のモーターが、全開で回転する
PWM 0-100: 60  # ← ちょっと弱くなる
PWM 0-100: 0   # ← 完全に止まる
PWM 0-100: end # ← 数字以外を何でも良いので入力すると専用プロンプトを抜けられる
End.
#

モーターは回転したでしょうか。
モーター2台とも動くことを確認しておいてください。
上記で確認したMACアドレス2つは、後ほど利用するのでメモしておきます。

水中モーターをコントローラーを作る

ペライチのwebアプリを作ります。

特に意味はないのですが、一般ユーザーに戻って作業します。

コマンド(#root)
exit

※注)下記、一部書き換えが必要です (ココ☆の個所)

コマンド($pi)
mkdir /home/pi/container-ship/mabeee-server
cd /home/pi/container-ship/mabeee-server

# ↓メモしておいたMACアドレスでそれぞれ書き換えてください (ココ☆)
cat << EOM > .env
MABEEE_MACADDR_1=ab:12:cd:34:ef:56
MABEEE_MACADDR_2=9a:8b:7c:65:de:43
EOM

cat << EOM > Dockerfile
FROM balenalib/rpi-raspbian
RUN apt-get update && apt-get install -y \
    python3-dev python3-pip \
    libglib2.0-dev bluez
RUN pip3 install setuptools bluepy datetime python-dotenv
WORKDIR /app
COPY . /app
CMD ["python3","server.py"]
EOM

# server は、ちょいと長めなのでWebからゲット
wget -o server.py https://gist.githubusercontent.com/ie4/8d58bbef2f52f12dcf97b47509fac07e/raw/

docker build -t 192.168.100.1:5000/mabeee-server:0.1 .
docker push 192.168.100.1:5000/mabeee-server:0.1

cd /home/pi/container-ship/kubernetes

cat << EOM > container-ship-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: container-ship
EOM

cat << EOM > mabeee-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: mabeee-svc
spec:
  selector:
    app: mabeee
  ports:
  - protocol: TCP
    port: 80
    targetPort: 8080
  type: ClusterIP
  clusterIP: 10.96.100.1
EOM

cat << EOM > mabeee-deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mabeee-deployment
  labels:
    app: mabeee
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mabeee
  template:
    metadata:
      labels:
        app: mabeee
    spec:
      hostNetwork: true
      containers:
      - name: mabeee
        image: 192.168.100.1:5000/mabeee-server:0.1
        ports:
        - containerPort: 8080
EOM

# ネームスペースYAMLのapply
kubectl apply -f container-ship-namespace.yaml

# 作成したネームスペースに切り替え
kubectl config set-context $(kubectl config current-context) --namespace=container-ship

ホストのBluetoothを使うために hostNetwork: true を指定しています。
また、応答速度上げるため(WebアクセスのたびにBluetoothで再接続しなくても済むよう)に、MabeeeとBluetoothコネクション張りっぱなしにしてます。ただ、そのせいでpodは1つしか同時に立ち上げられない(2つめ以降はBluetooth接続が1つ目に占有されているので使えない)という、本格的にK8sである必要性を疑う構成になってきていますが、その辺はどうぞ、大目に見てやってください。

ここで一度、水中モーターの電源スイッチをOFF/ONします。
※既存のコネクションを確実に切断するため

コマンド($pi)
# デプロイとサービスのapply
kubectl apply -f mabeee-deploy.yaml -f mabeee-service.yaml

kubectl get--watch など付けて、Podが立ち上がるのを待ちます。

実行例
$ kubectl get po -o wide --watch
NAME                                 READY   STATUS              RESTARTS   AGE   IP              NODE        NOMINATED NODE   READINESS GATES
mabeee-deployment-746b8bd5cf-hj9x9   0/1     ContainerCreating   0          60s   192.168.100.3   k8s-node2   <none>           <none>
mabeee-deployment-746b8bd5cf-hj9x9   1/1     Running             0          65s   192.168.100.3   k8s-node2   <none>           <none>

それでは先ほどのブラウザに戻ります。
うまく立ち上がってくれていれば、以下のような表示になります。

Left を押すとモーター1が、 Right を押すとモーター2が、Forward を押すと両方のモーターが回転し、Stop を押すと両方止まります。

理屈の上では、これでコンテナ船が前進・旋回できるはずです。

502エラー等で、上手く動かない場合などは

水中モーターのコントロール画面で「502 Bad Gateway」が出る場合は、たいていBluetooth接続で失敗しています。

ラズパイ1で docker logs --tail 100 -f proxykubectl logs --tail 100 -f svc/mabeee-svc などでログを追ってみてください。

あるいは、水中モーターの電源をOFF/ON(Bluetooth接続を強制切断)した上で、ラズパイの近くに水中モーターを置き、以下のようにPodを立ち上げ直してみてください。(削除することで自動復旧させて、再起動がわりにしてます…)

実行例
$ kubectl get po
NAME                                 READY   STATUS    RESTARTS   AGE
mabeee-deployment-123a4bc5de-f6789   1/1     Running   0          26m
$ 
$ kubectl delete po mabeee-deployment-123a4bc5de-f6789
pod "mabeee-deployment-123a4bc5de-f6789" deleted
$ 

SORACOM Airで、持ち歩けるようにする

モーターを駆動させることには成功しましたが、このままだと外に持ち出して、みんなに自慢する事ができません。
特にWi-Fiのないところへ持っていくと、「ただのガラクタ」に成り下がってしまいます。

そこで、SORACOM Airを使って、どこでもインターネットに接続可能な「最先端のおもちゃ」にレベルアップさせます。

SORACOM Airでインターネット接続

事前に https://console.soracom.io/ で、アカウント登録及びSIMの登録を行っておきます。

ラズパイ側のセットアップはコマンド1発でOKで、びっくりするほど簡単です。
ラズパイ1に、SORACOM-SIMの入ったUSBドングルが刺さっていることを確認して、pi ユーザーで以下を実行します。

コマンド($pi)
curl https://soracom-files.s3.amazonaws.com/setup_air.sh | sudo bash

ラズパイ側の設定は、以上。

上記コマンドのセットアップが終わったら
https://console.soracom.io/ へアクセスし、該当のSIMが 使用中(オンライン) になっている事を確認します。

これで無事、ラズパイがインターネットへ接続されました。

SORACOM経由にすると、当然費用が発生します SIM 1枚2枚での運用なら、月額数千円にいくような事態にはなりませんでしたが、ミドルウェアのインストールや動画等の通信などには、十分にご注意ください

SORACOM Napterを使って、サービスを公開

インターネットには繋がりましたが、このままではサービスを公開できません。

ラズパイ側にせっかくK8s用のエンドポイントを用意しても、そのエンドポイントにアクセスする術がありません。

IoTと言えば一般的に、クラウドへの各種リソース要求やセンサーデータの収集など、「ラズパイから外への通信」 が主な使われ方のような気がしますが、今回はその逆の 「外からラズパイへの通信」 が必要になります。

一般的なモバイルWi-Fiであれば、諦めざるをえないユースケースかもしれませんが、SORACOM経由の場合、「SORACOM Napter」を使えば実現できます。

今回のセットアップでも、ラズパイにNAPT設定を入れて「 ラズパイ3台でインターネットへの通信を共有 (動的なNAPT)」しましたが、SORACOM側で「 インターネットからの特定IPアドレス・ポートへの通信を、ラズパイ1の特定ポートへ転送 (静的なNAPT)」する機能が「SORACOM NAPTer」です。
※間違ってたら、ご指摘ください…

前置きが長くなりましたが、では、実際に使い方を説明します。

管理コンソールのSIM管理画面で、対象のSIMを選択した状態で右クリックし「オンデマンドリモートアクセス」をクリック。

ラズパイ1のNginx-Proxyは80番で待ち受けているので、「デバイス側ポート」を80に変更し、「OK」ボタンを押下。
※「アクセス可能時間」や「アクセス元IPアドレスレンジ」も適宜変更(IPアドレスレンジの方は、空欄のままだと管理画面へアクセスしてる環境のIPが設定される※それ以外の環境からはアクセスできない)

発行されたHTTPの項目のURLへアクセスすると、ラズパイ1のNginx-Proxyにアクセスでき、結果、先ほど作成した水中モーターコントロール画面へアクセスできます。

ただ、オンデマンドリモートアクセスの名が指し示す通り、現状MAX8時間までしか設定できないので、常時接続用の穴をあける用途には使えなさそうです。

そもそも「持ち歩き」にあたっては、バッテリーが何時間持つか問題もあって、常時接続にはもともと向かないという事情があるので、オンデマンド接続で問題ない気もします。

移動先の電源を使ってサービスを提供する、というケースでは常時接続したいかもしれないので、そのための方法も後述します。

ラズパイ側のNAPT設定の変更

次に、Wi-Fi用に設定していたNAPT(IPマスカレード)設定をSORACOM経由に変更します。

80番ポート同様に、22 番ポートに対して、同じ手順でオンデマンドリモートアクセス設定を追加します。
そして、発行された「ホスト名(orIPアドレス)」と「ポート番号」でラズパイ1にSSH接続し、ラズパイ側のNAPTの設定を eth0 -> wlan0 から eth0 -> ppp0 へ変更しましょう。

変更するためのコマンドは、root になってから以下を実行すればOKです。

コマンド(#root)
cat << EOM > /root/ipv4-napt.sh
iptables -t nat -A POSTROUTING -o ppp0 -j MASQUERADE
iptables -A FORWARD -i ppp0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -i eth0 -o ppp0 -j ACCEPT
EOM

※この /root/ipv4-napt.sh/etc/rc.local 内で実行する設定がラズパイ1には入れてあります

これで再起動すれば、k8s-node1k8s-node2 もSORACOM経由でインターネットへ出て行けるようになります。

常時接続用のトンネルを掘る

外出先で「Wi-Fiはないが電源はあるので、いざという時のために、24時間常に外側からアクセスできるようにしたい」という要望があった場合は、インターネット側にSSH接続可能なサーバーを用意して「SSHのポートフォワード機能」を使って、外向けにポートを常時解放することができます。
いわゆる ngrok 的な構成です。※ngrokほど手軽じゃないですが…

ラズパイ側から、インターネット上のサーバーに下記のようSSHする事で、ポートフォワードが可能です。

ssh -fN <ユーザー名>@<サーバーのホスト名orIPアドレス> -R <サーバー側のポート>:0.0.0.0:<ラズパイ側のポート>

航行させてみる

最後に、水没に十分に気をつけながら、いざ航行させてみます。 [1:5]

芝生からプールへの、ブルーグリーンならぬ、🌱💧グリーンブルー・デプロイです☆

Kubernetes(操舵手)が、まさに船を動かした瞬間でした。

余談

その他の記念撮影

ちなみに、黄色いコンテナの中身は・・・?

コンテナ船なので、黄色いコンテナの中にラズパイを入れたかったのですが、サイズ的にちょうどいいラズパイZero系はCPU(ARMv6)事由でK8s(1.6以降)が使えないらしく、ラズパイ3BなどのB系はギリギリ(斜めに差し込んでも)コンテナに入らず、コンテナ内にラズパイを入れるのは、断念しました。

で、代わりに黄色いコンテナに入れたものは・・・


最近のお気に入りなミニカーで、広島ならではのチョイスにしてみました。
※実際は1コンテナ1台しか入らなかったので、2台までしか積めなかった

Kubernetes なのに、船長はあいつ

乗船ゲートの上部にキャビンが付いていて、そこに船長が乗っているのですが、よくみると・・・

こいつは、もしかして

いや、すみません。だいぶ違いますね・・・

※写真の青い奴は、お祭りとかで釣って遊ぶやつです

メンターとナイトプール

もろもろ実験が終わったので、ビニールプールの底にLEDテープ敷いてナイトプール化したり、ラズパイの代わりに我がメンター(ラバーダック)に乗船してもらって、記念撮影を楽しむ一夜。

参考URL

すみません、まとめ切れてませんが、めちゃくちゃ沢山の記事を参考にさせて頂きました。

最後に

調べながら手を動かしながらで書いたので、ツッコミどころが多い事かと思います。
とはいえ、出来る限りツッコミどころのないように心がけたつもりです。どうか、ご指摘・お叱りいただく際は優しめのお言葉で、ご指南頂けますと幸いです🙇

脚注
  1. 水に浮かべる際は、以下の点にご注意ください。
    - 水に浮かべる際は、自己責任でお願いします
    - ぶっちゃけ私は、早々に沈没(水没)させました…
    - すぐに水から上げて電源を抜き、事なきを得ましたが、防水必須です
    - バッテリーとラズパイを3台ずつ積むと、さすがに浮力が足りないようです
    - 水に浮かべると、船尾の際が水面と数ミリしか離れていないので、少しでも傾くと一気に水が流れ込んできて沈みます
    - 基盤むき出しの電子機器なので、水没すれば壊れるし、LiPoバッテリーのショートは非常に危険です(バッテリーだけは一時的にテープ等で完全に防水しておくのが良いです)
    - 水没しないように万全に備えるか、見た目を愛でて楽しみましょう
     ⇒ 記事に戻る: ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  2. ぶっちゃけRaspberryPi 4B 2GBでやった方が良いです
    - ラズパイはタイミングによっては品切れになっている可能性もありますが、基本的に3B以降であれば、どれでも実現できるはずです
    - ラズパイ4以降にする場合は、USBケーブル(micro/20cm)×3本 を Type-Cのケーブル に読み替えて調達してください
    - また、ラズパイ4以降でディスプレイを繋いで作業する場合は、HDMIケーブルのラズパイ側端子をMicro HDMIにする必要があります
    - 「3Bを既に持ってるから」以外の理由で、3Bでやる意味はほぼ無いです。新たに買うなら4Bが良いです
    - 一時期、Amazonで ラズパイ3B が3700円前後で普通に売られてましたが、今は値段上がってしまって、安くても4000円強するので、1000円プラスして4Bにした方が絶対良いです
    - 結論:3Bを3台あまらせてる方以外(新しく購入する方)は、4Bを購入して、4Bでやった方がいいです
     ⇒ 記事に戻る: ↩︎ ↩︎

  3. コンテナ船の中敷きについて
    - 加工精度は必要としないので、3Dプリントでなくとも、紙とテープで作ってしまっても良いかと思います
    - 3Dプリンタがある方は、ThingiverseにSTLファイル等をアップしましたので、良かったらプリントしてみてください。 https://www.thingiverse.com/thing:4940828
    - 最近は3Dプリントもめちゃくちゃ安くなってきたので、買ってしまう手もあるかと思います。ちなみに、自分は1年前に1万5千円弱で購入したKingroonのコイツでプリントアウトしましたが、かなり満足してます(台がマグネットシートでめくれるので、アイテムを取り外しやすいのもGOOD)。
     ⇒ 記事に戻る: ↩︎

  4. ラジコン化のハマりポイントについて
    - 防水目的でビニール袋に入れるとBluetoothが切れやすくなる
    - そもそもモーターを水に沈めるとBluetooth接続が不安定になる
    - Bluetoothが不安定だとアプリ側で500エラー多発する
    - 調査がなかなかに難しい
    - とにかくハマリポイント多め
     ⇒ 記事に戻る: ↩︎

  5. 接続元に mDNS に仕組みが必要になりますが、今どきの開発環境なら大抵備わっているかと思います
    記事に戻る: ↩︎

  6. 今回の記事はネットワーク周りの設定が多く、ネットワーク周りでハマる(sshできなくなる)可能性が高いので、念のためディスプレイ等を繋いで作業できるようにしておいた方が安全です
     ⇒ 記事に戻る: ↩︎

Discussion