Kubernetes CNI (flannelなど) を使ってROSロボットを動かそう

# はじめに
ROSロボットをKubernetesクラスタ上で動かす際に、`hostNetwork: true`で動かす事例を非常に多く見かけます。非常に多くと言うか99.9999%それしか見ない気がします。

私達が公開しているOSSである[RDBOX](https://github.com/rdbox-intec/rdbox)のチュートリアルでもその方式を採用しています。
各種デバックツール（rviz、rqtなど）を使う際に、Kubernetes上で動いていることを気にしなくても良い点は大きなメリットです。

ですが、[公式ドキュメント](https://kubernetes.io/docs/concepts/configuration/overview/)では

> hostPortの理由と同じくして、hostNetworkの使用はできるだけ避けてください。

とあります。なるほど。

確かにCNI(Container Network Interface)を使うことで、hostNetworkを使うこと以上のメリットがあることが分かっています。セキュリティ上のメリット、アクセスコントロールなどなど。詳しくは[Cluster Networking \| Kubernetes](https://kubernetes.io/docs/concepts/cluster-administration/networking/)を御覧ください。
多くの人が採用していないように、CNIを使うことは多少面倒です。しかし、両論併記するのが正しいと感じ、本投稿ではCNIを使ってROSロボットを動かしてみたいと思います。
なお、CNIとしては[flannel](https://github.com/coreos/flannel)を利用しました。


# TL;DR
サンプルコードはこちらにあります。[rdbox\-intec/study\-cni](https://github.com/rdbox-intec/study-cni)

roscoreとChatterのいつものROSのサンプルです。
以下に示す感じののDeploymentを構成します。注目は、`hostNetwork: true`を一切指定していないところです。
`rdbox/study-cni:v1.0`はtalker及び、listenerをroslaunchで起動できるようにしたコンテナです。
技術的な各POINTは、コメントにある通りです。詳細については後で説明します。

## 実行結果

```bash
$ git clone https://github.com/rdbox-intec/study-cni.git
$ cd study-cni
$ kubectl apply -f manifest
deployment.apps/simulation-listener created
deployment.apps/simulation-roscore created
deployment.apps/simulation-talker created
service/simulation-roscore created

$ kubectl get pods
NAME                                   READY   STATUS    RESTARTS   AGE
simulation-listener-6976c99bd4-84729   1/1     Running   0          25m
simulation-roscore-5fcf5c9654-7wdfh    1/1     Running   0          25m
simulation-talker-6cffc7c7d4-k4t4s     1/1     Running   0          25m

$ kubectl logs simulation-listener-6976c99bd4-84729 -f
[INFO] [1594001173.025856]: /listenerI heard hello world 1594001173.024013
[INFO] [1594001173.125795]: /listenerI heard hello world 1594001173.124052
[INFO] [1594001173.226228]: /listenerI heard hello world 1594001173.2242084
[INFO] [1594001173.326462]: /listenerI heard hello world 1594001173.3246925
[INFO] [1594001173.425796]: /listenerI heard hello world 1594001173.4240358
[INFO] [1594001173.526164]: /listenerI heard hello world 1594001173.524595
[INFO] [1594001173.626199]: /listenerI heard hello world 1594001173.6245856
[INFO] [1594001173.726744]: /listenerI heard hello world 1594001173.724751
```

## roscore

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: simulation-roscore
  labels:
    app: simulation-roscore
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simulation-roscore
  template:
    metadata:
      labels:
        app: simulation-roscore
    spec:
      containers:
      - name: simulation-roscore
        image: ros:noetic-ros-core-focal
        tty: true
        args:
        - roscore
        env:
          - name: ROS_IP                                     # POINT1
            valueFrom:
              fieldRef:
                fieldPath: status.podIP
        ports:                                               # POINT2
          - containerPort: 11311
            protocol: TCP

---
                                                             # POINT3
apiVersion: v1
kind: Service
metadata:
  name: simulation-roscore
  labels:
    app: simulation-roscore
spec:
  ports:
  - port: 11311
    protocol: TCP
  selector:
    app: simulation-roscore
```

## Application
### talker
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: simulation-talker
  labels:
    app: simulation-talker
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simulation-talker
  template:
    metadata:
      labels:
        app: simulation-talker
    spec:
      containers:
        - name: simulation-talker
          image: rdbox/study-cni:v1.0
          tty: true
          args:                                             # POINT4
          - roslaunch
          - --screen
          - --wait
          - talker
          - talk.launch
          env:
            - name: ROS_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: ROS_MASTER_URI                          # POINT５
              value: "http://simulation-roscore:11311"
```

### listener
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: simulation-listener
  labels:
    app: simulation-listener
spec:
  replicas: 1
  selector:
    matchLabels:
      app: simulation-listener
  template:
    metadata:
      labels:
        app: simulation-listener
    spec:
      containers:
        - name: simulation-listener
          image: rdbox/study-cni:v1.0
          tty: true
          args:
          - roslaunch
          - --screen
          - --wait
          - listener
          - listen.launch
          env:
            - name: ROS_IP
              valueFrom:
                fieldRef:
                  fieldPath: status.podIP
            - name: ROS_MASTER_URI
              value: "http://simulation-roscore:11311"

```

## Dockerfile

```dockerfile
FROM ros:noetic-ros-core-focal

LABEL maintainer="INTEC Inc<info-rdbox@intec.co.jp>"

ENV ROS_DISTRO=noetic

COPY ./ros_entrypoint.sh /ros_entrypoint.sh

RUN apt-get update && \
    apt-get install -y --no-install-recommends \
                                    build-essential

RUN /bin/bash -c "source /opt/ros/noetic/setup.bash && \
                mkdir -p /catkin_ws/src && \
                cd /catkin_ws/src && \
                catkin_init_workspace && \
                cd /catkin_ws/ && \
                catkin_make && \
                source /catkin_ws/devel/setup.bash && \
                chmod +x /ros_entrypoint.sh"

COPY ./talker /catkin_ws/src/talker
COPY ./listener /catkin_ws/src/listener

RUN /bin/bash -c "source /opt/ros/noetic/setup.bash && \
                source /catkin_ws/devel/setup.bash && \
                cd /catkin_ws/ && \
                catkin_make && \
                apt autoclean -y && \
                apt autoremove -y && \
                rm -rf /var/cache/apt/archives/* /var/lib/apt/lists/*"

ENTRYPOINT ["/ros_entrypoint.sh"]
```


# 技術解説
## 全般
皆様おなじみの通り、ROSのNode間通信はどのポートを使うか分からないので全部開いておく必要があります。[ROS Security](http://wiki.ros.org/Security)そこで、今回採る方式では、ROS Masterの通信（Port:11311、TCP）のみをKubernetesのServiceとして公開し、残りのPod間（ROSでいうところのNode間）通信にはflannelのVXLANをそのまま利用します。

## POINT1
```yaml
env:
  - name: ROS_IP
      valueFrom:
      fieldRef:
          fieldPath: status.podIP
```
全てのmanifestに登場しますが、Pod間で通信するために、ROSで使用するデフォルトIPをPodIPに切り替えておく必要があります。これによってCNIでアサインされたIPアドレスがROS_IPとして使用できます。fieldRefによってPod固有のプロパティを参照することが出来ます。詳しくは[Expose Pod Information to Containers Through Environment Variables \| Kubernetes](https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/)を確認下さい。


## POINT2
ROS Masterの通信をサービスとして公開するために公開用のポートを指定します。

## POINT3
POINT2で公開設定したポートを、Kubernetesのサービスとして公開します。selectorによってROS MasterのDeploymentファイルで指定したPodが対象のサービスとなっていることを明示しています。

## POINT4
launchファイルをwaitオプション付きで起動します。kubectlでApplyされるmanifestファイルは起動順序を考慮しないため、特定のroslaunchファイルを指定して動かす場合には必ずwaitオプションを付与することをおすすめします。
よって、KubernetesでROSを使う場合、各ROSプロセスはrosrunではなくroslaunchを使って起動するように心がける必要があります。

## POINT5
POINT2、POINT3で指定した通り、ROS Masterがサービスとして公開されます。クラスタ内部では、`simulation-roscore`というURIでこの公開されているサービスにアクセスすることができるようになっています。


# おわりに
こんな感じで、ROSをよりKubernetesの流儀に則った形で利用することが出来ました。flannelだけでなくcalico、canalなどより高機能なCNIを使うことでより高度な制御も実現可能です。
また、各デバッグツールはコンテナに閉じ込めてしまえば問題なくデバックもできます。例えば、Tiryohさんが公開されている[Tiryoh/docker\_ros\-desktop\-vnc: A Docker image to provide HTML5 VNC interface to access Ubuntu LXDE \+ ROS](https://github.com/Tiryoh/docker_ros-desktop-vnc)を組み合わせることで、今までと大きく違いのない形での開発も可能です。是非Kubernetesを有効活用した充実したROSライフをお送り下さい。

# 次回予告
ROS2版の解説も作ります。

# 宣伝
Kubernetes上でのROS活用について、我々のOSSではより深く・簡単に理解することが出来ます。是非、ご活用下さい。
[rdbox\-intec/rdbox: GitHub](https://github.com/rdbox-intec/rdbox)