ブログ:
組み込みLinuxシステム向け継続的インテグレーション

2021年9月8日水曜日
Savoir-faire Linux

Savoir-faire Linux

はじめに

継続的インテグレーション(CI)とは、各チームの作業を共有レポジトリーに常時、頻繁に統合させる開発作業の慣行です。コードの品質、信頼性と新たな不具合がないことは、各変更の統合前に自動的に行われるビルドとテストで確認します。

ソフトウェアアプリケーションとは異なり、CIを組み込みシステムで実践する場合には考慮しなければならない事柄がいくつかあります。
  1. 同じ周辺機器とCPUアーキテクチャーを備えたマシンではないため、CIを実行しているマシンではテストを実行できません。最小限のハードウェアインターフェースを持つシステムであれば仮想マシンを利用することも可能ですが、ハードウェアが複雑な場合や接続性が多い場合には限界があります。
  2. テスト対象デバイス(DUT)を本番環境イメージにできる限り近い状態に保持するため、テストは非干渉的でなければなりません。つまり、余分な環境依存性を厳密に除外し、軽量なテストツールを利用する必要があります。
  3. テストでは、単体テストの実施によりアプリケーションレイヤが想定どおりに動作するのを確認するだけでなく、システムレベルのテストスイートを用いてオペレーティングシステム自体とハードウェアおよびファームウェアの機能も確かめる必要があります。
一般的に、組み込みシステムのCIパイプラインには最低で次の段階が含まれます。
  1. コードのフェッチ:デスクトップソフトウェアの場合と同様、まずはコードを取得する必要があります。このプロセスは開発者が新しい変更を作成したときに自動トリガできます。
  2. ビルド:OSとアプリケーションを含む全システムのイメージを再コンパイルする必要があります。このステップは、コード品質の確認を行うよう静的分析ツールを利用して拡張することもでき、ビルドキャッシュの活用により加速化が可能な場合もあります。
  3. ターゲットへの展開:新しくコンパイルされたイメージをターゲットに展開する必要があります。このステップにはディスクの消去、ネットワークからのブート設定、ターゲットの再起動などが含まれます。
  4. 構成とテスト:テストを実施する前に、そのほかの環境構成が必要な場合があります。
  5. コードの統合:コードのテストが完了したら、別のブランチやレポジトリへの統合または別の形でのデリバリーが可能です。
CI pipeline for an embedded system

ハードウェアの依存性とリソースの制約があることから、組み込みの世界でそのまま利用できるCIソリューションを見つけるのは困難です。この記事では、セルフホストで幅広い範囲の組み込みシナリオ向けに簡単にカスタマイズできる、組み込みシステム向けのオープンソースCIプロジェクトを紹介します。

このアプローチを検証するため、i.MX 6搭載Toradex Apalis評価ボードを用いて実際のケースシナリオを展開しました。

Toradex Apalis Evaluation Board

さらに、QEMUターゲットマシン向けの対応パイプラインも開発しました。これはCIチェーンを実装し、物理的なボードなしで同じテストを実行します。

CIの展開

Jenkinsは、幅広く利用されているオートメーションサーバーで、CIパイプラインでのソースコードのビルド、テストと展開を可能にします。SSH接続、パイプライン進捗とテスト結果の可視化といった特定機能を処理するためのインストール可能プラグインが含まれています。

再現性を実現し構成を容易にするため、JenkinsサーバーはDockerコンテナ上に展開しました。ソースコードと利用方法の手順は、https://github.com/savoirfairelinux/base_ciで入手できます。

また、この概念実証で利用したJenkinsパイプラインはhttps://github.com/savoirfairelinux/base_ci_pipelineで入手できます。

このパイプラインは、同様のプロジェクトのテンプレートとしても利用できます。

CI deployment pipeline
ビルドのオートメーション

この例では、Linuxディストリビューションの取得にYocto projectを使用しました。Yocto Projectは、オープンソースのコラボレーションプロジェクトで、複数のプラットフォーム向けのカスタムLinuxディストリビューションを作成するのに役立ちます。レイヤ構造になっているので、Yoctoのベースレイヤとボード製造者のレイヤの上に独自のディストリビューションを簡単にカスタマイズできる柔軟性があります。

フェッチ

ビルド手順の後はフェッチ操作が続きます。この段階の目標は、CI環境のソースをインポートすることです。このプロジェクトでは、repoでインポートしたToradexのソースを利用しました。repoは、複数のgitレポジトリからソースを簡単にフェッチし、ローカルで任意に整理するために作成されたツールです。

これはGitHub、GitLab、Gerritなどからのソースのフェッチにも簡単に利用できるはずです。Jenkinsでも、リモートレポジトリ上で変更があった場合に自動的にジョブをトリガすることができます。

ビルド

CIでは、ターゲットにイメージを展開する前に、ビルドが正常終了することを確認する必要があります。ビルドのアーティファクト生成は、再現性のためにクリーンなワークスペースで実行しなければなりません。

Toradex Apalis iMX6を使用していたため、このボード向けにToradexが提供する汎用イメージで作業を行いたいと考えましたが、これには追加的なテストツールが含まれません。このため、repoツールでソースをダウンロードするのにToradexが提供する裸のmanifestを使いました。

選択したのは、meta-toradex-demosレイヤから提供されるtdx-reference-minimal-imageです。もう1つインストールしたのは、Ansibleの使用に必要なphyton3でした。これはYoctoのローカル構成に追加しました(CORE_IMAGE_EXTRA_INSTALL += "python3")。ビルドを大幅に高速化するため、ビルド間で未変更のアーティファクトの再利用を可能にするJenkins Docker内のパーシステントなYocto sstateキャッシュを使用しました。

