前回は、Linuxで使えるコンテナの実装を説明したあと、LXCを使って簡単にコンテナの作成、起動、停止が行えるところを紹介しました。今回は、そのコンテナの仕組みを簡単に説明し、その仕組みからくるメリット・デメリットを紹介した後、コンテナはカーネルのどのような仕組みを使って動作しているのかを簡単に説明していきたいと思います。
コンテナの仕組み
コンテナをまだ使ったことがない方でも、VMwareやVirtualBox、KVMといった仮想マシン(VM)を使ったことはあるという方は多いのではないでしょうか。まずはVMとの比較をしながら、コンテナの仕組みを説明してみましょう。
VMでは図1のように、コンピュータの上で動くOSやVMを実現するためのハイパーバイザの上で、実際のハードウェアをエミュレートするVMが動きます。つまり実際の物理的なコンピュータと同じようなものがソフトウェアによって実現されているので、このVMを使うにはOSが必要になります。つまり、図1のように物理的なコンピュータ上で動くOSの上でさらにOSが動きます。また、VMを動かしているOSからはVMのプロセスが見えているだけで、VM上で動作しているプロセスは見えません。
一方コンテナは、図2のように、起動する全てのプロセスはコンピュータ上にインストールされたOS(ホストOS)上で直接起動します。通常のプロセスの動作と異なるのは、そのプロセスの一部をグループ化し、他のグループやグループに属していないプロセスから隔離した空間で動作させる点です。貨物輸送のコンテナのように、隔離された空間にプロセスが入っているので、この空間を『コンテナ』と呼ぶわけです。実際のコンテナのように、あるコンテナの内部から他のコンテナの内部を見ることはできません。
この隔離された空間を作り出すのは、OSのカーネルの機能です。OSを通して使用できるコンピュータのリソースを各コンテナごとに隔離して、ホストOS上で直接動作するプロセスや他のコンテナから独立した空間を作り出し、リソースを分割、分配、制限するわけです。
VMが仮想的なハードウェアの環境を作り出すのに比べて、コンテナ内では独立したOSの環境が作り出されるので、『OSレベルの仮想化』と呼ばれたりもします。つまりコンテナの内部では、他のコンテナやホストOSの環境と独立しているような、仮想的なOS環境が実現されているのです。
コンテナのメリット
簡単にコンテナの仕組みを説明したところで、この仕組みから得られるメリットをあげてみましょう。
高密度化が可能
コンテナではカーネルが直接プロセスを操作して隔離された空間を作り出すので、コンピュータ上で動作するOSは1つだけです。最低でも2つのOSが動作する必要のあるVMに比べると、消費するリソースは少なくて済みます。多数のコンテナを作成しても、動作しているOSは1つですので、VMに比べると高密度化が実現できます。
オーバーヘッドが少ない
ハードウェアの仮想化が不要で、隔離された空間を作るだけで済みますので、オーバーヘッドが少なくて済みます。
起動が速い
コンテナの起動は、OSから見ると単にプロセスが起動しているだけですので、通常のプロセスが起動するのとほとんど差はありません。つまり非常に速く起動できます。
アプリケーションのみの起動が可能
VMと異なり、コンテナ内ではinitが最初に起動して各種デーモンが起動する必要はありません。単にプロセスを隔離した環境を作り出せばコンテナを作成できますので、たとえばWebサーバだけが起動しているといったような、コンテナの中に1つだけプロセスが存在する環境を作れます。
このような環境は アプリケーションコンテナ と呼ばれます。もちろんinitを最初に起動して、通常のOSが起動するのと同じような環境も作成できます。こちらは システムコンテナ と呼ばれます。
コンテナのデメリット
逆にコンテナでは仕組み上、どうしてもできないことがあります。それをあげてみましょう。
異なるOSのシステム、プログラムは動かせない
起動しているOSは1つで、Linuxのカーネルが作り出している環境ですので、当然、他のLinux以外のOS用のプログラムは動作しませんし、コンテナ環境に他のOS(カーネル)はインストールできません。
Linuxの任意のディストリビューションはインストールできます。しかし、動作しているカーネルは1つですので、厳密にはディストリビューションを物理マシンやVMにインストールした環境と全く同じ環境ではありません。たとえばUbuntu上のコンテナにCentOSをインストールして起動したとしても、動作しているカーネルはUbuntuのカーネルです。
カーネルに関わる操作はできない
正確にはカーネルに関わる操作は可能です。しかしコンテナごとに異なる操作を行うことはできません。カーネルの機能で実現している環境ですので、当然全コンテナから見えるカーネルは同一です。したがって、コンテナから見えるデバイスやロードされているカーネルモジュールは同じになります。
以上、メリットとデメリットを挙げてみました。いずれもコンテナが持つ特徴に深く関わるものですので、コンテナが特別優れているわけでも、劣っているわけでもなく、その特徴を生かしてコンテナを利用すると良い場面、仕組み上コンテナの使用が適切ではない場面があるだけです。コンテナに向いた場面で利用するためにも、コンテナの仕組みを理解が必要となるでしょう。これは他の技術にも言えることですね。
Linuxカーネルのコンテナ関連機能 ~名前空間(Namespace)
コンテナの仕組みを簡単に見たところで、Linuxカーネルに実装されているコンテナに関連する機能のうち、代表的なものをあげて説明していきましょう。実は、Linuxカーネルに実装されているコンテナ関連の機能は、コンテナ専用の機能として実装されているものばかりではありません。もちろん、コンテナで使うことを念頭に開発されていますが、その機能は単独でも使用できます。
名前空間(Namespace)とは
プロセスをグループ化して、コンテナの隔離された空間を作り出す機能に重要な役割を果たすのが 名前空間 (Namespace) という機能です。名前空間は『名前空間』という機能がひとつ存在するわけでなく、独立させたいリソースによっていくつかの機能があります。
どのような名前空間があるかを表1で見てみましょう。
表1 名前空間の種類
名前空間の名前 | 隔離されるリソース | 実装されたカーネルバージョン |
マウント名前空間 | マウントの集合、操作 | 2.4.19 |
UTS名前空間 | ホスト名、ドメイン名 | 2.6.19 |
PID名前空間 | プロセスID(PID) | 2.6.24 |
IPC名前空間 | SysV IPCオブジェクト、POSIXメッセージキュー | 2.6.19 |
ユーザ名前空間 | UID、GID | 3.8 |
ネットワーク名前空間 | ネットワークデバイス、アドレス、ポート、ルーティングテーブル、フィルタなど | 2.6.26 |
執筆時点のカーネル 3.14 で実装されている名前空間は以上です。
OS起動時にはデフォルトの名前空間が存在し、デフォルトでは全てのプロセスはそのデフォルトの名前空間に属します。プロセスの起動時に独立した名前空間で実行する指定を行うと、そのプロセスは別の名前空間で実行されます。どのリソースを独立させたいかによって、表1に示した名前空間を単独で指定したり、組み合わせて指定したりします。
それでは、それぞれの名前空間を、実際にいくつかの名前空間を作りながらもう少し詳しく見ていきましょう。
ここでは名前空間の体験を行うためにunshare
というコマンドを使用します。unshare
コマンドはutil-linuxに含まれるコマンドで、引数に与えたコマンドを起動する際、オプションで与えた名前空間を新たに作成して、その名前空間内で実行します。unshare
はutil-linux 2.17以降に含まれますが、バージョンによって作成できる名前空間に差があります。ここではUbuntu 14.04LTS上で実行した例を示します。以下では、このベースとなるUbuntuの環境を『ホストOS』と呼ぶことにします。
マウント名前空間
Linuxカーネルに最初に実装された名前空間です。マウント名前空間を使うと、名前空間内で行ったマウント操作を、他の名前空間には反映させないといったことができます。もちろん、他の名前空間に反映させるという設定を行うこともできます。
この機能を使うと、コンテナ内でマウント操作を行った場合でも、そのマウントはホストOSや他のコンテナからは見えません。
それでは、マウント名前空間を試してみましょう。unshare
コマンドでマウント名前空間を指定して、シェルを実行し、その上でmount
コマンドを実行してみます。
ここでは/etc/hosts
を/root/hosts
にバインドマウントしています。その後で/root/hosts
を確認すると、/etc/hosts
と同じ内容であり、確かにバインドマウントされているのが確認できます。
さて、ここで別のシェルを実行して、同様に/root/hosts
を確認してみます。
unshare
で作成した名前空間内ではバインドマウントされていましたが、ホストOS上で直接実行しているシェル、つまり別のマウント名前空間から見るとサイズが0で、ファイルの中身は空です。つまりマウントは、unshare
コマンドで作成したマウント名前空間内でだけ有効であることがわかります。
マウント名前空間はかなり高機能で細かな制御が可能です。こちらの文書がかなり詳しいのでご参照ください。
UTS名前空間
UTS名前空間はシンプルな機能です。名前空間ごとにホスト名やドメイン名を独自に持つことができます。この機能によりコンテナごとにホスト名を指定できます。
UTS名前空間もunshare
コマンドを使って試してみましょう。
以上のようにホスト名が変わっています。ここで別のシェルを開いてホスト名を確認してみてください。
unshare
を実行したシェル上ではホスト名が変わっていました。しかしホストOS上では変わっていない事がわかります。unshare
を実行したシェルを抜けて元の名前空間に戻った後にhostname
コマンドを実行しても、ホスト名は元のままです。
IPC名前空間
IPC名前空間はプロセス間通信(IPC)オブジェクトを名前空間ごとに独立して持つことができます。具体的にはSystem V プロセス間通信オブジェクトと、POSIXメッセージキューです。
PID名前空間
PID名前空間は文字通りプロセスIDを名前空間ごとに持てます。通常はPIDは一意に決まる数字が振られますが、異なる名前空間にいるプロセスは同じPIDを持つことができます。
新しく作成したPID名前空間の最初のプロセスのPIDは、作成した名前空間内で見ると1となります。このプロセスは、名前空間を作る元となった親の名前空間でも別のPIDを持ちます。つまり親子関係のある名前空間では、それぞれの名前空間ごとにPIDを持っていて、カーネル内部でその対応関係を管理しています。
この機能は、親の名前空間から子の名前空間のプロセスを見ることはできます。逆に子の名前空間から親の名前空間のプロセスは見えません。
言葉で言うとわかりづらいですね。これも実際に実行例を見てみましょう。ここではunshare
ではなく、第1回で実行例を見たlxc-start
コマンドで実際にシステムコンテナを起動してみましょう。
lxc-start
コマンドを実行して、コンテナの起動を確認してからpstree
コマンドを実行してみてください。ここで表示されているPIDはlxc-start
を実行したホストOS上でのPIDです。
ここでコンテナ内に入ってプロセスを確認してみましょう。sudo lxc-console -n ubuntu01
と実行してログインしても、ssh経由でログインしても構いません。以下はホストOSからsshコマンドで確認しています。lxc-create
で作成したUbuntu環境にはpstree
コマンドがないのでインストールしています。
ホストOS上でプロセスを確認すると、lxc-start
の子プロセスとしてinit
が起動しており、init
の子プロセスとしていろいろなプロセスが起動しているのを見ることができています。つまりホストOS上からは、コンテナのプロセスを全て見ることができています。
コンテナ内では、ホストOSと同様にinit
を元にいろいろなプロセスが起動しているのが確認できています。確認できているのはコンテナ内のinit
から起動したプロセスのみです。つまりコンテナ内のプロセス以外は見えていません。コンテナ内で見る限りはそこがコンテナでないOSの環境なのか、コンテナ内の環境なのかはわかりません。
コンテナ内での最初のプロセスであるinit
のPIDはホストOSのinit
と同じで1となっています。つまり名前空間が異なると同じPIDが存在できることがわかります。
また、プロセスのPIDをホストOS、コンテナそれぞれの環境で見ると、それぞれの環境でPIDを持っており、そのPIDは異なっていることもわかります。
異なる名前空間で独立したPIDを持てるということは、コンテナのマイグレーションで異なるホストへコンテナを移動する場合も重要な機能となります。
ネットワーク名前空間
ネットワーク名前空間は各種のネットワークリソースを名前空間ごとに独立して持つことができます。具体的にはネットワークデバイス、IPアドレス、ルーティングテーブル、ポート番号、フィルタリングテーブルなどです。
この機能により、コンテナはホストOSとは別のIPアドレスを持てますし、ホストOS上で使用中のポートがあったとしても、コンテナでも同じ番号のポートを使うことができます。たとえば、ホストとコンテナの両方で、それぞれに割り当てた別のIPアドレスで、同じポート番号で待ち受けるウェブサーバを起動できます。また、ホストOSとコンテナの両方にeth0
という同じ名前のネットワークデバイスを持つこともできます。
この機能も簡単に体験しておきましょう。
新しいネットワーク名前空間を作成すると、ループバックインターフェースのみを持つ、ルーティングテーブルもフィルタリングテーブルも空の名前空間ができました。
この新しくできた名前空間をどう使うかを理解するには、名前空間以外の知識も必要になってきますので、この先の応用は、この連載の後で説明するコンテナで使うネットワーク関連の機能の説明のところで取り上げる予定です。
ユーザ名前空間
ユーザ名前空間は名前空間機能の中で最も最近実装された機能です。この機能はUID、GIDの空間を名前空間ごとに独立して持つことができます。名前空間内のUID、GIDとホストOS上のUID、GIDとが紐づけられ、名前空間の中と外で異なるUID、GIDを持つことができます。
この機能が最も効果を発揮する場面がコンテナ内のrootユーザを扱うときです。先に述べたようにホストOS上のUID、GIDと紐づけを行うことができますので、コンテナ内ではUID、GID共に0のrootユーザを、ホストOS上ではたとえばUID、GID共に100000である一般ユーザとして扱うことができます。
つまり、コンテナのrootがホストOS上では特権を持たないことになりますので、セキュアにコンテナ環境を提供することが可能になります。また一般ユーザがコンテナ環境を構築して起動するということが可能になりました。
独立したユーザ名前空間を作ることで、特権のないプロセスが今まで特権を必要としていた機能にアクセスすることができるようになったので、アプリケーションからこの機能を使うことで、新たな可能性が広がるように思います。
ユーザ名前空間の実例についても、この連載の後で取り上げる一般ユーザが扱うコンテナの話題のところで説明する予定です。
まとめ
今回はコンテナの仕組みを簡単に説明したあと、コンテナを実現するための重要な機能である名前空間機能について説明しました。今回説明したのは3.14カーネルに実装されている機能です。現時点でコンテナを実現するための主要な機能は揃っていると言えるでしょう。一方で、カーネルの開発メーリングリストにはまだいろいろな名前空間機能が提案され、議論がなされています。まだまだ発展途上で目が離せない興味深い機能です。
次回はLinuxカーネルに実装されているもうひとつの重要なコンテナ関連機能であるcgroupについて説明したいと思います。
LXC公式サイトの翻訳
最後にひとつ宣伝を。前回の記事で紹介したLXCの公式サイトを有志で日本語に翻訳してhttp://lxc-jp.github.io/で公開しています(※)。
一度ご覧になってみてください。