はじめに
「CloudNative Days TOKYO 2020」で、Jetsonのk3sで監視カメラシステムをデプロイするセッションがあり、少し感化されて自宅のラズパイkubernetesクラスタでもやってみようと思い立ちました。
環境
自宅のネットワーク構成がプライベートネットワークの多段構成だったため、設備面で結構悩みました。
今回の作業で増築した部分は下図の赤い部分です。
kubernetesクラスタのネットワークが自宅のプライベートネットワーク内にさらにネットワーク(有線)を作って構築しており、masterをルータにして外部接続させてます。自宅のプライベートネットワークだけならWifi Routerに接続して終わりだったのですが、kubernetesは有線LANにしてパケットも見たかったという当初の思惑があり、このような構成になってます。
そのせいで離れた場所にkubernetesのノードを配置できない状況でした。解決方法はkubernetesクラスタのネットワークにWifiアクセスポイントを設置してWifiで繋げるという選択しか思いつきませんでした。クローゼットの奥を探すと、昔使用していたEthernet ConverterセットのWifiルータがあったので、APモードで使用する事にしました。
- Wifi AP + Ethernet Converter (自宅にあった昔使ってた余り物)
- Aterm WR8700N + WL300NE-AG
- Raspberry Pi 4B
- Rspberry Pi OS 64bit(beta)
USB3のSSDからのBOOT。
インストールは『Raspberry Pi 4 の 64bit版と USB Boot』を参考にさせていただきました。 - motionEye (ccrisan/motioneye)
- 参考にした情報 wiki/Install In Docker
- USBカメラ
-
ELP-USBFHD06H-MFV(2.8-12)
Raspberry Pi 3B+の時に買ったもので、「USB2.0」「低照度」「UVC」「マニュアルフォーカス」「産業用カメラ」のキーワードで探して購入しました。少々高いですが、室温40度の窓際でも2シーズン壊れてません。
夜間は人間の目で見るより若干暗く映る感じで、外灯があればそこそこ見れる感じです。
構築
Raspberry Pi OS 64bit(beta)をkubernetesのworkerに設定
STEP1 : Workerノードにて
IPアドレス固定化したり、kernelパラメータいじったり、swapを無効化したりした状態です。
# curl -sSL https://get.docker.com | sh
# curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | apt-key add -
# echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" >> /etc/apt/sources.list.d/kubernetes.list
# apt update
# apt install kubelet=1.17.0-00 kubeadm=1.17.0-00 kubectl=1.17.0-00
# update-alternatives --set iptables /usr/sbin/iptables-legacy
# iptables --policy FORWARD ACCEPT
2019年末に構築したっきりなので、バージョンは1.17.0-00を指定してます。お使いのものに置き換えて読んでください。
最近のOSかdockerかは知りませんが、FORWARDのデフォルトポリシーをACCEPTにしなきゃ正常に動かないです。
個人的にはsystemdでサービス化して設定しています。
STEP2 : Masterノードにて
# kubeadm token create
4vstbr.hod97s2tevtsmolh
# openssl x509 -pubkey -in /etc/kubernetes/pki/ca.crt | openssl rsa -pubin -outform der 2>/dev/null | openssl dgst -sha256 -hex | sed 's/^.* //'
5511e8fe84cb1da5c62ad756e72f3725da8a06fde0f6179563d58df283444ba0
tokenとhashをメモる。tokenは期限があるので再利用はしない方がいいです。
STEP3 : Workerノードにて
# kubeadm join 10.0.0.1:6443 --token 4vstbr.hod97s2tevtsmolh --discovery-token-ca-cert-hash sha256:5511e8fe84cb1da5c62ad756e72f3725da8a06fde0f6179563d58df283444ba0
# kubectl label node chiya node-role.kubernetes.io/worker=
ノード名はご自身のものに置き換えて読んでください。
元々armhfで構築していますが、arm64のノードが追加されました!
# kubectl get node -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
chino Ready master 251d v1.17.0 10.0.0.1 <none> Raspbian GNU/Linux 10 (buster) 4.19.97-v7l+ docker://19.3.5
chiya Ready worker 15h v1.17.0 10.0.0.5 <none> Debian GNU/Linux 10 (buster) 5.4.51-v8+ docker://19.3.12
cocoa Ready worker 251d v1.17.0 10.0.0.2 <none> Raspbian GNU/Linux 10 (buster) 4.19.75-v7l+ docker://19.3.5
rize Ready worker 251d v1.17.0 10.0.0.3 <none> Raspbian GNU/Linux 10 (buster) 4.19.75-v7l+ docker://19.3.5
syaro Ready worker 135d v1.17.0 10.0.0.4 <none> Raspbian GNU/Linux 10 (buster) 4.19.118-v7l+ docker://19.3.11
# kubectl get node --show-labels
NAME STATUS ROLES AGE VERSION LABELS
chino Ready master 251d v1.17.0 beta.kubernetes.io/arch=arm,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm,kubernetes.io/hostname=chino,kubernetes.io/os=linux,node-role.kubernetes.io/master=
chiya Ready worker 16h v1.17.0 beta.kubernetes.io/arch=arm64,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm64,kubernetes.io/hostname=chiya,kubernetes.io/os=linux,node-role.kubernetes.io/worker=
cocoa Ready worker 251d v1.17.0 beta.kubernetes.io/arch=arm,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm,kubernetes.io/hostname=cocoa,kubernetes.io/os=linux,node-role.kubernetes.io/worker=
rize Ready worker 251d v1.17.0 beta.kubernetes.io/arch=arm,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm,kubernetes.io/hostname=rize,kubernetes.io/os=linux,node-role.kubernetes.io/worker=
syaro Ready worker 135d v1.17.0 beta.kubernetes.io/arch=arm,beta.kubernetes.io/os=linux,kubernetes.io/arch=arm,kubernetes.io/hostname=syaro,kubernetes.io/os=linux,node-role.kubernetes.io/worker=
archを見るような処理はちゃんとarchに合ったコンテナイメージを取得します。
# kubectl get pod -n kube-system -o wide | grep flannel
kube-flannel-ds-arm-2gdmm 1/1 Running 41 251d 10.0.0.1 chino <none> <none>
kube-flannel-ds-arm-758n9 1/1 Running 14 251d 10.0.0.2 cocoa <none> <none>
kube-flannel-ds-arm-l6fkd 1/1 Running 15 251d 10.0.0.3 rize <none> <none>
kube-flannel-ds-arm-xd9rm 1/1 Running 0 135d 10.0.0.4 syaro <none> <none>
kube-flannel-ds-arm64-dj57r 1/1 Running 4 16h 10.0.0.5 chiya <none> <none>
# kubectl describe pod kube-flannel-ds-arm64-dj57r -n kube-system | grep -i image:
Image: quay.io/coreos/flannel:v0.11.0-arm64
Image: quay.io/coreos/flannel:v0.11.0-arm64
すばらしいですね!
motioneye on kubernetes
motioneyeのdocker.hubイメージのタグを見ればわかると思いますが、arm64がありません。
結論から言うと、試しにデプロイしたらarm64のdockerでarmhfも動きました。ちゃんと調べてませんが、ライブラリ関連も全部armhfで構成されたイメージだから動作したのだと思います。しかし、kernel寄りの処理はエラーが起きないか不安ですね...今回カメラデバイス(/dev/video0)を使うのですが、大丈夫かなぁ?という不安はありました。(動きましたが)
Wikiの「Install-In-Docker」に記載されているコマンドからkubernetesのyamlを書き起こします。
docker run --name="motioneye" \
-p 8765:8765 \
--hostname="motioneye" \
-v /etc/localtime:/etc/localtime:ro \
-v /etc/motioneye:/etc/motioneye \
-v /var/lib/motioneye:/var/lib/motioneye \
--restart="always" \
--detach=true \
ccrisan/motioneye:master-amd64
使うイメージは、「ccrisan/motioneye:master-armhf」ですね。ポート番号は「8765」です。
あとはボリュームですね。ホスト側の/etc/localtimeを見せて、設定ファイル置き場の/etc/motioneyeと録画データを/var/lib/motioneyeに永続ボリュームマッピングすればいいのだと思います。
ボリュームの戦略は全く考えていません。デプロイ先のノードは固定するしCSIも無いので、単純にhostPathで作成します。パフォーマンスも最強なので...
で、一番大事なビデオデバイスの部分です。
--device=/dev/video0
これも/etc/localtimeと同じく、hostPathで定義しちゃえばいいと思います。
以下が録画データ用のyamlです。
apiVersion: v1
kind: PersistentVolume
metadata:
name: motioneye-pv
labels:
type: local
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /opt/motioneye/data
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: motioneye-pvc
labels:
app: motioneye
tier: motioneye
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Gi
以下が設定ファイル用のyamlです。
apiVersion: v1
kind: PersistentVolume
metadata:
name: motioneye-etc-pv
labels:
type: local
spec:
capacity:
storage: 100Mi
accessModes:
- ReadWriteOnce
persistentVolumeReclaimPolicy: Recycle
hostPath:
path: /opt/motioneye/etc
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: motioneye-etc-pvc
labels:
app: motioneye
tier: motioneye
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
ポート番号はServiceにnodePortで公開するよう定義します。
apiVersion: v1
kind: Service
metadata:
name: motioneye
labels:
app: motioneye
spec:
type: NodePort
ports:
- port: 8765
targetPort: 8765
nodePort: 30876
protocol: TCP
selector:
app: motioneye
最後にDeploymentの定義です。
ホスト側の/dev/video0を使うために、securityContextでrootの特権を与えてます。
nodeSelectorでデプロイ先のノード名を指定しています。(確かkubernetes.io/hostnameの使用は推奨されていなかったはずなので、ちゃんとラベルを付けてあげましょう。)
apiVersion: apps/v1
kind: Deployment
metadata:
name: motioneye
labels:
app: motioneye
spec:
replicas: 1
selector:
matchLabels:
app: motioneye
template:
metadata:
labels:
app: motioneye
spec:
containers:
- image: ccrisan/motioneye:master-armhf
name: motioneye
ports:
- containerPort: 8765
name: motioneye
securityContext:
privileged: true
volumeMounts:
- name: motioneye-local-storage
mountPath: /var/lib/motioneye
- name: motioneye-etc-storage
mountPath: /etc/motioneye
- name: etc-localtime
mountPath: /etc/localtime
- name: dev-video0
mountPath: /dev/video0
volumes:
- name: motioneye-local-storage
persistentVolumeClaim:
claimName: motioneye-pvc
- name: motioneye-etc-storage
persistentVolumeClaim:
claimName: motioneye-etc-pvc
- name: etc-localtime
hostPath:
path: /etc/localtime
- name: dev-video0
hostPath:
path: /dev/video0
nodeSelector:
kubernetes.io/hostname: chiya
yamlが全部できたらファイルのあるディレクトリで一気にapplyします。
# kubectl apply -f .
デプロイ状況を確認します。
# kubectl get pod,pv,pvc,svc -o wide | grep motioneye
pod/motioneye-664556dcdb-bb2hb 1/1 Running 1 13h 172.16.6.10 chiya <none> <none>
persistentvolume/motioneye-etc-pv 100Mi RWO Recycle Bound default/motioneye-etc-pvc 19h Filesystem
persistentvolume/motioneye-pv 100Gi RWO Recycle Bound default/motioneye-pvc 20h Filesystem
persistentvolumeclaim/motioneye-etc-pvc Bound motioneye-etc-pv 100Mi RWO 19h Filesystem
persistentvolumeclaim/motioneye-pvc Bound motioneye-pv 100Gi RWO 20h Filesystem
service/motioneye NodePort 10.96.185.61 <none> 8765:30876/TCP 20h app=motioneye
...正直、ストレージの状態がいいのか悪いのかよくわかってません。今後の宿題でしょう。
Chromeの設定
Chromeのバージョンによっては監視カメラのストリーミングを見る事ができません。
その場合は、以下の設定を変更してみてください。
chrome://flags/#enable-lazy-image-loading
motionEyeの設定
自宅内からはmasterノード(192.168.1.9)のnodePortで設定したポート(30876)でアクセスしています。
初期状態だと「admin」パスワードなしでログインできます。
「add camera...」でカメラを追加しますが、環境によって色々出てくると思います。
適当に選んでみて、設定メニューの「Video Device」で「Camera Device」が「/dev/video0」になっているものがホストから共有したデバイスなので探してください。※カメラの映像も映るので...


