AWS RoboMakerをROS 2 FoxyとGazero 11を使って試してみる

本記事は、[KDDIアジャイル開発センター Engineer & Designer Advent Calendar 2022](https://qiita.com/advent-calendar/2022/kag)の20日目の記事となります。

# 本記事で紹介すること
**AWS RoboMaker**を**ROS 2 Foxy**と**Gazero 11**の環境で試します。
サンプルアプリケーションとして[AWS RoboMaker Sample Application - Hello World](https://github.com/aws-robotics/aws-robomaker-sample-application-helloworld)を使用します。

# AWS RoboMakerについて
[AWS公式サイト](https://aws.amazon.com/jp/robomaker/)の説明によると、
> AWS RoboMaker は、ロボットデベロッパーがインフラストラクチャを管理することなく、シミュレーションの実行、スケール、自動化を可能にするクラウドベースのシミュレーションサービスです。

とあります。

ロボット開発においてシミュレーション環境を用意しようとすると、以下のような問題に当たると思います。
- 環境構築が大変
- 対応するOSが限定されている
- それなりのマシンスペックが必要

このあたりを解決してくれるのがAWS RoboMakerです。
AWS RoboMakerには、以下のメリットがあります。
- 開発環境のインフラ管理が不要
- シミュレーションテスト・デプロイの自動化
- AWSの他のサービスとの連携が容易

そんなAWS RoboMakerでは**ROS**という技術が基盤になっています。
次項ではROSについて簡単に説明します。

# ROSについて
[ROS - Robot Operating System](https://www.ros.org/)はOSと書いていますが、厳密にはOSではなく、ロボット開発のためのミドルウェアおよびプラットフォームです。
[*ROSとは何か？*](https://answers.ros.org/question/12230/what-is-ros-exactly-middleware-framework-operating-system/) という質問に対して、公式で以下のように説明されています。
> ROS = plumbing + tools + capabilities + ecosystem

<dl>
    <dt>plumbing: 通信</dt>
    <dd>Pub/Sub通信</dd>
    <dt>tools: ツール群</dt>
    <dd>設定・起動・監視・デバッグ・可視化・ログ取得・テスト・停止を行う広範囲にわたるツール</dd>
    <dt>capabilities: 機能群</dt>
    <dd>多様なライブラリ群</dd>
    <dt>ecosystem: エコシステム</dt>
    <dd>ROSパッケージの共有</dd>
</dl>

詳しくは以下の記事が参考になります。
- [ROS（Robot Operating System）とは？内容と利用するメリットについて解説](https://robokaru.jp/fundamental-knowledge/ros/#ROS%E3%81%A8%E3%81%AF)

このような特徴から、ROSは多くのロボット開発の現場で利用されています。

## ROS 1 と ROS 2
ROSは元々研究開発用に作られたもののため、ネットワークの品質やコンピュータなどがかなり理想的である必要がありました。
そのため、実製品に適用するのが難しいという問題点を抱えていました。

そこで新たに開発がスタートしたのが**ROS 2**です。
**ROS 2**では不安定なネットワークにも対応できたり、組み込みコンピュータにも載せられるように工夫がされています。
また、複数台のロボットを制御できるようにもなりました。

**ROS 1**は現行の[Noetic](http://wiki.ros.org/noetic)を最後に新たな開発は終了しており、これからは**ROS 2**がメインになっていきます。

そこで本記事では、[ROS 2 Foxy](https://docs.ros.org/en/foxy/index.html)を使って、AWS RoboMakerを試していきます。

**参考になる記事**
- [ROSとは｜特長・ROS1とROS2の違い・活用事例・動かせるロボット](https://macro-send.com/blog/ros)

# 試してみる
以下を参考にハンズオンを進めていきます。
- [AWS RoboMaker Developer Guide](https://docs.aws.amazon.com/ja_jp/robomaker/latest/dg/chapter-welcome.html)


環境は以下の通りです。
- ROS 2 Foxy
- Gazebo 11

また以下の3つが使える状態である必要があります。
- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
- [Docker](https://docs.docker.com/get-docker/)
- [VCS Import Tool](http://wiki.ros.org/vcstool)
- [Python3](https://www.python.org/downloads/)

## アプリケーションのDockerイメージをビルド
まずロボットアプリケーションのDockerイメージをビルドします。
[AWS RoboMaker Sample Application - Hello World](https://github.com/aws-robotics/aws-robomaker-sample-application-helloworld)をクローンしてきます。
ブランチは`ros2_foxy`を指定します。
続いて依存パッケージを`vcs`でインポートします。
```shell
git clone https://github.com/aws-robotics/aws-robomaker-sample-application-helloworld.git -b ros2_foxy helloworld 
cd helloworld
vcs import robot_ws < robot_ws/.rosinstall
vcs import simulation_ws < simulation_ws/.rosinstall
```

次に、`helloworld`ディレクトリ内に`Dockerfile`と`entrypoint.sh`を作成し、以下の内容をコピペします。
```Dockerfile:Dockerfile
# ======== ROS/Colcon Dockerfile ========
# This sample Dockerfile will build a Docker image for AWS RoboMaker 
# in any ROS workspace where all of the dependencies are managed by rosdep.
# 
# Adapt the file below to include your additional dependencies/configuration 
# outside of rosdep.
# =======================================

# ==== Arguments ====
# Override the below arguments to match your application configuration.
# ===================

# ROS Distribution (ex: melodic, foxy, etc.)
ARG ROS_DISTRO=foxy
# Application Name (ex: helloworld)
ARG APP_NAME=robomaker_app
# Path to workspace directory on the host (ex: ./robot_ws)
ARG LOCAL_WS_DIR=workspace
# User to create and use (default: robomaker)
ARG USERNAME=robomaker
# The gazebo version to use if applicable (ex: gazebo-9, gazebo-11)
ARG GAZEBO_VERSION=gazebo-11
# Where to store the built application in the runtime image.
ARG IMAGE_WS_DIR=/home/$USERNAME/workspace

# ======== ROS Build Stages ========
# ${ROS_DISTRO}-ros-base
#   -> ros-robomaker-base 
#      -> ros-robomaker-application-base
#         -> ros-robomaker-build-stage
#         -> ros-robomaker-app-runtime-image
# ==================================

# ==== ROS Base Image ============
# If running in production, you may choose to build the ROS base image 
# from the source instruction-set to prevent impact from upstream changes.
# ARG UBUNTU_DISTRO=focal
# FROM public.ecr.aws/lts/ubuntu:${UBUNTU_DISTRO} as ros-base
# Instruction for each ROS release maintained by OSRF can be found here: 
# https://github.com/osrf/docker_images
# ==================================

# ==== Build Stage with AWS RoboMaker Dependencies ====
# This stage creates the robomaker user and installs dependencies required 
# to run applications in RoboMaker.
# ==================================

FROM public.ecr.aws/docker/library/ros:${ROS_DISTRO}-ros-base AS ros-robomaker-base
ARG USERNAME
ARG IMAGE_WS_DIR

RUN apt-get clean
RUN apt-get update && apt-get install -y \
   lsb  \
   unzip \
   wget \
   curl \
   xterm \
   python3-colcon-common-extensions \
   devilspie \
   xfce4-terminal

RUN groupadd $USERNAME && \
   useradd -ms /bin/bash -g $USERNAME $USERNAME && \
   sh -c 'echo "$USERNAME ALL=(root) NOPASSWD:ALL" >> /etc/sudoers'
   
USER $USERNAME
WORKDIR /home/$USERNAME

RUN mkdir -p $IMAGE_WS_DIR

# ==== ROS Application Base ====
# This section installs exec dependencies for your ROS application.
# Note: Make sure you have defined 'exec' and 'build' dependencies correctly 
# in your package.xml files.
# ========================================
FROM ros-robomaker-base as ros-robomaker-application-base
ARG LOCAL_WS_DIR
ARG IMAGE_WS_DIR
ARG ROS_DISTRO
ARG USERNAME

WORKDIR $IMAGE_WS_DIR
COPY --chown=$USERNAME:$USERNAME $LOCAL_WS_DIR/src $IMAGE_WS_DIR/src

RUN sudo apt update && \ 
   rosdep update && \
   rosdep fix-permissions

# Note: This will install all dependencies. 
# You could further optimize this by only defining the exec dependencies. 
# Then, install the build dependencies in the build image.
RUN rosdep install --from-paths src --ignore-src -r -y

# ==== ROS Workspace Build Stage ==== 
# In this stage, we will install copy source files, install build dependencies
# and run a build. 
# ===================================
FROM ros-robomaker-application-base AS ros-robomaker-build-stage
LABEL build_step="${APP_NAME}Workspace_Build"
ARG APP_NAME
ARG LOCAL_WS_DIR
ARG IMAGE_WS_DIR

RUN  . /opt/ros/$ROS_DISTRO/setup.sh && \
   colcon build \
    --install-base $IMAGE_WS_DIR/$APP_NAME
    
# ==== ROS Robot Runtime Image ====
# In the final stage, we will copy the staged install directory to the runtime 
# image.
# =================================
FROM ros-robomaker-application-base AS ros-robomaker-app-runtime-image
ARG APP_NAME
ARG USERNAME
ARG GAZEBO_VERSION

ENV USERNAME=$USERNAME
ENV APP_NAME=$APP_NAME
ENV GAZEBO_VERSION=$GAZEBO_VERSION

RUN rm -rf $IMAGE_WS_DIR/src

COPY --from=ros-robomaker-build-stage $IMAGE_WS_DIR/$APP_NAME $IMAGE_WS_DIR/$APP_NAME

# Add the application source file to the entrypoint.
WORKDIR /
COPY entrypoint.sh /entrypoint.sh
RUN sudo chmod +x /entrypoint.sh && \
   sudo chown -R $USERNAME /entrypoint.sh && \
   sudo chown -R $USERNAME $IMAGE_WS_DIR/$APP_NAME
   
ENTRYPOINT ["/entrypoint.sh"]
```
```shell:entrypoint.sh
#!/bin/bash
set -e
source "/home/$USERNAME/workspace/$APP_NAME/setup.bash"
if [[ -f "/usr/share/$GAZEBO_VERSION/setup.sh" ]]
then
   source /usr/share/$GAZEBO_VERSION/setup.sh
fi
printenv
exec "${@:1}"
```

続いて、Dockerイメージをビルドします。
まずロボットアプリケーションをビルドします。
```shell
DOCKER_BUILDKIT=1 docker build . \
 --build-arg ROS_DISTRO=foxy \
 --build-arg LOCAL_WS_DIR=./robot_ws \
 --build-arg APP_NAME=helloworld-robot-app \
 -t robomaker-helloworld-robot-app
```
次にシミュレーション用のアプリケーションをビルドします。
```shell
DOCKER_BUILDKIT=1 docker build . \
 --build-arg GAZEBO_VERSION=gazebo-11 \
 --build-arg ROS_DISTRO=foxy \
 --build-arg LOCAL_WS_DIR=./simulation_ws \
 --build-arg APP_NAME=helloworld-sim-app \
 -t robomaker-helloworld-sim-app
```


## DockerイメージをECRにプッシュ
次にビルドしたアプリケーションのDockerイメージをECRにプッシュします。
環境変数をセットします。
```shell
export robotapp=robomaker-helloworld-robot-app
export simapp=robomaker-helloworld-sim-app
export account=<AWS ACCOUNT NUMBER>
export region=<AWS REGION>
export ecruri=$account.dkr.ecr.$region.amazonaws.com
```
ECRにリポジトリを作成します。
```shell
aws ecr get-login-password --region $region | docker login --username AWS --password-stdin $ecruri
aws ecr create-repository --repository-name $robotapp
aws ecr create-repository --repository-name $simapp
```
先程ビルドしたDockerイメージにタグを付けます。
```shell
docker tag $robotapp $ecruri/$robotapp
docker tag $simapp $ecruri/$simapp
```
DockerイメージをECRにプッシュします。
```shell
docker push $ecruri/$robotapp
docker push $ecruri/$simapp
```

## アプリケーションの作成
ロボットアプリケーションを作成します。
```shell
aws robomaker create-robot-application \
--name my-robot-app \
--robot-software-suite name=General \
--environment uri=$ecruri/$robotapp\:latest
```
シミュレーションアプリケーションを作成します。
```shell
aws robomaker create-simulation-application \ 
--name my-sim-app \ 
--simulation-software-suite name=SimulationRuntime \
--robot-software-suite name=General \ 
--environment uri=$ecruri/$simapp\:latest
```

## シミュレーションジョブの作成
いよいよシミュレーションを実行していきます。
ここからはAWSコンソールを使います。
AWS RoboMakerのコンソールに移動し、`シミュレーション実行>シミュレーションジョブ`から`シミュレーションジョブの作成`を押します。
以下のように設定しましょう。

![スクリーンショット 2022-12-20 1.27.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/bcba23db-f4ac-a005-51f7-cfeb6d64a01f.png)
![スクリーンショット 2022-12-20 1.21.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/86bf4180-ce76-b9bd-6891-a3edddfd374b.png)

`次へ`を押します。
続いて、先程CLIで作成したロボットアプリケーションを選択します。

起動コマンドは以下のようにします。

`/bin/bash, -c, ros2 launch hello_world_robot rotate.launch.py`

各コマンドはダブルクオートなどで囲まず、カンマで区切るのがミソです。
また、単に`ros2 launch hello_world_robot rotate.launch.py`と書くのでもうまくできません。
ここにつまづきました…

![スクリーンショット 2022-12-20 1.21.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/9467a03d-5d61-7a89-6310-24a1ad046b28.png)
![スクリーンショット 2022-12-20 1.22.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/3f5b40e3-c584-0ea2-14e2-34165e135cb6.png)

ロボットアプリケーションツールは以下のようにします。
![スクリーンショット 2022-12-20 1.22.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/33ff687c-8674-5758-7a5c-8a5637995224.png)

同様に、シミュレーションアプリケーションも設定します。
起動コマンドは`/bin/bash, -c, ros2 launch hello_world_simulation empty_world.launch.py`です。
![スクリーンショット 2022-12-20 1.22.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/4fbbe54f-6538-d06c-65f4-1c5e11f8b96a.png)
![スクリーンショット 2022-12-20 1.22.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/5232e92c-d929-61dc-6681-39dc994b34ca.png)
![スクリーンショット 2022-12-20 1.22.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/f603c1fa-91d7-c4bd-ca66-9455f44861ad.png)

## シミュレーションの実行
作成が完了し、以下の画面が出てきたらシミュレーションが実行されています。
**gzclient**の`接続`ボタンをクリックしましょう。
![スクリーンショット 2022-12-20 1.28.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/a3e9995f-fec3-cf5c-8397-36de53c8e65b.png)

すると、以下の**Gazebo**の画面が表示されると思います。(表示まで時間がかかると思います)
少し操作方法にクセがありますが、クリックしながらカーソルを動かしたり、シフトキーを押しながら操作すると、カメラの位置を変えられます。
すると真ん中で機体ががグルグル回っているのが確認できるかと思います。
![スクリーンショット 2022-12-20 1.49.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/408546/a3825bad-381b-ff46-6a5d-2b7d0f14cee6.png)


トピックを確認しましょう。
**ロボットアプリケーションツール**の**robot-terminal**の`接続`ボタンを押すとターミナルが開きます。
ターミナルで以下のコマンドを実行します。
`source /home/robomaker/workspace/helloworld-robot-app/setup.bash`
そして`ros2 topic list`を打つと、トピック一覧が見れます。
`/cmd_vel`は機体の移動速度を制御するトピックです。
`ros2 topic echo /cmd_vel`で現在publishされている`/cmd_vel`の中身を見てみると、
```
---
linear:
  x: 0.0
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.1
---
```
となっていると思います。
`angular.z`のみ速度が`0.1`になっているため、機体がその場で回転運動をしていることがわかります。

以上で**AWS RoboMaker**でシミュレーションを起動することができました。

# まとめ
今回は[AWS RoboMaker Developer Guide](https://docs.aws.amazon.com/ja_jp/robomaker/latest/dg/chapter-welcome.html)に沿って、**ROS 2 Foxy**バージョンで**AWS RoboMaker**でシミュレーションの起動までを試してみました。
シミュレーション環境のカスタマイズや、CI/CDなども今後試していければと思います。