はじめに
業務でROSプロジェクトをリリースするときに一番の悩みの種になるのが、CIとデプロイだと思います。数台であれば手動でセットアップしても管理可能だと思いますが、+100台のような大規模になる場合は、プロジェクトのバージョン、依存しているライブラリの管理、デプロイのタイミング等で考慮しなければいけないことが山のようにあります。今回は、NVIDIA JetsonにROSプロジェクトをリモートデプロイする手順をまとめました。
※本記事内容は2019年6月に検証した内容のため、公開時では最新情報と異なる可能性があります。実際に試すときは最新の情報をご確認してください。
前提知識
下記の知識があるとベストですが、なくても大筋は理解できるように基本的な部分から説明する予定です。不要な方は、読み飛ばしください。
- ROSの仕組みの基礎的な知識
- CircleCIの基礎的な知識
- colconの基礎的な知識
- AWS Greengrass & RoboMakerの基礎的な知識
環境
環境 | バージョン |
---|---|
開発機 | Ubuntu 16.04.6 LTS |
ROS | Kinetic Kame |
colcon-core | 0.3.22 |
colcon-ros | 0.3.10 |
Jetson TX2 | Linux tegra-ubuntu 4.4.38-tegra |
事前調査
まずはじめに必要な情報をまとめます。
AWS RoboMakerでデプロイするには何をする必要があるか?
1つめのドキュメントで詳しく解説してあるため、こちらを読めばほぼ理解できるかと思います。結論からいうと、ROSの最新ビルドツールであるcolconのbundleという機能を用いてデプロイしていることがわかります。
Jetson TX2でROSを動かすために
Jetson TX2は、aarch64
プラットフォームのため、ROSプロジェクトをどこかでクロスコンパイルする必要があります。RoboMakerでTurtleBot3にデプロイするでCloud9を使いクロスコンパイルする紹介記事がありますが、こちらはARMHF
プラットフォームであり、aarch64
は非対応でしたので今回は、CircleCIでクロスコンパイルしてbundleを行います。
CircleCIでarm64のdockerコンテナを動かすには
Support for ARM based Docker images. , Making your Docker images ARM compatible for Raspberry Piにもある通り、CircleCIでARMベースのコンテナを動かすことはできません。しかしCircleCIのExecutorタイプの選び方を読むと、Executorにはdocker・machine・macos
の3つが存在し、machineを選択すると専用の仮想マシン環境(VM)が使用できることがわかります。つまり自分でコンテナさえ用意できればarm64
でもCircleCIが使用できることがわかりました。
ROSのdockerイメージについて
ROSのオフィシャルイメージを確認すると、arm64v8
というタグが確認でき、これがaarch64
に対応しています。今回はこちらを元にコンテナを作成します。
これでタイトルの通り、ROSプロジェクトをJetson TX2にCircleCIでcolcon bundleしてAWS Greengrass&RoboMakerでデプロイ
できることがわかったので実際にデプロイをしてみましょう。
手順
これまでのドキュメントを見てきてわかる通り、いくつかの要素によって構成されているため少々複雑な手順となります。そのため今回は1つずつ順をおって構築までの手順を説明していこうと思います。
テスト用のROSプロジェクトを用意する
好きなプロジェクトを用意してください。ここではチュートリアルにある一番簡単なシンプルな配信者(Publisher)と購読者(Subscriber)を書く(C++)を用います。今回はせっかくCircleCIを用いるので一番簡単な下記のようなテストを用意しました。テストの書き方については、ソースを確認するのが一番だとおもいます。もしあまりROSプロジェクトの経験がない場合は、一度catkin_makeでコンパイルして動作を確認しましょう。
# include <ros/ros.h>
# include <gtest/gtest.h>
# include <std_msgs/String.h>
# include "common_talker.h"
# include <thread>
# include <atomic>
# include <chrono>
using namespace std;
using namespace std::chrono;
using namespace std::this_thread;
struct AnyHelper
{
AnyHelper() : count(0)
{
}
void cb(const std_msgs::String::ConstPtr& msg)
{
ROS_INFO("%s", msg->data.c_str());
++count;
}
uint32_t count;
};
class UTestSuite : public ::testing::Test
{
private:
public:
UTestSuite() = default;
~UTestSuite() override = default;
};
TEST_F(UTestSuite, creationTest)
{
ASSERT_NO_THROW(SampleTalker sample_node;) << "constcut node failed";
}
TEST_F(UTestSuite, addValueOne)
{
SampleTalker sample_node;
ASSERT_EQ(2, sample_node.countUp(1)) << "failed to count EXIT";
}
TEST(PublishTest, simplePubTest)
{
ros::NodeHandle nh;
AnyHelper h;
SampleTalker sample_node;
ros::Subscriber sub = nh.subscribe("chatter", 0, &AnyHelper::cb, &h);
EXPECT_EQ(sub.getNumPublishers(), 1U);
std_msgs::String new_msg;
new_msg.data = "message:";
sample_node.sendMessage();
ros::Duration(1.0).sleep();
ros::spinOnce();
EXPECT_EQ(h.count, 1U);
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "common_test");
testing::InitGoogleTest(&argc, argv);
thread t([] {
while (ros::ok())
ros::spin();
});
auto res = RUN_ALL_TESTS();
ros::shutdown();
return res;
}
colconでビルドできるようにする
つぎにcolconでビルドをできるようにします。catkin_makeとcolconでは、実行ファイルを生成する仕組みが異なるため、実際にはinstallを自分で設定をする必要があります。今回は下記に簡単に追記例も示しますが、Optional Step: Specifying Installable Targetsを確認して自分に必要な設定を記述します。
install(TARGETS ${PROJECT_NAME} common_talker common_listener
ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
install(DIRECTORY include/${PROJECT_NAME}/
DESTINATION ${CATKIN_PACKAGE_INCLUDE_DESTINATION}
FILES_MATCHING PATTERN "*.h"
)
install(FILES
launch/common.launch
DESTINATION ${CATKIN_PACKAGE_SHARE_DESTINATION}
)
catkin_makeからcolconへの移行はいくつかハマりポイントがあるので、いつか記事をまとめようとおもいます。それでは、colconをインストールしてビルド&テスト&bundleしていきましょう。
$ sudo apt-get update
$ sudo apt-get install python3-pip python3-apt
$ pip3 install -U setuptools
$ pip3 install -U colcon-common-extensions colcon-ros-bundle
$ colcon build
$ colcon test
$ colcon test-result --all --verbose
$ colcon bundle
bundleはすべての依存関係を構築しているため多少時間がかかるかもしれません。ここまでエラーなくできたら、bundleしたパッケージでROSが動くか確認しましょう。まず作業用環境を作成します。
$ mkdir -p output_test
$ cd output_test
次に作業環境にoutput.tarをoutput_test移動させ、bundleしたパッケージの展開をします。詳細な手順と仕組みは、こちらで確認できます。
$ tar -xvf output.tar
$ mkdir -p dependencies
$ tar -zxvf dependencies.tar.gz -C dependencies/
$ mkdir -p workspace
$ tar -zxvf workspace.tar.gz -C workspace/
次に環境設定を行います。
$ BUNDLE_CURRENT_PREFIX=/home/wnwn/output_test/dependencies source /home/wnwn/output_test/dependencies/setup.sh
$ BUNDLE_CURRENT_PREFIX=/home/wnwn/output_test/workspace source /home/wnwn/output_test/workspace/setup.sh
最後に現状でrosのコマンドがbundle環境を正常に参照できているか確認します。
$ which roslaunch
/home/wnwn/output_test/opt/ros/kinetic/bin/roslaunch
これでビルドしたROSパッケージをroslaunchから起動できていればcolconへの移行作業はおわりです。
ローカルのdockerでarm64コンテナを動かす
x86_64のUbuntuでarm64のコンテナを使用するには、QEMUを使用します。
$ sudo docker pull arm64v8/ros:kinetic-ros-base
$ sudo docker run -it arm64v8/ros:kinetic-ros-base
standard_init_linux.go:211: exec user process caused "exec format error"
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
99f47fee2e9c arm64v8/ros:kinetic-ros-base "/ros_entrypoint.sh …" 24 seconds ago Exited (1) 23 seconds ago distracted_rubin
$ sudo apt install qemu-user-static
$ sudo docker cp /usr/bin/qemu-aarch64-static 99f47fee2e9c:/usr/bin/qemu-aarch64-static
$ sudo docker commit 99f47fee2e9c arm64v8/ros:kinetic-ros-base-cross
sha256:4b09be00eb91a3202eb83103281ffc6e8e32632345b1b5b3b32ccecf3df53b15
$ sudo docker run -v ~/catkin_pkg:/root/ros_ws/ -it arm64v8/ros:kinetic-ros-base-cross
$ apt-get update
$ apt-get install python3-pip python3-apt
$ pip3 install -U setuptools
$ pip3 install -U colcon-common-extensions colcon-ros-bundle
$cd ~/ros_ws
$ colcon build
$ colcon test
$ colcon test-result --all --verbose
上記でビルドしたパッケージが実行できれば問題ありませんが、今回のわたしの環境の場合は、os.networkInterfaces() throws EAFNOSUPPORT under qemu-user-static on x86 hostのため動かず、qemuの最新バージョンを取ってきて入れなおしました。動いた方は、下記の手順は不要です。
$ wget https://github.com/multiarch/qemu-user-static/releases/download/v4.0.0-2/qemu-aarch64-static.tar.gz
$ tar -zxvf qemu-aarch64-static.tar.gz
$ sudo docker cp ./qemu-aarch64-static 99f47fee2e9c:/usr/bin/qemu-aarch64-static
$ sudo docker commit 99f47fee2e9c arm64v8/ros:kinetic-ros-base-cross1
$ sudo docker run -v ~/catkin_pkg:/root/ros_ws/ -it arm64v8/ros:kinetic-ros-base-cross1
$ apt-get update
$ apt-get install python3-pip python3-apt
$ pip3 install -U setuptools
$ pip3 install -U colcon-common-extensions colcon-ros-bundle
$ colcon build
$ colcon test
$ colcon test-result --all --verbose
colcon bundleの依存関係にarm64を追加する
buildとtestができるようになりましたが、ここでそのままcolcon bundle
を実行すると依存関係が解決できていません。その理由は、--apt-sources-list
オプションをつけないとデフォルトでxenial.sources.listが呼ばれます。その中身がこれでARM Support
にarmhfしか含まれていません。そのため、これをコピーして下記のように書き換えます。
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ xenial main restricted universe multiverse
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ xenial-updates main restricted universe multiverse
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ xenial-backports main restricted
deb [arch=armhf,arm64] http://ports.ubuntu.com/ubuntu-ports/ xenial-security main restricted universe multiverse
そして用意したmy.xenial.sources.list
を下記のように指定して実行します。
colcon bundle --apt-sources-list my.xenial.sources.list
これでarm64のコンテナでbundleができるようになりました。
Jetson TX2でローカルでbundleしたプロジェクトを動かす
SCPで前述で作成したoutput.tarをJetson TX2に移動します。移動したら先ほどbundleで作成したプロジェクトが動くか確認する手順と同じで順で確認します。
$ mkdir -p output_test
$ cd output_test
$ tar -xvf output.tar
$ mkdir -p dependencies
$ tar -zxvf dependencies.tar.gz -C dependencies/
$ mkdir -p workspace
$ tar -zxvf workspace.tar.gz -C workspace/
$ BUNDLE_CURRENT_PREFIX=/home/nvidia/output_test/dependencies source /home/nvidia/output_test/dependencies/setup.sh
$ BUNDLE_CURRENT_PREFIX=/home/nvidia/output_test/workspace source /home/nvidia/output_test/workspace/setup.sh
$ which roslaunch
/home/nvidia/output_test/opt/ros/kinetic/bin/roslaunch
これでx86_64でホストしたdockerのarm64イメージでcolcon bundleしたROSプロジェクトがJetson TX2で動くことが確認できました。
AWSの準備する
わたしは、CloudFormationの知識があまりないため、Terraformを使用していますが、TerraformはGreengrassとRoboMakerにまだ対応していないため、できるならばCloudFormationで管理したほうが良いのかなと思います。今回のプロジェクトで使用するサービスは下記ですが、必要に応じて権限を作成してください。
- iam
- lambda(sts:AssumeRole)
- greengrass(sts:AssumeRole)
- robomaker:UpdateRobotDeployment
- s3
- ecr
ECRにイメージを登録する
AWSのECRに登録するDockerファイルの例を示します。
FROM arm64v8/ros:kinetic-ros-base-tx2
LABEL maintainer="wnwn"
RUN apt-get update && \
apt-get install -y curl ssh git python3-pip python3-apt doxygen graphviz clang-format&& \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# setup github & keychain
RUN mkdir -p ~/.ssh && \
touch ~/.ssh/known_hosts && \
ssh-keyscan github.com >> ~/.ssh/known_hosts
# setup pip
RUN pip3 install -U setuptools && \
pip3 install -U colcon-common-extensions colcon-ros-bundle awscli && \
rm -rf ~/.cache/pip/
イメージのビルドは、下記のようなシェルにしています。
# !/usr/bin/env bash
# スクリプトが存在するディレクトリ
SCRIPT_DIR=$(cd $(dirname $0);pwd)
# プロジェクトのHOMEに移動する
cd ${SCRIPT_DIR}/
# 使用するdocker imageを取得
CONTAINER_ID=$(sudo docker ps -a | grep -E "arm64v8/ros:kinetic-ros-base " | head -1 | awk '{print $1}')
if [[ -z ${CONTAINER_ID} ]]; then
sudo docker pull arm64v8/ros:kinetic-ros-base
sudo docker run -it arm64v8/ros:kinetic-ros-base
CONTAINER_ID=$(sudo docker ps -a | grep -E "arm64v8/ros:kinetic-ros-base " | awk '{print $1}')
fi
# setup for tx2(arm64)
TX2_ID=$(sudo docker images | grep -E "kinetic-ros-base-tx2 " | head -1 | awk '{print $3}')
if [[ -z ${TX2_ID} ]]; then
wget https://github.com/multiarch/qemu-user-static/releases/download/v4.0.0-2/qemu-aarch64-static.tar.gz
tar -zxvf qemu-aarch64-static.tar.gz
sudo docker cp ./qemu-aarch64-static ${CONTAINER_ID}:/usr/bin/qemu-aarch64-static
sudo docker commit ${CONTAINER_ID} arm64v8/ros:kinetic-ros-base-tx2
rm -rf qemu-aarch64-static qemu-aarch64-static.tar.gz
fi
# for circleci
sudo docker build -t arm64v8/ros:kinetic-ros-base-tx2-circleci .
ECRにpushをします。AWSコマンドが使えることが前提です。************
には、自分のAWS IDを使用します。
# !/usr/bin/env bash
# スクリプトが存在するディレクトリ
SCRIPT_DIR=$(cd $(dirname $0);pwd)
# プロジェクトのHOMEに移動する
cd ${SCRIPT_DIR}/
# ログインする
sudo sh -c "~/.local/bin/aws ecr get-login --region ap-northeast-1 --profile circleci --no-include-email | sh"
# タグの登録
sudo docker tag arm64v8/ros:kinetic-ros-base-tx2-circleci ************.dkr.ecr.ap-northeast-1.amazonaws.com/circleci_for_tx2
sudo docker push ************.dkr.ecr.ap-northeast-1.amazonaws.com/circleci_for_tx2
これでECRへの登録ができました。
CircleCIでarm64イメージを動かす
ここまでくるとあとは作業なだけですので、config.yml
を示します。実際に何をCIするかは、各自の自由です。
version: 2.1
orbs:
slack: circleci/slack@3.2.0
references:
default: &default
working_directory: ~/catkin_ws
tf_defaults: &tf_defaults
<<: *default
machine: true
build_tool_test_and_build_test: &build_tool_test_and_build_test
run:
name: build_tool_test_and_build_test
command: |
docker run --rm --privileged multiarch/qemu-user-static:register --reset
$(aws ecr get-login --region ap-northeast-1 --no-include-email)
docker run -v ~/.ssh/:/root/.ssh/ -v ~/catkin_ws/:/root/catkin_ws/ --entrypoint /root/catkin_ws/shell/circleci_test.sh -it ************.dkr.ecr.ap-northeast-1.amazonaws.com/circleci_for_tx2
colcon_bundle: &colcon_bundle
run:
name: colcon_bundle
command: |
docker run --rm --privileged multiarch/qemu-user-static:register --reset
$(aws ecr get-login --region ap-northeast-1 --no-include-email)
docker run -v ~/.ssh/:/root/.ssh/ -v ~/catkin_ws/:/root/catkin_ws/ --entrypoint /root/catkin_ws/shell/circleci_bundle.sh -it ************.dkr.ecr.ap-northeast-1.amazonaws.com/circleci_for_tx2
s3_upload: &s3_upload
run:
name: s3_upload
command: |
aws s3 cp ./bundle/output.tar s3://${S3SOURCE}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_TAG}/
jobs:
test:
<<: *tf_defaults
steps:
- add_ssh_keys:
fingerprints:
- "****************************************"
- checkout
- *build_tool_test_and_build_test
upload:
<<: *tf_defaults
steps:
- add_ssh_keys:
fingerprints:
- "****************************************"
- checkout
- *colcon_bundle
- *s3_upload
- slack/notify:
title: "CIRCLE CI S3 UPLOAD NOTIFICATION"
message: "*TAG*\n${CIRCLE_TAG}\n*COMMIT*\nhttps://github.com/****/${CIRCLE_PROJECT_REPONAME}/commit/${CIRCLE_SHA1}\n*SOURCE*\ns3://${S3SOURCE}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_TAG}/output.tar\n"
workflows:
version: 2
test-and-deploy:
jobs:
- test
- upload:
filters:
tags:
only: /^v.*/
branches:
ignore: /.*/
わたしのプロジェクトは、外部の複数のリポジトリと内部のリポジトリをまとめたパッケージで構成されており、自分のプロジェクトについては、テストの他にもROS C++ Style Guideに則り、記述されたドキュメントの生成(doxygen)やコーディング規約(roscpp_code_format)のチェック(run-clang-format)、bundleファイルの管理やslack通知なども行っています。CircleCIが使えるようになれば、細かいところにも簡単に手が届いて便利ですね!
Jetson TX2にGreengrassをセットアップする
ここまで読んでくださったみなさま、おつかれさまでした。あともうすこしです。あとはすべて公式の手順のみで動かすことができます。まずTX2にGreengrassをセットアップしましょう。
ネットで検索するとカーネルの再構築等をやっている記事が見受けられますが、わたしの場合は特に不要でした。手順が終了したら、greengrassを起動しましょう。
$ cd /greengrass/ggc/core/
$ sudo ./greengrassd start
もし正常に起動しない場合やAWSコンソールに表示されない時のトラブルシューティングは下記をみるとよいです。
AWS IoT Greengrass : Troubleshooting with Logs
colcon bundleして出来上がったパッケージをS3に配置する
RoboMakerからリモートデプロイするには、デプロイするパッケージをS3に配置する必要があります。わたしの場合、CircleCIでbundle終了後にS3に配置し、そのURLをslackに通知するようにしています。
AWSコンソールからRoboMakerでTX2にデプロイする
それでは、AWSコンソールからTX2にデプロイする設定をします。まずフリートを作成します。
フリート名称を入力し作成
をクリックします。
次にロボットを作成します。
名称・アーキテクチャ・グループを選択します
作成が完了したらフリートに登録します。
そしてロボットアプリケーションを作成します。
名前・ソフトウェアスイート名・バージョン・ARM64ソースファイルを入力します。
フリート・ロボットアプリケーションを入力します。
バージョン(新規作成)を新規作成します。
パッケージ名・launchファイルを入力します。ここで作成を押すとすぐにデプロイが始まるので注意が必要です。
作成すると下記の状態になり進行中になります。成功が表示されればリモートデプロイ完了です。
Jetson TX2でリモートデプロイしたROSプロジェクトが動いているか確認する
Jetson TX2にSSHで入り、rostopicをこれまでどおりechoすれば動いていることが確認できます。
まとめ
全部まとめてみると難しいようにみえますが、ひとつひとつみていくとそこまで複雑なことはやっていないと思います。ただドキュメントが不足していたり、そもそも記述がないこともあるので、その時はソースコードを読んでgithubのissueを確認するのが最も正確で近道です。
これでRoboMakerとCircleCIをつかえば大量のデバイスのデプロイを簡単に管理することができるようになりました。RoboMakerは、今後リソースの監視やシミュレーションまわりだったりどんどん機能がアップデートされていく予定なので楽しみですね!