Raspberry Pi で【リアル☆Kubernetes】を作る!!
※注意)Qiitaからの移転で、2021年08月25日に投稿した記事です
※旧タイトル:「モバイル『コンテナ・クラスタ』を作る!」
🌞夏!もほぼ終わりかけてますが、8月の残り数日で、夏休みの自由研究がてら
「コンテナをもっと勉強したい」あるいは「RaspberryPiを使って何か作ってみたい」
そんなニーズはありますかね?
今回は、その両方を実現する『モバイル・コンテナ・クラスター』のご紹介です。
目的 (やりたいこと)
楽に 持ち運べる『コンテナ・クラスタ』 を作る!
背景・動機
業務ではコンテナでサービスを組むことが増えているのに、プライベートでは未だにVMばっか触ってる。
そろそろプライベートでも、がっつり「コンテナ・オーケストレーション」していきたい。
せっかくだから、持ち運べるようにして、みんなに自慢したい☆
そんな思いから、作りました。
作ったもの
さっそくですが、まずは完成品を見てもらいましょう!
コンテナだけに~♪
…って、すみません。
要はこいつに、DockerやらKubernetesやら、なんならAmazon ECSを搭載して、持ち歩こうって話です。
そもそも「コンテナクラスタを持ち歩く必要があるのか?」と問われると「ない」と即答できますが、意味とか必要性とか、その辺は一旦忘れます。
ただ、今回作る『コンテナ船クラスタ』の魅力みたいなモノはあると思っていて、その辺を整理すると、以下のような感じになるかと思います。
- コンテナ船クラスタの魅力
などなど。
とにかく見た目が楽しい。
なんなら、楽に持ち運べるので、どこでも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で実行する前提です
sudo -i
swapの無効化。(kubeletが対応してない為)
systemctl stop dphys-swapfile
systemctl disable dphys-swapfile
cgroupsの有効化。
sed -i -e "1 s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1/g" /boot/cmdline.txt
NAT周りの設定。
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
#
ひとしきり更新して、再起動。
apt-get update
apt-get upgrade -y
reboot
Docker と K8s のインストール
再起動後、再度 root になって以下。
まずは、Dockerのインストールと設定。
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 のインストール。
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
念の為、再起動。
reboot
K8s の設定
まずは、マスターで K8s を初期化。
root でも良いですが、これは pi ユーザーでやりました。
※ラズパイ4B以降の場合 --ignore-preflight-errors=Mem
オプションは、メモリ2GB以上あるので不要
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 …略
のようなコマンドが記載される。
これはこの後、ノードを追加する際に使うので、コピってメモしておく。
また、以下のような作業をしろとも言われてるので、しておく。
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の補完設定もしておく。
source <(kubectl completion bash)
echo "source <(kubectl completion bash)" >> ~/.bashrc
CNIプラグインのFlannelをインストール。
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 になる。
sudo -i
swapの無効化。
systemctl stop dphys-swapfile
systemctl disable dphys-swapfile
cgroupsの有効化。
sed -i -e "1 s/$/ cgroup_enable=cpuset cgroup_enable=memory cgroup_memory=1/g" /boot/cmdline.txt
NAT周りの設定。
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
ひとしきり更新して、再起動。
apt-get update
apt-get upgrade -y
reboot
Docker と K8s のインストール (Masterと同じ)
Masterの時と同じやり方でインストールしてください。
K8s へノードの追加
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
の設定だけ、以下のようにします。
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を動かしてみる (設定方法)
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経由でプロキシする方法で行きます。
ブラウザからアクセスする (設定方法)
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 をインストール。
sudo apt-get install docker-registry -y
このレジストリへpush/pullする設定が必要ですが、前述のDockerインストールの項目で /etc/docker/daemon.json
に "insecure-registries": ["192.168.100.1:5000"]
を仕込んでおいたので、特に設定要らずで、このまま使えます。
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で作業していきます。
cd /home/pi/container-ship/kubernetes
kubectl delete -f my-server-deploy.yaml -f my-server-service.yaml
sudo -i
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アドレスをゲットできます。
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台ずつ制御してみます。
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アプリを作ります。
特に意味はないのですが、一般ユーザーに戻って作業します。
exit
※注)下記、一部書き換えが必要です (ココ☆の個所)
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します。
※既存のコネクションを確実に切断するため
# デプロイとサービスの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 proxy
や kubectl 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 ユーザーで以下を実行します。
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です。
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-node1
と k8s-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
すみません、まとめ切れてませんが、めちゃくちゃ沢山の記事を参考にさせて頂きました。
最後に
調べながら手を動かしながらで書いたので、ツッコミどころが多い事かと思います。
とはいえ、出来る限りツッコミどころのないように心がけたつもりです。どうか、ご指摘・お叱りいただく際は優しめのお言葉で、ご指南頂けますと幸いです🙇
-
水に浮かべる際は、以下の点にご注意ください。
- 水に浮かべる際は、自己責任でお願いします
- ぶっちゃけ私は、早々に沈没(水没)させました…
- すぐに水から上げて電源を抜き、事なきを得ましたが、防水必須です
- バッテリーとラズパイを3台ずつ積むと、さすがに浮力が足りないようです
- 水に浮かべると、船尾の際が水面と数ミリしか離れていないので、少しでも傾くと一気に水が流れ込んできて沈みます
- 基盤むき出しの電子機器なので、水没すれば壊れるし、LiPoバッテリーのショートは非常に危険です(バッテリーだけは一時的にテープ等で完全に防水しておくのが良いです)
- 水没しないように万全に備えるか、見た目を愛でて楽しみましょう
⇒ 記事に戻る: ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ -
ぶっちゃけ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でやった方がいいです
⇒ 記事に戻る: ↩︎ ↩︎ -
コンテナ船の中敷きについて
- 加工精度は必要としないので、3Dプリントでなくとも、紙とテープで作ってしまっても良いかと思います
- 3Dプリンタがある方は、ThingiverseにSTLファイル等をアップしましたので、良かったらプリントしてみてください。 https://www.thingiverse.com/thing:4940828
- 最近は3Dプリントもめちゃくちゃ安くなってきたので、買ってしまう手もあるかと思います。ちなみに、自分は1年前に1万5千円弱で購入したKingroonのコイツでプリントアウトしましたが、かなり満足してます(台がマグネットシートでめくれるので、アイテムを取り外しやすいのもGOOD)。
⇒ 記事に戻る: ↩︎ -
ラジコン化のハマりポイントについて
- 防水目的でビニール袋に入れるとBluetoothが切れやすくなる
- そもそもモーターを水に沈めるとBluetooth接続が不安定になる
- Bluetoothが不安定だとアプリ側で500エラー多発する
- 調査がなかなかに難しい
- とにかくハマリポイント多め
⇒ 記事に戻る: ↩︎ -
接続元に mDNS に仕組みが必要になりますが、今どきの開発環境なら大抵備わっているかと思います
記事に戻る: ↩︎ -
今回の記事はネットワーク周りの設定が多く、ネットワーク周りでハマる(sshできなくなる)可能性が高いので、念のためディスプレイ等を繋いで作業できるようにしておいた方が安全です
⇒ 記事に戻る: ↩︎
Discussion