こんにちは、ティアフォーでオープンソースの自動運転システム「Autoware」の開発をしている三宅です。現在は主に開発環境周りを担当しており、効率的な開発プラットフォームを目指して日々改善を重ねています。
前回のブログ記事では、Autowareのコンセプトやリポジトリ構成について説明しました。
今回は実装面の詳細として、以下のような様々な自動化の仕組みについて解説します。
AutowareはGitHub上でソースコードを管理しているため、GitHub公式のGitHub Actionsを使用しています。Publicリポジトリに対しては無料で使えるので、オープンソースのAutowareとの相性が良いです。
各リポジトリの.github/workflows/*.yamlにワークフローファイルを置くことで、指定したタイミングでワークフローを実行してくれるという仕組みです。ユーザーの要件に合わせて、柔軟な構成が可能です。詳細は、GitHub Actions Documentationをご参照ください。
GitHub Actionsは、登場初期は不便に感じることも若干ありましたが、非常に速いサイクルで新機能の追加や改善がされてきており、今ではとても便利に使えます。
ちなみに、今後の開発ロードマップはGitHub public roadmapで確認できます。機能のリリース後はGitHub BlogやGitHub Changelogでアナウンスされるので、最新機能を追いかけたい方は、これらのRSSを購読しておくと良いと思います。
開発環境構築の自動化は、単純に手間を減らすことにも繋がりますが、例えば「自分の環境だと動くのに、○○さんの環境だと動かない」のような問題を防ぐためにも重要です。
Installation - Autoware Documentationにある通り、Autowareでは2種類のインストール方法をサポートしています。それぞれ詳細を説明していきます。
ローカル開発環境は、1つ1つコマンドをコピー&ペーストで実行して構築もできますが、手間がかかりますしミスも出やすいです。AutowareではAnsibleにより、依存ソフトウェアをインストールする作業を自動化しています。
具体的には、まずは様々なインストール手順をRoleとして定義します。例えば、ROS 2のRoleはansible/roles/ros2/tasks/main.yamlです。
そして、Playbookファイルのansible/playbooks/universe.yamlにインストールしたいRoleの一覧を書くと、ansible-playbookコマンドでインストールできます。
実際には、簡単なコマンドの実行で済むようにシェルスクリプトでラップしているので、setup-dev-env.shを実行するだけで、依存ソフトウェアが全てインストールされます。
ちなみに、RoleやPlaybookの定義はAnsible Galaxyに準拠しているため、Autowareの外部からも参照できます。例えば、以下のコマンドを実行すると、Ubuntu 22.04にROS 2 Humbleがインストールできます。
cat << EOS > ansible-galaxy-requirements.yaml
collections:
- name: https://github.com/autowarefoundation/autoware.git#/ansible
type: git
version: main
EOS
ansible-galaxy collection install -f -r ansible-galaxy-requirements.yaml
ansible localhost --ask-become-pass -m include_role -a name=autoware.dev_env.ros2 -e rosdistro=humble
これにより、Autowareをカスタマイズする際に、必ずしも autowarefoundation/autowareをForkしなくてもAutowareの依存ソフトウェアをインストールするRoleを参照できます。つまり、Autowareのカスタマイズ方法の選択肢が増えて、拡張性の高いプラットフォームになります。
Dockerは、必要な依存ソフトウェアを閉じ込めた、軽量な仮想環境(=コンテナ)を作ることができます。Dockerさえインストール済みなら同じ環境の再現が容易なので、セットアップもお手軽ですし、他人との環境の差異による問題も発生しづらいです。
ただし、依存ソフトウェアは時折変化するため、定期的にDockerイメージの作り直しが必要です。Autowareではこの作業を自動化しています。
その仕組みは、まずはDockerコンテナの元となるイメージをDockerfileとして定義します。1つのDockerfileから複数(ビルド前と後)のDockerイメージを生成できるようにDocker Buildx Bakeを使用しているため、そのBakeの定義用にdocker-bake.hclも作成します。
これらを、docker-build-and-push-main.yamlのワークフローで定期的にビルドし、GitHub Container Registryにアップロードしています。
若干細かい話になりますが、もう少し詳細部分も解説します。
Autowareでは、AMD64 (x86_64) だけでなく、ARM64もサポートする必要があります。しかし、無料で使えるGitHub-hosted runnersではARM64マシンは現在サポートされていません。
Buildxで、QEMUによるマルチアーキテクチャビルドはサポートされていますが、残念ながら処理時間のオーバーヘッドが大きく、Autowareをビルドするとワークフローの制限時間を超過してしまって使えませんでした。
そこで、docker-build-and-push-main-self-hosted.yamlという別ワークフローを用意して、Amazon Web Services (AWS) 上に構築したSelf-hosted runnersを使用してビルドしています。
しかしこのままだと、CPUアーキテクチャ毎に別々でDockerイメージを生成している関係で、タグの指定方法がautoware:latest-amd64やautoware:latest-arm64になってしまいます。これだと、CPUアーキテクチャを意識する必要があって使いづらいです。
そのため、update-docker-manifest.yamlのワークフローで、docker manifestコマンドによりautoware:latest-amd64とautoware:latest-arm64を結合して、autoware:latestを生成しています。
ソフトウェアエンジニアリングにおいては、日々変化する環境に対するソフトウェア品質の維持が求められます。変化には、PR等によって実際に管理対象のソフトウェア構成が変化する「内部要因」と、依存ソフトウェアやプラットフォームが変化する「外部要因」があります。
Autowareでは様々なCIを導入して、変化に対する品質管理を自動化しています。Pull request guidelines - Autoware Documentationに説明があるように、各種CIがPassしないとPRをマージできないようになっています。
以下でそれぞれ解説します。
PRの変更内容自体を検査する前に、前提条件として、フォーマットを揃えておけるとスムーズです。Autowareでは主に以下3点のフォーマットを確認しています。
DCOとは、簡単に言うと「コミッターが、コミットの差分をプロジェクトに提供する権利を持っているか」を宣言する署名です。GitHub Appをインストールするだけで使用できます。
「万が一ライセンス問題が発生した場合に、責任の所在を明確にするための手続き」という理解で良いと思います。オープンソースプロジェクトを健全に運用するためには必須の手続きです。
Conventional Commitsは、コミットメッセージのフォーマットに関する規約です。人やプログラムが、コミットの意味を容易に理解できるように設計されています。
変更の種類(機能追加やバグ修正)や、変更のスコープ(対象機能)をコミットメッセージで示す必要があり、自然と適切な単位でPRが作成されるようになるため、レビューもやりやすくなります。
amannn/action-semantic-pull-requestを使用すると、PRのタイトル(=Squashマージする場合のコミットメッセージ)やコミットメッセージの規約違反を自動的に検知できます。
基本的なことですが、PRの品質向上のためには、適切なPRの説明を書き、適切にテストやレビューが行えるようにすることが重要です。説明欄を毎回1から作成していると、記載も大変ですし、フォーマットの統一感も無くなってしまいます。
PRのテンプレートを作成しておくと、PR作成時に自動的にフォームをテンプレートで埋めてくれます。以下は、Autowareのテンプレートです。PRの種別や大きさによってフォーマットを使い分けるために、複数のテンプレートを用意しています。
ちなみに、GitHub公式では複数のPRテンプレートから選択する機能は上手くサポートされていないため、ちょっとした工夫を入れています。具体的には、PULL_REQUEST_TEMPLATE.mdをデフォルトのテンプレートとして用意しておき、その中に各テンプレートのリンクを記載しておくことで、PR作成時のPreviewボタンからテンプレートを選択できるようにしています。
本格的なテストをする前に、ソースコードの品質として最初に確認すべきポイントは、ソースコードのフォーマットです。フォーマットが直に品質に影響することはありませんが、レビュー効率などを通して間接的に大きな恩恵を受けられます。
レビュアーが慣れていない記法に対して適切なレビューをするのは困難なため、プロジェクト内で共通の基準を持ち、全員がそれに合わせることが重要です。そして可能であれば、その基準に合うように自動でソースコードを修正できることが好ましいです。手間の削減になりますし、機械的に指摘をすることで様々なコミュニケーショントラブルも回避しやすくなります。
フォーマット修正に一貫性を持たせることも重要です。一貫性が無いと、例えば誤って改行を入れると異なるフォーマットに修正され、不要なdiffが出てしまいノイズになってしまいます。したがって、C++であればClangFormat、PythonであればBlackのような、強い一貫性を持つフォーマッタを使用するべきです。
また、Lintもお手軽で効果が大きいです。ありがちな単純ミスを防いでくれたり、ベストプラクティスに従った記法に書き換えることで、可読性や処理効率を向上させてくれたりします。
例えば、C++のLinterではClang-Tidyが非常に優秀です。コンパイルオプションの情報を含んだcompile_commands.jsonが必要なので一度ソースコードをビルドする必要がありますが、豊富な項目を検知できます。Autowareでは、検知された項目を適用しやすいように、以下のように修正内容のSuggestion付きでコメントを付けてくれるGitHub Actionsのワークフローを使用しています。
Clang-Tidyの修正Suggestionの例
これらの自動フォーマットやLintの実行プラットフォームとして、Autowareの大部分で活用しているのが、pre-commitです。pre-commitは、.pre-commit-config.yamlに様々なpre-commit hooksを記載しておくと、 pre-commit run -aで各hookを実行してくれます。pre-commit installによってリポジトリにインストールしていれば、git commit時に自動でpre-commitを走らせることも可能です。
このpre-commit hooksは、公式のpre-commit/pre-commit-hooksに加えて、サードパーティの便利なものが多数公開されています。例えばAutowareでは、C++用にpre-commit/mirrors-clang-formatやcpplint/cpplintを使用しています。
自分の用途に合ったpre-commit hooksが見つからない場合は、簡単に自作することもできます。ティアフォーでも、ROS関係のpre-commit hooksもいくつか開発しています。例えば、package.xmlのdependタグを並び替えるものや、インクルードガードのマクロ名を自動で修正するものがあります。詳細は、tier4/pre-commit-hooks-rosをご覧ください。
pre-commitは、CI上でも簡単に実行ができるので便利です。使い方は、pre-commit.ciをリポジトリにインストールするだけです。
CI上でもpre-commitを実行する必要がある理由は、開発者の手元でだけでチェックしていると、もし開発者がpre-commitの実行を忘れてしまうと、規約に合わないソースコードがマージされてしまう可能性があるためです。PRを出す前に手元でチェックすることも大切ですが、万が一に備えてCIでガードしておくと安心です。
ちなみに、自動フォーマットやLintを行う別のプラットフォームにはament_lintもあります。ament_lintは、ROSパッケージのpackage.xmlにFormatterやLinterパッケージの依存を書いておくと、パッケージのテスト実行時に、対応するツールを実行してくれます。
しかし、例えば1行のフォーマットミスが、30分かけたビルドとテストの際に判明したとしても、遅くて効率が悪いです。pre-commitであれば、ビルド前の早期に問題の発見と修正ができて効率的なので、Autowareでは可能な限りpre-commitを使うようにしています。
AutowareではPRに対して、自動フォーマットやLintに加えて、様々なテストを行っています。
これらのテストは、効率化のために、基本はPRの差分に対して行っています。例えば、1行の変更に対してシステム全体をテストすることは、無駄であるケースがほとんどだからです。差分とは言っても、厳密に影響範囲を割り出すことは難しいので、現在は「変更されたファイルのあるパッケージと、その依存関係」を対象にしています。
依存関係とは、単に変更されたパッケージの親パッケージを見れば良いわけではなく、依存している子パッケージも含みます。ライブラリパッケージの親パッケージには、そのライブラリのユーザーが居ないためです。そのような依存パッケージは、ROS 2パッケージのビルドツールであるcolconの--packages-above-and-dependenciesというオプションで取得できます。
PRのマージ後には、差分ではなく全体のテストを行っています。理由は、以下の2つです。
数年間このような運用をしていますが、「差分のテストは通ったが全体のテストは通らなかった」という記憶は特に無いので、差分パッケージに対するテストは十分に実用的だと考えています。
もちろん必要に応じて、マージ前のタイミングで全体に対するテストを実行することも可能で、大規模なPRに対しては実施することがあります。これは、GitHub Actionsのworkflow_dispatchイベントを使うと実現できます。便利なので、様々なワークフローに設定しておくと良いですね。
テストは、PR単位だけではなく、定期的にも行います。定期テストの目的は主に2つあります。
自動運転システムのテストは、大規模で複雑なものが多いです。実行に数十時間かかるテストや、自動化が難しいテストも存在します。
例えば、シミュレータによる網羅的なシナリオテストや、実機を使用したテストを、全てのPRに対して毎回行うのはコストが高すぎるので、定期テストとして実施しています。
システムの依存関係は、基本的にはプロジェクト内でバージョン固定してコントロール可能にしておくべきですが、それが難しいケースもあります。
例えば、AutowareはROSに依存していますが、ROSでは月に一度程度の頻度で、一部パッケージのバージョンを更新することがあります。ROS DiscourseのPackaging and Release Managementというタグを見ると、そのSyncの作業記録が見られます。基本的にはインタフェースを壊さないようなソフトウェアがリリースされますが、稀に破壊的変更が混入してしまうこともあります。
ROS以外の事例としては、最近では以下のようなものに遭遇しました。
どのような不測の事態が起きるか分からないので、万が一に備えた定期テストは必須です。
常に品質の高いソフトウェアを開発できていたとしても、全く何も壊さないように開発をすることは困難です。時には、レガシーなインタフェースを廃止するために、互換性を打ち切る破壊的な変更も必要になります。
その時に重要なのがバージョン管理です。古いインタフェースを使い続けたい人は古いバージョンを使い、新しいバージョンに切り替えたい人は新しいバージョンを使う、のような使い分けが可能になります。
単にバージョンを区別するだけであれば、Gitのタグを付与すれば十分ですが、GitHubのリリース機能を併用してそのバージョンの変更点を説明したリリースノートを作成すると、分かりやすくなります。
このリリースノートの自動生成はGitHub公式でもサポートされていますが、変更種別の分類のためにはPRに対するラベル管理が必要なので、少し手間がかかります。
メンテナンスコストを最小にするため、Autowareではgit-cliffを採用し、以下のような自動化をしています。
Draftをスキップして完全に自動化することもできますが、再確認や承認の意味も込めて、最後は手動の工程を入れています。
以下は、作成されたリリースノートの例です。変更点が分類されていて、分かりやすくなっています。
GitHub Releaseの例
AutowareにはドキュメントサイトのAutoware Documentationがありますが、ここでもいくつか自動化を取り入れています。
まずは前提知識としてドキュメントサイトの仕組みを簡単に説明し、その後に自動化の仕組みを解説します。
ドキュメントサイト作成のフレームワークには、Material for MkDocsを使用しています。ほとんど設定不要で、Markdownファイルを置くだでモダンなドキュメントサイトを構築できて、非常に便利です。mikeというツールを併用すると、GitHub Pages上で複数バージョンのドキュメントを簡単に公開することもできます。
行っている自動化は2つあります。
PRのレビュー効率化のために、「このPRのマージ後の見栄えはどうなるか」を、マージ前に手軽に確認できると便利です。
プレビューサイトが無い場合は、開発者が手元でPRのブランチをチェックアウトし、mkdocs serve等を実行してローカルでドキュメントを生成する必要があるため、手間がかかります。
プレビューサイトの作成は、mike deployコマンドを使うだけで実現できます。gh-pagesブランチ内にバージョン毎にディレクトリを分けてドキュメントを作成してくれるため、プレビューサイトもそのバージョンの1つとして扱っています。実際のブランチに置かれているファイルを見ると、仕組みが理解できると思います。
PRのマージ後は、プレビューサイトは基本的には不要になります。必ずしも削除する必要はありませんが、放置しすぎるとドキュメントのバージョン一覧が煩雑になってしまうので、定期的なクリーンアップは必要です。
バージョン情報はversions.jsonに格納されており、mike listコマンドで取得できます。このドキュメントのバージョンと、GitHub APIで取得できるPRの情報を比較して、閉じられたPRに対応するドキュメントのバージョンが見つかった場合に、mike deleteコマンドで削除しています。
Autowareには数十以上のリポジトリが存在しますが、複数のリポジトリでCIワークフローの設定を共通化したいことが頻繁にあります。
この際、仮に管理の自動化を何もしない場合は、ワークフローの更新時に多数のリポジトリに手動でPRを出す必要があり、手間がかかります。shepherdのような、複数リポジトリの操作を一括で行えるツールもありますが、今回のケースでは、毎回定義ファイルを用意するのは面倒です。
上手く自動化するためには、どうすれば良いでしょうか?以下2点に分けて、それぞれ考えてみたいと思います。
各ワークフローに処理をべた書きすると共通化が難しくなるのは、容易に想像できます。ソースコードを至るところにコピペするのと同じです。
共通化のためには、共通の関数やクラスがあると良さそうです。GitHub Actionsの場合は、Reusable WorkflowやComposite Actionを活用すると、ワークフローの処理が共通化できます。
Autowareでは、autowarefoundation/autoware-github-actionsに様々なReusable WorkflowやComposite Actionを作成し、各リポジトリのワークフローから呼び出しています。各リポジトリに詳細なロジックを書く必要が無くなり、メンテナンスの手間を減らせます。
共通化の残課題は以下の通りです。
これらの課題を解決するために、sync-filesというComposite Actionを作りました。以下の流れでファイルを同期できます。
ちなみに、ファイル同期の際は、完全にファイルを同一の状態に同期したいとは限らないので、少し応用的な機能もサポートしています。
例えば、autoware-documentationとopen-ad-kit-docsでは、MkDocsプラグインの設定を共通化するために、サイト名等を除いて同一設定のmkdocs.yamlを使用したい要件があります。
そのため、open-ad-kit-docs/.github/sync-files.yamlの設定では、autoware-documentation/mkdocs.yamlに対して、サイト名やURLの文字列置換をしてからファイルを同期しています。
本記事では、Autowareの様々な自動化の仕組みを紹介しました。自分で言うことではないですが、結構頑張って自動化に取り組んでいるのではないかと思います。
もし何か使えそうな知識を見つけたら、ぜひご自身の開発に取り入れてみてください!
ところで、Autowareの開発やメンテナンスは、メンバー企業や有志の善意によって支えられております。コントリビューター達の励みになりますので、リポジトリのスターも是非よろしくお願いします!
https://github.com/autowarefoundation/autowareを開き、Sign inボタンからご自身のGitHubアカウントにログイン後、Starボタンを押せばスター完了です。GitHubアカウントをお持ちでない方も、Sign upボタンから無料アカウントをすぐに作成できます。
オープンソースのソフトウェアを一緒に開発していきませんか?
ティアフォーでは、「自動運転の民主化」というビジョンに共感を持ち、自らそれを実現する意欲に満ち溢れた新しい仲間を募集しています。
Media Contact
pr@tier4.jp
Share the post
LinkedIn | X | Facebook | Instagram