🌟

Raspberry Pi + Yocto + Docker + FastAPI + Celeryでエッジサービスの実装

に公開

作りたいものの全体像から参照しています
https://zenn.dev/takumique/articles/a451ffd2393b1f

作りたいもの

Raspberry Pi 4 (RasPi4)にYoctoを載せて、その上にdocker/docker-compose環境を構築し、その上にAPI+コアロジックを立ち上げます。

2個の自作コンテナと1個のdocker-hubからダウンロードするコンテナをdocker-composeで実行します。

  • 自作コンテナ
    • sample-api: gunicorn + uvicorn + fastapiのAPIサーバ
    • sample-core: Celeryのワーカーとなるコアロジックサーバ
  • docker-hubコンテナ
    • redis: Celeryのバックエンドとなるインメモリメッセージキュー

シングルマシン上でAPIとコアロジックを疎に分離する理由は、

  • コードの保守性の向上
  • コンピューテーションリソースが要求されるタスクの非同期・キューイング・同時実行を容易に制御
  • クラウド環境への移植性(コアロジックの中にクラウド上でGPUベースのコンテナに載せたいロジックがあった場合など)

動作する全ソースコードをGitHubで公開しています。

Yoctoでのコンテナイメージビルドについて

Yoctoのビルド内でコンテナイメージをビルドするには、Yoctoを使ってコンテナイメージをビルドすることになります。

ここで注意が必要なのが、<build dir>/local.confでIMAGE_INSTALL:appendしたものはコンテナイメージにもインストールされてしまいます。そこで<build dir>/local.confではIMAGE_INSTALL:appendの代わりにCORE_IMAGE_EXTRA_INSTALL:appendを使用しました。ドキュメントによると、これはcore-image-*のターゲットのみにインストールされるとのことで、コンテナイメージにインストールしたくない場合に便利そうです。

実装

それでは作っていきます。meta-sampleレポジトリを作成します。構成は以下のようになっています。

meta-sample +- recipes-sample  →自作コンテナのアプリのレシピ、自作コンテナイメージをビルドするレシピ
            +- recipes-packages  →自作コンテナが依存するPythonパッケージのレシピ
            +- recipes-container  →ターゲットイメージ上でコンテナを自動でインポート・ランするレシピ

なお、initマネージャはsystemdを前提にしています。

Yoctoへのdocker/docker-composeの組み込み

Yocto標準の仮想化関連のレシピはmeta-virtualizationに入っています。まずはこれをダウンロードします。

git clone -b walnascar git://git.yoctoproject.org/meta-virtualization

仮想化をビルド対象に含めるため、以下のように追加します。

bblayers.conf
  ${TOPDIR}/../meta-openembedded/meta-filesystems \
  ${TOPDIR}/../meta-virtualization \
local.conf
DISTRO_FEATURES:append = " virtualization"

最後にdocker/docker-composeをインストールするよう追加します。

local.conf
CORE_IMAGE_EXTRA_INSTALL:append = " docker"
CORE_IMAGE_EXTRA_INSTALL:append = " docker-compose"

これでターゲットデバイス上にdocker/docker-compose環境の準備ができました。

実行時に必要なディスクスペースの確保

コンテナイメージのインポートなどで実行時にディスクスペースを必要とします。Yoctoでビルドされるwicはパーティション構成も含んでおり、rootfsのパーティションサイズはrootfsのサイズ+余白(IMAGE_OVERHEAD_FACTOR)となり余裕がありません。

そこでドキュメントにあるようにスペースを確保します。

私は64GBのSDカードを使っているので40GiB確保しました。これでrootfsパーティションはトータル52GiBになりました。今後の機能追加を考えても余裕があります。

local.conf
IMAGE_ROOTFS_EXTRA_SPACE = "41943040"

自作コンテナのアプリ

アプリ用に別のリポジトリを作成します。アプリ自身はホスト上で開発し、動作確認できたリビジョンをインストールするレシピを作成します。(recipes-sampleディレクトリを参照してください)

APIサーバ

アプリ用のレポジトリを作成して実装します。アプリケーション自身の動作確認はホスト上で行うのでDockerfile/requirements.txtも作成していますが、Yoctoビルドには使用しません。

コアロジックサーバ

こちらもアプリ用のレポジトリを作成して実装します。同様にアプリケーション自身の動作確認はホスト上で行うのでDockerfile/requirements.txtも作成していますが、Yoctoビルドには使用しません。

Pythonパッケージの依存関係の解決

自作コンテナをホスト上で実行しpip show <package>で依存パッケージを取得しました。

必要なPythonパッケージのレシピがmeta-openembeddedにあればそれを使い、無ければ自分でレシピを作成することになります。PyPI(pip)でインストールできるパッケージであれば簡単にレシピを作成できます。(recipes-packagesディレクトリを参照してください)

コンテナイメージビルド

前述のアプリと依存パッケージをインストールしたコンテナイメージをビルドします。(recipes-sampleディレクトリを参照してください)
meta-virtualizationにお手本があるので、それをベースにします。

コンテナイメージのターゲットイメージへのデプロイ

以下のようなbbファイルで実現しました。(recipes-container/sample-services-preloadディレクトリを参照してください)

do_install()にsample-api-container-imageおよびsample-core-container-imageのdo_image_complete()への依存を追加して、do_install時に${DEPLOY_DIR_IMAGE}/sample-api-container-image-${MACHINE}.rootfs.tar.bz2および${DEPLOY_DIR_IMAGE}/sample-core-container-image-${MACHINE}.rootfs.tar.bz2が出来上がっていることを保証します。

そしてdo_install()でイメージファイル(ファイルシステムのtar.bz2)をターゲットイメージ上の/opt/sampleにインストールします。

sample-services-preload.bb
SUMMARY = "bitbake-layers recipe"
DESCRIPTION = "Recipe created by bitbake-layers"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/MIT;md5=0835ade698e0bcf8506ecda2f7b4f302"

SRC_URI = "file://."
S = "${WORKDIR}/sources"
UNPACKDIR = "${S}"

do_install() {
    install -d "${D}/opt/sample"
    install -m 644 ${S}/compose.yaml ${D}/opt/sample/compose.yaml
    install -m 644 "${DEPLOY_DIR_IMAGE}/sample-api-container-image-${MACHINE}.rootfs.tar.bz2" "${D}/opt/sample"
    install -m 644 "${DEPLOY_DIR_IMAGE}/sample-core-container-image-${MACHINE}.rootfs.tar.bz2" "${D}/opt/sample"
}

FILES:${PN} = "/opt/sample/*"

DEPENDS = "sample-api-container-image sample-core-container-image"
do_install[depends] += "sample-api-container-image:do_image_complete"
do_install[depends] += "sample-core-container-image:do_image_complete"

実行時のコンテナイメージのDockerへのインポート・ラン自動化

このままでは、ただターゲットイメージ上にtar.gzがあるだけなので、起動時にdockerへインポートし、docker-composeでサービスを起動させます。(recipes-container/sample-services-autorunディレクトリを参照してください)

Yoctoターゲットのビルド対象への追加

最後にこれまでのレシピをYoctoターゲットのビルド対象に追加します。

bblayers.conf
  ${TOPDIR}/../meta-sample \
local.conf
CORE_IMAGE_EXTRA_INSTALL:append = " docker"
CORE_IMAGE_EXTRA_INSTALL:append = " docker-compose"
CORE_IMAGE_EXTRA_INSTALL:append = " sample-services-autorun"

Discussion