RustのプロジェクトでDockerfileを書かなくてもよくする話

この記事はもともと「2025年も暮れだけどまだDockerfileで消耗してるの?」というタイトルを思いついたのだけどちょっとさすがに煽ってるなと思えるくらいにはなったらしい。さて何かちょっとしたマイクロなサービスをHTTPなりgRPCなりで作ったあと、手頃なインフラ(Cloud Runとか、まあKuberentesでも)コンテナで動かしてサービス公開したいとなったときに、たったひとつのExecutableを入れるのにこんなDockerfileを書くことは多いと思う。

FROM rust as builder

RUN mkdir /build
WORKDIR /build
COPY . .
RUN cargo build --release

FROM debian
RUN mkdir /app
COPY --from=builder /build/target/release/your-server /app/
ENTRYPOINT ["/app/your-server"]

で、これを .github/workflow/ci.yaml でコンテナイメージに焼いてこれを docker push していくわけだ。
だがちょっと待ってほしい。毎度毎度同じようなDockerfileを書いて、しかもプログラムが大きくなったり、テストが複雑になっていけばビルド時間が延びていく。キャッシュを効かせる工夫をどうのこうのとやっていかないといけない。ランタイムで必要なライブラリができたら、このファイルに apt-get install -q -y libhogehoge3u4 みたいなことを書いていくことになる。Dockerfile がどんどん大きくなって、細かいところまで目が届かなくなり、チームには退職者も新規参加者もありノウハウは失われていく…

そうしてこのDockerfileを使ったコンテナ、ある日突然起動に失敗するようになったりする。私が某所ではまったケースだと、 Rust 公式イメージlatestは debianベース になっており、これの更新サイクルは debian:latest と必ずしも一致しないことがある。そうするとライブラリの互換性が保証されず、ランタイムでいきなり落ちる…といったことになる。もっと我儘をいうとUbuntuイメージを使いたい。そうすると依存ライブラリによっては rust イメージがそのままでは使えないことがある。

もっと我儘をいうと docker コマンドがなくてもコンテナをビルドできるようになりたい。そもそもOCIという共通仕様があって、コンテナイメージのフォーマットは決まっているわけだから、実行ファイルをコピーするだけでイメージつくれないか。Goなら Ko というツールがあって、しかもSingle-binary executableが作れるので実行ファイルポン置きしただけのコンテナイメージをつくり、しかも ko apply と打つとそれをKubernetesにデプロイまでしてくれたりする。Dockerいらず!

Rustでもこれをやりたい。ということでどうやるかを考えた。残念ながらRustではSingle-binary executableを作れないので一筋縄ではいかない。幸いRustでもOCIイメージを読み書きするクレートがすでにあるので、これをゴニョれば案外簡単にいけるんじゃないか。と思った時代がわたしにもあった。いろいろ調べたメモは
OCI にある。

Here's the smallest container image ever with a Rust program inside.

kuenishi (@kuenishi.bsky.social) 2025-03-02T15:13:27.520Z
bsky.app

でもこのプロトタイプのコードは結構汚いし、実行ファイルを解析して依存ライブラリをビルド環境からコピーしてくるみたいなハックがかなり多くて汚かったのでやめた。そう。一旦だるくなって離脱した。

仕切り直し

ところが、このあと cargo-chef で作るイメージと実行時のイメージの依存ライブラリでABIが合わずコンテナが起動しなくなる問題に当たった。これは一旦ワークアラウンドできたが、もっと楽になんとかしたい。まずはインターフェースから考えよう。そう、やりたいことはこうだ(例)。

$ cargo container build
...(building container)...
$ cargo container push
Pushed image example.com/kuenishi/your-server:0.1.0
$ kubectl apply -f ./manifests/

まずはこの使い心地を試すために、内部的に定型的なDockerfileを生成して docker コマンドを呼び出すことでユーザー体験を試せないかというわけで、試験実装を作った。結構体験がよかったので、じゃあ crate にするか・・・となったところで「かぶってるやん」となって、ここからはリンクしないが、名前を考えるのに結構時間を使った。

さんざん悩んだ挙げ句、最終的には cargo-stow という名前に決めた。stow は詰め込むみたいな意味で、コンテナイメージにビルドされたブツをしまい込むといった意味イメージである。

リンク

これは、 Cargo.toml にいくつかレシピを書くだけでcargoからコンテナイメージを作ったりアップロードしたりできるものだ。相変わらずDockerをインストールする必要はあるが、それ以外のことは全部やることを目指している(といいつつ、 cargo-chef のような複雑な最適化はしていない)。具体的には、 Package metadata を書くだけで簡単なコンテナならビルドできるようになるものだ。レポジトリのExampleから例を引用すると、

