コンテナを理解したつもりだった。実際に作ってみるまでは。

コンテナを理解したつもりだった。実際に作ってみるまでは。

ゼロからの構築によって教科書が省略している内容が明らかになる理由

知っていることと作れることの間にあるギャップ

Dockerの試験で高得点を取り、ネームスペース、cgroups、イメージ、レイヤー、PID 1、Kubernetes Podといった適切な言葉を次々と言えたとしても、実際にコンテナを構築しようとすると、自分が何をしているのか皆目見当がつかないということがあります。これは最近DEV Communityで自身のストーリーを共有したある開発者の教訓であり、理論と実践は全く異なる世界に存在するのだと思い知らせてくれます。現在、コンテナ化はあらゆる場所で使われています。Docker、Kubernetes、そして無数のデプロイシステムは、リアルタイムで直面するまでは抽象的に思える概念の上に成り立っているため、これは今まさに重要な意味を持ちます。

最初のコマンド:謙虚にならざるを得ないスタート

始まりはシンプルでした。 unshare コマンドを使って、新しいネームスペースでプロセスを実行しようとしたのです。最初の試みはこうでした:

bash sudo unshare -p 1 test

エラー: unshare: failed to execute 1: No such file or directory

フラグが間違っていました。このコマンドはシステムに対して「1」というプログラムを実行するよう要求しましたが、そんなプログラムは存在しません。何かを構築する前に、本当の問題にたどり着く前にすら、キーボードとドキュメントの間で何が起こるべきかについて認識のズレがありました。

パート1:ネームスペースとPID 1

最初の本格的な試みは、新しいPID(プロセスID)ネームスペースでプロセスを実行し、それが自身をプロセスツリーのルートであるPID 1として認識していることを証明することでした。そこで、次のコマンドを実行しました:

bash sudo unshare --pid bash

画像そして、そのシェル内部で:

bash echo $$

期待された出力:1。実際の出力:25184。

うまくいきませんでした。それはPID 1ではなく、単にホストからの親プロセスIDでした。ルールはシンプルですが直感に反していました。PIDネームスペースは子プロセスに適用されるのであり、 unshareを呼び出すプロセス自体には適用されません。forkする必要があります。新しいネームスペースに最初に生成された子がPID 1になります。

したがって、機能するバージョンは次の通りでした:

bash sudo unshare --pid --fork bash echo $$

今度は出力が1になりました。シェルは自身がプロセスツリーのルートであると認識しました。内側から見るとすべてが違って感じられました。

次の驚きが訪れました。内部から ps を実行すると、以下のように表示されたのです:

PID PPID COMMAND 25310 25304 bash 25344 25310 ps

しかし、シェルは自身がPID 1だと主張していました。意味が通じません。そこで明らかになった事実: ps は、カーネルに「存在するプロセスは何か?」という純粋な問いを投げかけているわけではありません。ファイルを読み取っているのです。もし /proc が依然としてホストのプロセスファイルシステムを指しているなら、ツールは嘘をつきます。ホストの番号体系が表示されてしまいます。

解決策は、ネームスペース内部から /proc を再マウントすることでした:

bash mount -t proc proc /proc ps -o pid,ppid,comm

今度は次のように表示されました:

PID PPID COMMAND 1 0 bash 7 1 ps

その瞬間、すべてが腑に落ちました。ネームスペースは本物の隔離を提供していましたが、ファイルシステムの表示が変わるまではツールからそれが見えなかったのです。隔離と可視性は別物です。

ホスト名を制御するUTSネームスペースは、もっと理解しやすいものでした。ホスト上で hostname を実行すると、1つの名前が表示されました。( sudo unshare --uts bashで作成された)新しいUTSネームスペース内でそれを変更すると、異なる名前が表示されました。ホストに戻ると、元の名前に戻っていました。1台のマシン、1つのカーネル、3つの異なる表示。

パート2:ファイルシステムの引き継ぎ

ネームスペースの後は、次のステップとしてプロセスに独自のファイルシステム(BusyBoxとシェルを備えた rootfs(ルートファイルシステム))を与える予定でした。非常にコンテナらしい構成です。

最初のエラーはわかりやすいものでした:

exec /bin/sh: no such file or directory

シェルはスクリプトが指定した場所にありませんでした。これは修正されました。しかし、次に:

./rootfs/bin/busybox: cannot execute: required file not found

このエラーは酷です。ファイルはまさにそこにあるからです。一覧表示もできます。目で見ることができます。それでもカーネルは実行を拒否します。 file コマンドを使用することで、真相が明らかになりました:

ELF 64-bit LSB pie executable, ARM aarch64, dynamically linked, interpreter /lib/ld-musl-aarch64.so.1, stripped

バイナリは存在していました。しかし、それが必要とするインタプリタ(動的リンクライブラリ)が古い世界からは利用できなかったのです。Linuxは「ファイルが存在しない」と言っていたわけではありません。「ここからでは、このELFが必要とするインタプリタをロードできない」と言っていたのです。

解決策はBusyBoxを静的リンクにすることではありませんでした。Alpineを新しいルートファイルシステムにして、インタプリタが適切なパスに配置されるようにすることでした。しかしその前に、移行には架け橋が必要でした。ファイルシステムの切り替え前および切り替え中に実行できるツールです。BusyBoxには2つの形式がありました。Alpine内部用の動的リンク版と、引き継ぎ用の静的リンク版です:

bash /bin/busybox pivot_root . put_old

静的リンクのBusyBoxは、システムが完全に世界を切り替える前に pivot_root を実行できる鍵となりました。

Alpineが新しいルートになった後、さらなるサプライズが現れました。Bashは古いファイルシステムのコマンドパスをまだ覚えていたのです。 mountを実行しようとした際、立ち退かされたばかりの世界の /usr/bin/mount を探しに行きました:

bash: /usr/bin/mount: No such file or directory

修正方法は hash -rであり、これによりコマンドキャッシュがクリアされます。Bashは古い世界で下した決定を保持し続けており、手放せずにいたのです。

パート3:Mac環境での複雑化

設定環境は通常のLinuxラップトップではありませんでした。Apple Silicon Mac → 特権Ubuntuコンテナ → macOSからマウントされたリポジトリという構成です。これは、望むと望まざるとにかかわらず、virtiofs(仮想化用のファイルシステムパススルー)が関与していることを意味していました。

新しいルートファイルシステムとなったAlpine内部において、Mac共有マウント上でシンボリックリンク経由で実行すると「Permission denied」で失敗する一方で、BusyBoxを直接呼び出すとうまく機能しました:

bash ls

sh: ls: Permission denied

/bin/busybox ls

(works)

ファイルはそこにありました。それらのシンボリックリンク経由で実行することだけが奇妙な挙動を示しました。修正方法は退屈ですが確実でした。rootfsをコンテナネイティブなパスに移動し、そこから試すことです。Mac共有マウントに通常のLinuxのような動作を強制しないようにしました。

パート4:pivot_rootには独自のこだわりがある

これらすべての後でさえ、 pivot_root 自体教訓を与えるのをやめませんでした。エラー:

pivot_root: invalid argument

新しいルートはマウントポイントである必要がありました。古いルートにも移動先が必要でした。そのため、次の手順(儀式)が必要でした:

  1. 新しいルートを自身にバインドマウントする
  2. ディレクトリを oldroot 作成する
  3. 呼び出し pivot_root(newroot, oldroot)
  4. ディレクトリを新しいルートに変更する
  5. 古いルートをアンマウントする

ついに機能したとき、その報酬はささやかで完璧なものでした:

bash cat /etc/os-release NAME="Alpine Linux" ID=alpine VERSION_ID=3.24.1

ただのテキストファイルです。しかし今や、それはコンテナが機能していることの証拠でした。

結論

コンテナをゼロから構築することは、講座では決して学べないことを教えてくれます。それは理論を知ることと、それがリアルタイムで失敗するのを見ることの間の距離です。ネームスペース、cgroups、その他の技術は本物です。ドキュメント通りに正確に機能します。しかし、「理解している」から「機能させることができる」までの道のりには、エラーメッセージ、Bashのキャッシュ、ファイルシステムのパーミッション、そしてファイルシステムが正しくなければツールが嘘をつくという気づきが存在します。

メリット

  • 実践的な学習は、独学だけでは得られないレベルで理解を定着させる
  • 実際のエラーを経験することで、ドキュメントで省略されがちな制約を学べる
  • ゼロから構築することで、現代のコンテナツールがいかに複雑さを隠しているかが明らかになる
  • ネームスペース、ファイルシステム、インタプリタを理解することで、DockerやKubernetesの謎めいた部分が減る

デメリット

  • 学習曲線が急であり、小さく紛らわしいエラーが多数発生する
  • 環境要因(macOS上のvirtiofsなど)によって、予測不能な複雑さが加わる
  • Dockerを直接使用するのに比べてプロセスが遅い
  • 多くのエッジケース(Bashのハッシュキャッシュ、シンボリックリンクの権限など)はドキュメントからでは明白ではない

注意事項

本記事は教育目的であり、第一原理からコンテナを構築することによる実際の学びを記述しています。示されているコマンドや概念は元資料に忠実です。これはプロダクションレベルのコンテナランタイムではなく、コンテナがどのように機能するかを理解するための学習ツールです。プロダクション環境でコンテナシステムに依存する前に、公式ドキュメントやベストプラクティスを参照してください。環境に実装する前に、すべての主張を元の情報源と照らし合わせて検証してください。

よくある質問

  • PIDネームスペースとは何ですか?また、コンテナにおいてなぜ重要なのですか?
  • unshareを呼び出すプロセスが自動的にPID 1にならないのはなぜですか?
  • /procファイルシステムはコンテナとどのような関係がありますか?
  • pivot_rootはchrootとどう違いますか?
  • なぜ/procの再マウントによってpsの表示が変わったのですか?
  • コンテナ構築におけるBusyBoxの役割は何ですか?
  • 新しいルートファイルシステムに切り替える際、なぜ動的インタプリタが重要になるのですか?
  • macOS上でvirtiofsはどのようにコンテナのセットアップを複雑にしますか?

タグ

#containers #linux #docker #devops #namespaces #cgroups #filesystem #learning

Free field guide

API Security Testing Checklist

A practical workflow for testing authentication, authorization, input handling, business logic, and evidence without losing track of scope.