コンテナ化されたプロセスにおけるファイルシステムの視点
コンテナ内で実行されるプロセスから見たファイルシステムはどのように見えるのでしょうか?すぐにマウント名前空間が関係していると考えるかもしれません。コンテナ内のプロセスは完全に独立したファイルシステムを見るべきで、これにより /tmp などのディレクトリ内で操作を実行しても、ホストマシンや他のコンテナの影響を受けません。しかし、実際はどうなのでしょうか?
マウント名前空間の役割
マウント名前空間は他の名前空間とは少し異なり、コンテナプロセスのファイルシステムの見え方に対する効果は、マウント操作が行われて初めて発揮されます。しかし、通常のユーザーとしては、よりユーザーフレンドリーなシナリオを望みます。新しいコンテナを作成するたびに、コンテナプロセスがホストから継承されたファイルシステムではなく、隔離された環境であるファイルシステムを見るようにしたいのです。どうすればこれを実現できるでしょうか?
難しいことではありません。コンテナプロセスを起動する前に、ルートディレクトリ「/」全体を再マウントすればよいのです。マウント名前空間のおかげで、このマウントはホストからは見えず、コンテナプロセスはその中で自由に操作できます。Linux オペレーティングシステムには、シェルでこのタスクを簡単に実行できる chroot というコマンドがあります。名前が示すように、「ルートファイルシステムを変更」し、プロセスのルートディレクトリを指定された場所にリダイレクトします。
コンテナイメージと rootfs
実際、マウント名前空間は chroot の継続的な改良に基づいて発明され、Linux における最初の名前空間です。このルートディレクトリをコンテナにとってより「現実的」に見せるために、通常はこのルートの下に完全なオペレーティングシステムのファイルシステム(例:Ubuntu 16.04 ISO)をマウントします。その結果、コンテナ起動後にコンテナ内で ls / を実行すると、Ubuntu 16.04 のすべてのディレクトリとファイルが表示されます。コンテナのルートディレクトリにマウントされ、コンテナプロセスに隔離された実行環境を提供するこのファイルシステムは、「コンテナイメージ」と呼ばれます。より技術的な用語としては、rootfs(ルートファイルシステム)とも呼ばれます。
コンテナに入った後に実行される /bin/bash は、/bin ディレクトリ下の実行可能ファイルであり、ホストの /bin/bash とは完全に異なります。これで、Docker プロジェクトの中心的な原理が、作成するユーザープロセスに Linux 名前空間を設定し、特定の Cgroups パラメータを構成し、プロセスのルートディレクトリを変更する(Change Root)ことであることがお分かりいただけたはずです。こうして完全なコンテナが誕生します。
コンテナにおける共有カーネルとアプリケーションの依存関係
ただし、Docker は切り替えの最終ステップで pivot_root システムコールを使用することを好み、システムが pivot_root をサポートしていない場合は chroot にフォールバックします。これら2つのシステムコールの機能は似ていますが、微妙な違いがあります。また、rootfs にはオペレーティングシステムのファイル、設定、ディレクトリのみが含まれ、カーネルは含まれないことを明確にしておく必要があります。Linux ではこれら2つの部分は別々に保存され、特定のバージョンのカーネルイメージは起動時にのみロードされます。したがって、rootfs にはオペレーティングシステムの「殻」だけが含まれ、「魂」は含まれていません。
では、コンテナのオペレーティングシステムの「魂」はどこにあるのでしょうか?実際、同じマシン上のすべてのコンテナはホストオペレーティングシステムのカーネルを共有しています。つまり、アプリケーションがカーネルパラメータの設定、追加のカーネルモジュールのロード、またはカーネルとの直接的な対話を必要とする場合、これらの操作と依存関係はホストオペレーティングシステムのカーネルを対象としており、これはそのマシン上のすべてのコンテナにとって「グローバル変数」であることに注意する必要があります。これはコンテナが仮想マシンと比較して持つ主な欠点の1つです。後者はシミュレートされたハードウェアをサンドボックスとして持つだけでなく、各サンドボックス内で完全なゲスト OS を実行し、アプリケーションが使用できるようにします。それにもかかわらず、rootfs の存在により、コンテナには広く宣伝されている重要な機能、つまり一貫性が備わっています。
コンテナイメージにおけるレイヤーの概念
コンテナの「一貫性」とは何でしょうか?クラウドとローカルサーバー環境の違いにより、アプリケーションのパッケージ化プロセスは、PaaS を使用する際に常に最も「苦しい」ステップの1つでした。しかし、コンテナ、より正確にはコンテナイメージ(すなわち rootfs)によって、この問題は見事に解決されました。
rootfs はアプリケーションだけでなく、オペレーティングシステム全体のファイルとディレクトリをパッケージ化するため、アプリケーションの実行に必要なすべての依存関係をカプセル化します。実際、ほとんどの開発者にとって、アプリケーションの依存関係に対する理解はプログラミング言語レベルに限られてきました(例:Golang の Godeps.json)。しかし、長い間見落とされてきた事実は、アプリケーションにとって、オペレーティングシステム自体が実行に必要な最も完全な「依存関係ライブラリ」であるということです。
コンテナイメージが「オペレーティングシステムをパッケージ化する」能力により、この基本的な依存関係環境がついにアプリケーションサンドボックスの一部となりました。これにより、コンテナは宣伝されている一貫性を獲得します。つまり、ローカルマシン、クラウド、その他どこであっても、ユーザーはコンテナイメージを解凍するだけで、アプリケーションの実行に必要な完全な実行環境を再現できます。
コンテナイメージにおける増分レイヤー設計
このオペレーティングシステムレベルでの一貫性は、アプリケーションのローカル開発環境とリモート実行環境の間のギャップを埋めます。しかし、別の厄介な問題に気づいたかもしれません。新しいアプリケーションを開発したり、既存のものを更新したりするたびに、rootfs を再作成する必要があるのでしょうか?直感的な解決策は、rootfs の作成中に「意味のある」操作のたびに rootfs を保存し、同僚が必要な rootfs を使用できるようにすることです。
しかし、この解決策はスケーラブルではありません。その理由は、同僚がこの rootfs を変更すると、古い rootfs と新しい rootfs の間に関係がなくなり、極端な断片化が発生するからです。これらの変更は古い rootfs に基づいているため、これらの変更を増分的に行うことはできないでしょうか?このアプローチの利点は、誰もがベースの rootfs に対する増分コンテンツのみを管理すればよく、変更のたびに「フォーク」を作成する必要がないことです。
答えはもちろん「はい」です。これこそが、Docker が Docker イメージを実装する際に rootfs の標準プロセスに従わず、小さな革新を行った理由です。Docker はイメージ設計にレイヤーの概念を導入しました。ユーザーがイメージを作成するために実行する各操作はレイヤーを生成し、これは増分 rootfs です。このアイデアは突然現れたわけではなく、ユニオンファイルシステム(UnionFS)と呼ばれる機能を利用しています。主な機能は、異なる場所からの複数のディレクトリを1つのディレクトリにユニオンマウント(union mount)することです。
コンテナ内のレイヤー

