はじめに
ROSロボットをKubernetesクラスタ上で動かす際に、hostNetwork: true
で動かす事例を非常に多く見かけます。非常に多くと言うか99.9999%それしか見ない気がします。
私達が公開しているOSSであるRDBOXのチュートリアルでもその方式を採用しています。
各種デバックツール(rviz、rqtなど)を使う際に、Kubernetes上で動いていることを気にしなくても良い点は大きなメリットです。
ですが、公式ドキュメントでは
hostPortの理由と同じくして、hostNetworkの使用はできるだけ避けてください。
とあります。なるほど。
確かにCNI(Container Network Interface)を使うことで、hostNetworkを使うこと以上のメリットがあることが分かっています。セキュリティ上のメリット、アクセスコントロールなどなど。詳しくはCluster Networking | Kubernetesを御覧ください。
多くの人が採用していないように、CNIを使うことは多少面倒です。しかし、両論併記するのが正しいと感じ、本投稿ではCNIを使ってROSロボットを動かしてみたいと思います。
なお、CNIとしてはflannelを利用しました。
TL;DR
サンプルコードはこちらにあります。rdbox-intec/study-cni
roscoreとChatterのいつものROSのサンプルです。
以下に示す感じののDeploymentを構成します。注目は、hostNetwork: true
を一切指定していないところです。
rdbox/study-cni:v1.0
はtalker及び、listenerをroslaunchで起動できるようにしたコンテナです。
技術的な各POINTは、コメントにある通りです。詳細については後で説明します。
実行結果
$ 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
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
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 # POINT5
value: "http://simulation-roscore:11311"
listener
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
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そこで、今回採る方式では、ROS Masterの通信(Port:11311、TCP)のみをKubernetesのServiceとして公開し、残りのPod間(ROSでいうところのNode間)通信にはflannelのVXLANをそのまま利用します。
POINT1
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を確認下さい。
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を組み合わせることで、今までと大きく違いのない形での開発も可能です。是非Kubernetesを有効活用した充実したROSライフをお送り下さい。
次回予告
ROS2版の解説も作ります。
宣伝
Kubernetes上でのROS活用について、我々のOSSではより深く・簡単に理解することが出来ます。是非、ご活用下さい。
rdbox-intec/rdbox: GitHub