[package.metadata.container]
target_image = "registry.example.org/directory/greeter:latest"
base_image = "ubuntu"
build_deps = ["libssl-dev"]
runtime_deps = ["libssl3t64"]

いまのところ設定する項目は4つだけで、それぞれイメージ名、ベースイメージ(これは暗黙にUbuntuにしてもいいかもしれない)、ビルド時の依存ライブラリ(ヘッダとか)、ランタイムの依存ライブラリ(動的ライブラリとか)である。これを定義すると、あとは内部的にDockerfileを生成してコンテナイメージをビルドしてくれる。ためしにcargostow に含まれているExampleをビルドすると、以下のように Buildkit のログがそのまま出る。

$ cargo install cargo-stow
    Updating crates.io index
  Installing cargo-stow v0.0.1
    Updating crates.io index
     Locking 63 packages to latest compatible versions
   Compiling proc-macro2 v1.0.103
   Compiling serde_core v1.0.228
   Compiling quote v1.0.42
   Compiling unicode-ident v1.0.22
   Compiling memchr v2.7.6
(snip)
  Installing /home/kuenishi/.cargo/bin/cargo-stow
   Installed package `cargo-stow v0.0.1` (executable `cargo-stow`)
$ cargo stow build
[+] Building 0.9s (20/20) FINISHED                                                                                                                           docker:default
 => [internal] load build definition from tmp-dockerfile                                                                                                               0.0s
 => => transferring dockerfile: 768B                                                                                                                                   0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                                                                       0.0s
 => [internal] load .dockerignore                                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                                        0.0s
 => [internal] load build context                                                                                                                                      0.0s
 => => transferring context: 918B                                                                                                                                      0.0s
 => [builder  1/13] FROM docker.io/library/ubuntu:latest                                                                                                               0.0s
(中略)
 => exporting to image                                                                                                                                                 0.0s
 => => exporting layers                                                                                                                                                0.0s
 => => writing image sha256:10846c3f4d2e516b7625ecaf01f164d23351669098418af69ff2c1b1c0597961                                                                           0.0s
 => => naming to registry.example.org/directory/greeter:latest
$ docker run registry.example.org/directory/greeter:latest
Hello, world!

cargo stow push は今のところ docker push の雑ラッパーなので特に例示はしない。 cargo stow run は何ならまだ作っていない。
将来的には Docker バックエンドを投げ捨ててYoukiベースのフロントエンドを何か作り込めばDockerを捨てられるのでは・・・と期待しているが、それは次にやる気が出たときにしようとおもう(結構やることがある)。

結局現状やってることは、みんな似たようなDockerfileを再発明して苦労しているのを代わりにやったろうってだけなのだが、案外スッキリしたなと個人的には思っている。このままPoC作り捨てて終わってもよいし、使ってみようという方がいたらもうちょっとOSS業を趣味として続けてもいいかもしれない。

まとめ

  • ちょっと面白そうなツールを業務外で久しぶりに作った
  • めんどくさい拘りが出発点だけど、へんなプライドとかこだわりを捨ててまずは簡単にいこうとしたら案外簡単に作れた
  • これはPyspaアドベントカレンダー2025の記事です
  • これを作るにあたって結構探して調べたのですが、「そのツールならもう3年前からあるよ」といわれても凹まない程度には成長した自負があるので、もしあったら教えてください
  • CNCF Buildpack をこの記事を見たトンプー先生に教えてもらった。これは高機能で便利そう。

思想を追記

思想なので無視してもらってもよいが、もうちょっと思想を書いておくと、コンテナはこれからあって当たり前の時代になるのだから、プログラミング言語や処理系ごとにコンテナイメージのビルドがネイティブでサポートされていく時代になってもいいと思っている。Node, Python, Go, Rust といったメジャーな言語・処理系をコンテナに詰めるときには気をつけることややること、キャッシュすべきものが全然異なるのだから、それぞれのエコシステムで最善のプラクティスを実装して提供するほうがよさそう。Pythonもwheelに詰めるじゃなくてコンテナに詰めたらいいし、GoはKoがあるからいいけど(それでもKustomizeとかとちょっと組み合わせにくい)。

それはそれとして、一方でBazelのように、全てをビルドできる閉じた世界を作るというアプローチもあるとおもう。でもちょっとBazelは自分にはむずかしすぎるのと、各言語の全ライブラリからbzlmodが標準で提供されているのが当たり前みたいな世界にならないとちょっと難しい気がしており、一時期Bazelにハマってはいたものの、そういう世界は来ないんじゃないか・・・という気がちょっとしている。

まあこんなことしなくても全部エーアイがやってくれるから人間はDockerfileなんて書かなくていし何なら読まなくてもいいんだよという時代が来るかもしれない。ちょっとわからない。

ちなみにcargo-stowはエーアイなしで全部Emacsで書いた。趣味なんだからエーアイに書かせたら勿体ないよね!