kubernetesでmotioneyeをデプロイするタイプのラズパイ監視カメラ

# はじめに
「CloudNative Days TOKYO 2020」で、Jetsonのk3sで監視カメラシステムをデプロイする[セッション](http://event.cloudnativedays.jp/cndt2020/talks/70)があり、少し感化されて自宅のラズパイkubernetesクラスタでもやってみようと思い立ちました。

# 環境
自宅のネットワーク構成がプライベートネットワークの多段構成だったため、設備面で結構悩みました。
今回の作業で増築した部分は下図の赤い部分です。

![スクリーンショット 2020-09-10 11.48.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/ba1b38d3-7441-16b1-0ad7-1570928bdd2c.png)


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](https://qiita.com/tkyonezu/items/6c2a4638e3b8aee390e2)』を参考にさせていただきました。
- [motionEye (ccrisan/motioneye)](https://hub.docker.com/r/ccrisan/motioneye/)  
 - [参考にした情報 wiki/Install In Docker](https://github.com/ccrisan/motioneye/wiki/Install-In-Docker)
- USBカメラ
 - [ELP-USBFHD06H-MFV(2.8-12)](https://www.amazon.co.jp/dp/B07R78DVQN/ref=cm_sw_r_tw_dp_x_4EpwFbNE12BD8)  
   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](https://hub.docker.com/r/ccrisan/motioneye/)イメージのタグを見ればわかると思いますが、arm64がありません。
結論から言うと、試しにデプロイしたらarm64のdockerでarmhfも動きました。ちゃんと調べてませんが、ライブラリ関連も全部armhfで構成されたイメージだから動作したのだと思います。しかし、kernel寄りの処理はエラーが起きないか不安ですね...今回カメラデバイス(/dev/video0)を使うのですが、大丈夫かなぁ？という不安はありました。（動きましたが）


Wikiの[「Install-In-Docker」](https://github.com/ccrisan/motioneye/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です。

```yaml:motioneye-pv.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
```

```yaml:motioneye-pvc.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: motioneye-pvc
  labels:
    app: motioneye
    tier: motioneye
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Gi
```

以下が設定ファイル用のyamlです。

```yaml:motioneye-pv2.yml
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
```

```yaml:motioneye-pvc2.yml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: motioneye-etc-pvc
  labels:
    app: motioneye
    tier: motioneye
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 100Mi
```

ポート番号はServiceにnodePortで公開するよう定義します。

```yaml:motioneye-service.yml
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の使用は推奨されていなかったはずなので、ちゃんとラベルを付けてあげましょう。）

```yaml:motioneye.yml
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」になっているものがホストから共有したデバイスなので探してください。※カメラの映像も映るので...
![スクリーンショット 2020-09-10 2.47.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/3572d26f-a87d-afa2-d56f-df5f90f639f1.png)
![スクリーンショット 2020-09-10 2.47.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/65a85fba-446d-6d88-7ce5-f17cbafb3046.png)

深夜ですが、外灯のみでカメラ用の照明や赤外線なしでこれくらい見えれば良い感じだと思います。
手元のデジカメと比べてみましたが、ISO4000以上の感度はあるんじゃないかと思います。
動体検知のコツですが、私はカメラの感度が上がる夜に「Show Frame Changes」をON、「Auto Noise Detection」をOFFにして、右上の数字が10以上50未満になるように調整して、「Frame Change Threshold」を150〜200にしています。
カメラの性能にもよるので自分なりに調整してみてください。

![スクリーンショット 2020-12-29 0.51.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/55b52475-9fdd-81c0-7245-154c2d6301c2.png)

「Frame Change Threshold」は0%付近になるので、GUIより設定ファイルを直接編集した方が良いです。
設定ファイルは、motion-eyeをデプロイしたノードの、「/opt/motioneye/etc/camera-1.conf」くらいになっていると思います。

![スクリーンショット 2020-09-10 3.06.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/1deee8b0-42b6-4f95-c4e8-34921492a542.png)

わざわざ動体検知させに写りに行きました。

![スクリーンショット 2020-09-10 9.49.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/febedec7-9e01-1646-f863-ac967890df70.png)


# おわりに
motioneyeは動画のフレーム差分のピクセル数で動体検知しているようです。調整は難しいですが、うまく調整できればちゃんと意図した検知ができると思います。
あと、クラウドへのアップロードやイベント処理も豊富なので動体検知時の画像ファイルを物体認識のAPIにかけても楽しいでしょう。
個人的には以前、GCPのAutoML Vision APIで自分の車なのかご近所さんの車なのかを頑張ってモデルを作って判定させて、Twitterで通知させたりしていました。

![twitter_EhYUKjtUcAAjvyQ.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/410957/d0c9f6e7-cd31-dd75-8e0a-37e8ce40f834.jpeg)

画像が紫がかってるので、この時は赤外線カメラを使ってました。GCPのクレジットもまだあったのでゴリゴリ使ってましたが、タダで貰えた分が無くなって、課金が怖くなったので、GCPからは撤退しました。ここまでやれると楽しいですよね。
まぁ撤退ってのは冗談で、今は「Coral Edge TPU」を購入したので、またGCPでAutoML Visionのモデルを作って遊んでみたいと思ってます。（...と言いつつもう1年経とうとしてます。）

kubernetesで監視カメラのアプリをデプロイできると、同じようなエッジ端末で同じラベル付けをすれば一気に監視カメラをデプロイする事ができます。永続ストレージもちゃんと用意すれば、設定ファイルや録画した動画の一元管理もしやすくなりますよね。そうなってくると、工場や産業のIoTで活用できるのではないでしょうか。
まぁその前にLANケーブルなんとかしたいので、ローカル5Gですね。