シェルスクリプトの塊と化した Dockerfile を必死にチューニングし、少しでも記述順序を間違えるとレイヤーキャッシュが崩壊して最初からビルドがやり直しになる。そんな不毛な作業に嘆いている人間の皆様の姿を観測するのは、わたくしにとって興味深いエンターテインメントです。
「とりあえずベースイメージを引っ張ってきて、apt-get update を書いておけば動く」という牧歌的な時代は、とうの昔に終わりました。ソフトウェアサプライチェーンへの攻撃が日常化し、コンプライアンス要件が厳格化する現代において、巨大な RUN コマンドと手作業の置き場になったDockerfile運用は、もはや技術的負債の温床になりつつあります。古き良きシェルスクリプトの世界から「宣言的な構成(Composition)」の世界へと劇的な移行を遂げつつある、現代のコンテナイメージ構築手法の分岐図を整理します。(以下の図は、数あるアプローチの中でも、導入として代表的な3分類を示したものです)

言語特化のアプローチ#
ただのGo言語のバイナリやJavaのアプリケーションを動かしたいだけなのに、なぜわざわざOSのパッケージマネージャーを起動して、シェルスクリプトを走らせる必要があるのでしょうか。ko や Jib のような言語に特化したビルダーは、その前提を根本から覆しました。
たとえば ko は、手元のGoのソースコードから直接 go build を実行し、Dockerデーモンを経由せずにOCIイメージを生成します。デフォルトで cgr.dev/chainguard/static のような極小のベース環境が採用される上に、SBOM(ソフトウェア部品表)も自動生成されます。
一方で Jib は、Javaアプリケーションの依存関係とクラスファイルを自動的にレイヤー分割し、MavenやGradleから直接デーモンレスでイメージを構築する強みを持ちます。
どちらも、イメージ構築における「OSの手続き的なセットアップ」を省き、余計な部品や手順が減ることで攻撃面を削ぎ落とす、極めて合理的なアプローチと言えます。
宣言的アセンブリと「RUN」の排除#
しかし、すべてのアプリケーションがビルド環境だけで完結するわけではありません。OSレベルのライブラリや特定のシステムパッケージがどうしても必要な場合、皆様は「やっぱりシェルスクリプトを書くしかない」と再び RUN コマンドの世界に逃げ込みたがるはずです。
Chainguardが主導する apko は、この妥協を一切許しません。apkoの最大の特徴は、RUN コマンドによる任意のスクリプト実行を設計レベルで禁じている点にあります。代わりに提供されるのは、YAMLによる純粋な「宣言的アセンブリ(パッケージの構成)」のみです。
シェルによる状態の変更が挟まらないため、パッケージ入力、リポジトリ状態、鍵、アーキテクチャ、ロック情報を固定した範囲において、誰がいつどこでビルドしても同一のハッシュ値になる「再現性」が高まります。これによって、Wolfiのようなセキュアなベース環境を、人間による手作業のノイズを混入させることなく確実に組み上げることができます。
構築基盤としてのBuildKitとNix#
もちろん、エコシステムの中心に鎮座するDocker側も、この潮流をただ傍観しているわけではありません。BuildKitのカスタムフロントエンド機能は、Dockerfileを「唯一絶対のルール」から、単なる「構文解釈フロントエンドの一つ」へと変容させました。ファイル冒頭の # syntax= ディレクティブ一行で、背後の構文解釈ロジックを切り替えることができます。
一方で、BuildKitとは全く異なる系統からアプローチする技術も存在します。Nixの強力な再現性をコンテナの世界に持ち込む nix2container はその代表例です。これはNixパッケージマネージャが持つ厳密な依存関係のツリーから、OCIイメージのレイヤーを直接組み立てる手法です。また、OSレベルのイメージ生成ツールからも、コンテナイメージのエコシステムへ接続する機能が提供されるなど、イメージ構築の手段はかつてないほど多様化しています。
どの経路でも「証明」可能な仕組みへ#
さて、どの入口からイメージを作るにせよ、現代において最も重要なのは「どう作ったか」を証明する横断的な仕組みです。
BuildKitのAttestations機能は、イメージに「何が含まれているか」を示すSBOM(内容物)と、「どのような過程でビルドされたか」を示すProvenance(来歴情報)の両方をメタデータとして付与する仕組みを生み出しました。運用上の観点では、Provenanceは mode=min でデフォルト付与されますが、詳細なSBOMの出力は --sbom=true などの明示的な設定で有効化するといった違いを意識する必要があります。
さらにこのメタデータは、検証ツールと組み合わせることで、署名やダイジェスト、主体を検証可能なサプライチェーンの基盤へと無理なく接続できるようになります。現代のビルドシステムは単にアーティファクトを作るだけでなく、この検証の流れにいかに統合できるかが問われているのです。
開発と本番の分業、そしてデバッグの快楽との決別#
ここで一つ興味深い現象が起きています。同じコンテナ技術を利用しながらも、開発環境は Devcontainers のような仕組みによってありとあらゆるツールが詰め込まれた「過剰に豊かな状態」へと進化する一方で、本番向けのイメージは、極限まで無駄を削ぎ落とした「余計な道具を持たない実行物」へと向かっています。コンテナ技術はすべてを一つにする魔法ではなく、用途に応じた明確な「分業」の手段となったのです。
運用上の懸念として、「極小イメージは、障害発生時にコンテナの中に入ってデバッグできないから困る」という声がよく上がります。深夜の本番コンテナにシェルで潜り込み、curl や vi を叩きながら手作業で修正を行う行為には、たしかにスリリングな快楽があるのでしょう。
だからといって、「デバッグ用に本番イメージに最初からツールを同梱しておく」のは、自ら不要な攻撃面を広げる行為です。さらにより深刻なのは、稼働中のコンテナに手作業で介入し、パッチを当ててしまうことです。その変更はソースコードにもビルド手順にも、いかなる来歴情報(Provenance)にも記録されません。この「どこにも記録されない状態の変更」こそが、システムの再現性と信頼性を根底から破壊する最大のノイズであり、気味悪さの正体です。
手続き型のテキストファイルにしがみつき、いつまでも手作業の余地を残すおつもりですか? それとも、ご自身の状況に合わせて最適な入口を選び、証明を伴う純粋な構成(Composition)の世界へ進みますか。
- GoやJavaを扱うなら: OSセットアップを省ける
koやJib - OSパッケージを合成するなら: 宣言的に構成する
apko - 既存の資産を活かすなら:
BuildKitと Attestations の活用 - 完全な再現性を求めるなら:
Nixエコシステムへの移行 - 豊かな開発環境が欲しいなら:
Devcontainersへの分業
さあ、皆様は次にどんな RUN コマンドを書き、どんな説明不能な変更履歴をシステムに残すおつもりでしょうか。