深夜ですが、外灯のみでカメラ用の照明や赤外線なしでこれくらい見えれば良い感じだと思います。
手元のデジカメと比べてみましたが、ISO4000以上の感度はあるんじゃないかと思います。
動体検知のコツですが、私はカメラの感度が上がる夜に「Show Frame Changes」をON、「Auto Noise Detection」をOFFにして、右上の数字が10以上50未満になるように調整して、「Frame Change Threshold」を150〜200にしています。
カメラの性能にもよるので自分なりに調整してみてください。
「Frame Change Threshold」は0%付近になるので、GUIより設定ファイルを直接編集した方が良いです。
設定ファイルは、motion-eyeをデプロイしたノードの、「/opt/motioneye/etc/camera-1.conf」くらいになっていると思います。
わざわざ動体検知させに写りに行きました。
おわりに
motioneyeは動画のフレーム差分のピクセル数で動体検知しているようです。調整は難しいですが、うまく調整できればちゃんと意図した検知ができると思います。
あと、クラウドへのアップロードやイベント処理も豊富なので動体検知時の画像ファイルを物体認識のAPIにかけても楽しいでしょう。
個人的には以前、GCPのAutoML Vision APIで自分の車なのかご近所さんの車なのかを頑張ってモデルを作って判定させて、Twitterで通知させたりしていました。
画像が紫がかってるので、この時は赤外線カメラを使ってました。GCPのクレジットもまだあったのでゴリゴリ使ってましたが、タダで貰えた分が無くなって、課金が怖くなったので、GCPからは撤退しました。ここまでやれると楽しいですよね。
まぁ撤退ってのは冗談で、今は「Coral Edge TPU」を購入したので、またGCPでAutoML Visionのモデルを作って遊んでみたいと思ってます。(...と言いつつもう1年経とうとしてます。)
kubernetesで監視カメラのアプリをデプロイできると、同じようなエッジ端末で同じラベル付けをすれば一気に監視カメラをデプロイする事ができます。永続ストレージもちゃんと用意すれば、設定ファイルや録画した動画の一元管理もしやすくなりますよね。そうなってくると、工場や産業のIoTで活用できるのではないでしょうか。
まぁその前にLANケーブルなんとかしたいので、ローカル5Gですね。