ビルドの再現性を確保するため、ビルド環境の設定にはCQFDを使用しました。CQFDは、Savoir-faire Linuxが開発したツールで、Dockerの複雑さを抽象化し、使用している作業ディレクトリを制御、再現可能なコンテナに自動的に接続します。この目的は、複数マシンとチームメンバー間でビルド環境を簡単に使用、共有、アップデートできるようにすることです。ソフトウェアのビルドを実行するためにホストマシンにインストールする必要があるのがDockerとCQFDのみとなるため、CIの観点から見ると非常に重要な機能です。

ターゲットへの展開

コードがコンパイルすることを確認するのは第一歩として大事なことですが、最後の変更が以前の機能を壊さなかったことの検証はまったく行われません。このため、コードの品質を確認して不具合を検出するには、ビルド後にテストを実施する必要があります。

ビルド直後にイメージの自動展開を実施するために、ネットワーク上で起動できるようボードを構成しました。デバイスツリーとカーネル、rootfsをそれぞれインポートするにはTFTPとNFSサーバーが必要です。これら2つのサーバーを展開しやすくするため、2つのDockerコンテナを作成してJenkins Dockerサーバーと一緒に起動することにしました。これらは、CIを実行しているマシン、または、多少の調整を加えて異なるマシン上で直接ホストすることもできます。

ターゲットの側では、TFTPとNFSサーバーにアクセスできるようブートローダーを設定する必要があります。このため、U-Bootを次のように設定しました。

# setenv serverip 192.168.X.X
# setenv ipaddr 192.168.X.X

次に、デバイスツリーとカーネルのイメージをTFTPを使用して取得する必要があります。

# tftp ${kernel_addr_r} ${serverip}:zImage 
# tftp ${fdt_addr_r} ${serverip}:imx6q-apalis-eval.dtb

そして、NFSをrootfsのソースとして設定します。

# setenv bootargs console=${console} root=/dev/nfs rootfstype=nfs ip=dhcp \ nfsroot=${serverip}:${rootfs_name},v4,tcp

最後に、次を実行してブートを始めます。

# bootz ${kernel_addr_r} - ${fdt_addr_r}

bootcmd変数に上記のコマンドを設定すれば、各ブートで自動的にこの構成を実行できます。

コンパイルされた最新のrootfsイメージだけが実行されるようにするため、rootfsディレクトリ名にタイムスタンプを追加しました。各ブートでJenkinsのパイプラインがU-Bootスクリプトを生成し、環境とポイントを正しいrootfsに変更します。このスクリプトは、U-BootがTFTPでダウンロードし、rootfs_name変数の設定に使用されます。使用されなかったrootfsディレクトリはあとで消去されます。

いったんU-Bootが正しく構成されたら、新しくデプロイされたイメージで実行しなければならないのはボードの再起動のみです。今回は、ターゲットの再起動のパイプラインの段階でソフトウェアによるrebootコマンドで実行しましたが、プログラム可能プラグを採用してハードウェアによる再起動をするよう設定することも可能です。

すでにU-Bootを正しく構成済みした場合、Jenkinsの初期ビルドでは使用できるrootfsがないためにボードは恐らく起動しません。ソフトウェア再起動が不可能なため、最初のビルドでは最後にハードウェアの再起動が必要となります。

Deploy on target
リモート管理とテスト

いったん新しく生成されたイメージがターゲットデバイスで起動されたら、CIはテストを開始できます。このステップでは、ターゲットマシンへの干渉を最低限に抑える必要があります。Ansibleは、ターゲットマシンでSSH接続によるリモートタスクとPython3の自動化を提供するツールです。Ansibleの代替としてスタンドアロンのSSHプロトコルのようなほかのツールを利用することも可能です。

Ansibleでは、マシンのインベントリに応じてタスクをリモート実行が可能なプレイブック入れて整理します。今回のパイプラインでは、以前のイメージからのターゲットマシンの再起動をAnsibleが処理します。ターゲットにテストを送って実行し、最後に結果をフェッチしてレポートを生成します。

リモートの非干渉的検証のメカニズムを利用して、通常は高低両方のレベルの機能をテストする必要があります。これにはCukiniaを利用することにしました。Cukiniaは、Savoir-faire Linuxが開発したシェルベースのテストツールです。Linuxベースの組み込みファームウェアで、シンプルなシステムレベルの検証テストを実行するために設計されました。CukiniaはYoctoのベースレイヤmeta-oeに統合されています。

いったんテスト結果をフェッチしたら、JUnitレポートがCIによって表示されます。

Remote management and tests
コードの統合

各ビルドのテスト結果が出たら、次に考えられるのは2つのシナリオです。テストで失敗した箇所があった場合、開発者は、シナリオが壊れた原因をすぐに特定して修正できるはずです。一方で、CIがテストに合格した場合は、そのコードを統合する準備ができたと考えることができます。通常はこの段階で、コードを「リリース」ブランチに統合するか、異なるレポジトリにプッシュするか、もっと複雑なデリバリーのプロセスを開始することになります。

まとめ

組み込みシステムソフトウエア開発におけるCIの実践は、問題を検出してできる限り早期に解決するのに非常に役立ちます。このCIのターゲット段階にテストを追加すれば、リリースされるコードの品質を高めることにつながります。

この記事では、組み込みデバイス上で簡単なデプロイと素早いテストを実行できることを実証しました。ご利用のコンテキストに合わせて独自のパイプラインを作成することで、ここで提供した例をカスタマイズすることも可能です。

記者:
Albert Babi
, Free software consultant, Savoir-faire Linux

Kévin L'hôpital
, Free software consultant, Savoir-faire Linux

コメントを投稿

Please login to leave a comment!
Have a Question?