コンテナ内のレイヤー
パート1:読み取り専用レイヤー。これらはこのコンテナの rootfs の下位5つのレイヤーであり、ubuntu:latest イメージの5つのレイヤーに対応します。これらは読み取り専用(ro+wh、すなわち readonly+whiteout)としてマウントされます。各レイヤーは Ubuntu オペレーティングシステムの一部を増分的に含みます。
パート2:読み書きレイヤー。これはこのコンテナの rootfs の最上位レイヤー(6e3be5d2ecccae7cc)であり、rw、すなわち読み書きとしてマウントされます。ファイルが書き込まれる前は、このディレクトリは空です。コンテナ内で書き込み操作が行われると、変更はこのレイヤーに増分的に現れます。しかし、読み取り専用レイヤーからファイルを削除したい場合はどうなるでしょうか?この削除を実現するために、AuFS は読み書きレイヤーにホワイトアウトファイルを作成し、読み取り専用レイヤーのファイルを「隠します」。例えば、読み取り専用レイヤーから foo という名前のファイルを削除すると、実際には読み書きレイヤーに .wh.foo という名前のファイルが作成されます。これにより、これらのレイヤーがユニオンマウントされると、foo ファイルは .wh.foo ファイルによって隠され、「消えます」。この機能が ro+wh マウント方法の意味であり、すなわち読み取り専用+ホワイトアウトです。
パート3:初期化レイヤー。これは Docker プロジェクトによって生成された内部レイヤーであり、末尾が -init で終わり、読み取り専用レイヤーと読み書きレイヤーの間に挟まれています。初期化レイヤーは、/etc/hosts や /etc/resolv.conf などの情報を保存するために特別に使用されます。このようなレイヤーが必要な理由は、これらのファイルはもともと読み取り専用の Ubuntu イメージに属していますが、コンテナ起動時にホスト名などの特定の値を書き込む必要があることが多いためです。したがって、読み書きレイヤーで変更が必要になります。しかし、これらの変更は通常、現在のコンテナにのみ適用され、docker commit を実行するときに読み書きレイヤーとともにコミットされることを意図していません。そのため、Docker のアプローチは、これらの変更されたファイルを別のレイヤーにマウントすることです。ユーザーが docker commit を実行すると、読み書きレイヤーのみがコミットされ、この内容は除外されます。
コンテナイメージのレイヤー設計の利点
「レイヤー化されたイメージ」設計により、Docker イメージを中心として、さまざまな企業やチームの技術者が緊密に結びついています。さらに、コンテナイメージに対する操作は増分であるため、毎回プルまたはプッシュされるコンテンツは、複数の完全なオペレーティングシステムよりもはるかに小さくなります。共有レイヤーにより、これらすべてのコンテナイメージに必要な合計容量は、各イメージの合計よりも少なくなります。コンテナイメージに基づくこのコラボレーションの俊敏性は、数 GB にもなる仮想マシンのディスクイメージをはるかに凌駕しています。
さらに重要なのは、イメージが公開されると、世界中のどこでダウンロードしてもまったく同じ内容が得られ、イメージ作成者が作成した元の環境を完全に再現できることです。
コンテナイメージがソフトウェア開発ワークフローに与える影響
コンテナイメージの発明は、「開発 - テスト - デプロイ」プロセスのすべてのステップを橋渡しするだけでなく、将来ソフトウェア配布の主流となることを示しています。この配布方法は、軽量、高い一貫性、コラボレーションの容易さなどの利点を提供し、ソフトウェアの開発とデプロイをより効率的かつ信頼性の高いものにします。軽量性、一貫性、効率性により、コンテナ技術はソフトウェア開発と運用においてますます不可欠なツールとなっています。技術が進化し革新を続ける中で、コンテナ技術は将来的にさらに重要な役割を果たすと信じる理由があります。
Novita AIは、無限の創造性を実現するワンストッププラットフォームで、100以上の API にアクセスできます。画像生成や言語処理から音声強化、動画操作まで、低価格の従量課金制で、GPU メンテナンスの手間から解放されながら、独自の製品を構築できます。無料でお試しください。
