LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第2回コンテナの仕組みとLinuxカーネルのコンテナ機能[1]名前空間とは

前回は、Linuxで使えるコンテナの実装を説明したあと、LXCを使って簡単にコンテナの作成、起動、停止が行えるところを紹介しました。今回は、そのコンテナの仕組みを簡単に説明し、その仕組みからくるメリット・デメリットを紹介した後、コンテナはカーネルのどのような仕組みを使って動作しているのかを簡単に説明していきたいと思います。

コンテナの仕組み

コンテナをまだ使ったことがない方でも、VMwareやVirtualBox、KVMといった仮想マシン(VM)を使ったことはあるという方は多いのではないでしょうか。まずはVMとの比較をしながら、コンテナの仕組みを説明してみましょう。

図1 VMとコンテナの仕組み(1)仮想マシン
図1 VMとコンテナの仕組み(1)仮想マシン

VMでは図1のように、コンピュータの上で動くOSやVMを実現するためのハイパーバイザの上で、実際のハードウェアをエミュレートするVMが動きます。つまり実際の物理的なコンピュータと同じようなものがソフトウェアによって実現されているので、このVMを使うにはOSが必要になります。つまり、図1のように物理的なコンピュータ上で動くOSの上でさらにOSが動きます。また、VMを動かしているOSからはVMのプロセスが見えているだけで、VM上で動作しているプロセスは見えません。

図2 VMとコンテナの仕組み(2)コンテナ(OSレベル仮想化)
図2 VMとコンテナの仕組み(2)コンテナ(OSレベル仮想化)

一方コンテナは、図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、GID3.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コマンドを実行してみます。

$ sudo unshare --mount /bin/bash
 # touch /root/hosts
 # mount -o bind /etc/hosts /root/hosts
 # ls -l /root/hosts
 -rw-r--r-- 1 root root 224 Jul 13  2012 /root/hosts
 # cat /root/hosts
 127.0.0.1  localhost
  :(略)

ここでは/etc/hosts/root/hostsにバインドマウントしています。その後で/root/hostsを確認すると、/etc/hostsと同じ内容であり、確かにバインドマウントされているのが確認できます。

さて、ここで別のシェルを実行して、同様に/root/hostsを確認してみます。

$ sudo -s
 # ls -l /root/hosts
 -rw-r--r-- 1 root root 0 May 17 14:01 /root/hosts
 # cat /root/hosts
 #

unshareで作成した名前空間内ではバインドマウントされていましたが、ホストOS上で直接実行しているシェル、つまり別のマウント名前空間から見るとサイズが0で、ファイルの中身は空です。つまりマウントは、unshareコマンドで作成したマウント名前空間内でだけ有効であることがわかります。

マウント名前空間はかなり高機能で細かな制御が可能です。こちらの文書がかなり詳しいのでご参照ください。

UTS名前空間

UTS名前空間はシンプルな機能です。名前空間ごとにホスト名やドメイン名を独自に持つことができます。この機能によりコンテナごとにホスト名を指定できます。

UTS名前空間もunshareコマンドを使って試してみましょう。

$ hostname                      (unshare実行前にホスト名確認)
lxctest.example.jp
$ sudo unshare --uts /bin/bash  (UTS名前空間を指定してunshare実行)
# hostname container.example.jp (ホスト名をcontainer.example.jpに変更)
# hostname                      (ホスト名を確認)
container.example.jp            (指定したホスト名に変更されている)

以上のようにホスト名が変わっています。ここで別のシェルを開いてホスト名を確認してみてください。

$ hostname
lxctest.example.jp

unshareを実行したシェル上ではホスト名が変わっていました。しかしホストOS上では変わっていない事がわかります。unshareを実行したシェルを抜けて元の名前空間に戻った後にhostnameコマンドを実行しても、ホスト名は元のままです。

# hostname
container.example.jp
# exit
exit
$ hostname
lxctest.example.jp

IPC名前空間

IPC名前空間はプロセス間通信(IPC)オブジェクトを名前空間ごとに独立して持つことができます。具体的にはSystem V プロセス間通信オブジェクトと、POSIXメッセージキューです。

PID名前空間

PID名前空間は文字通りプロセスIDを名前空間ごとに持てます。通常はPIDは一意に決まる数字が振られますが、異なる名前空間にいるプロセスは同じPIDを持つことができます。

新しく作成したPID名前空間の最初のプロセスのPIDは、作成した名前空間内で見ると1となります。このプロセスは、名前空間を作る元となった親の名前空間でも別のPIDを持ちます。つまり親子関係のある名前空間では、それぞれの名前空間ごとにPIDを持っていて、カーネル内部でその対応関係を管理しています。

この機能は、親の名前空間から子の名前空間のプロセスを見ることはできます。逆に子の名前空間から親の名前空間のプロセスは見えません。

言葉で言うとわかりづらいですね。これも実際に実行例を見てみましょう。ここではunshareではなく、第1回で実行例を見たlxc-startコマンドで実際にシステムコンテナを起動してみましょう。

$ sudo lxc-start -n ubuntu01 -d
$ pstree -p
init(1)-+-acpid(1069)
  :(略)
        |-lxc-start(3060)---init(3071)-+-cron(3475)
        |                              |-dhclient(3393)
        |                              |-getty(3461)
        |                              |-getty(3464)
        |                              |-getty(3465)
        |                              |-getty(3507)
        |                              |-getty(3628)
  :(略)

lxc-startコマンドを実行して、コンテナの起動を確認してからpstreeコマンドを実行してみてください。ここで表示されているPIDはlxc-startを実行したホストOS上でのPIDです。

ここでコンテナ内に入ってプロセスを確認してみましょう。sudo lxc-console -n ubuntu01と実行してログインしても、ssh経由でログインしても構いません。以下はホストOSからsshコマンドで確認しています。lxc-createで作成したUbuntu環境にはpstreeコマンドがないのでインストールしています。

$ ssh -l ubuntu 10.0.3.18 apt-get install psmisc
$ ssh -l ubuntu 10.0.3.18 pstree -p
init(1)-+-cron(380)
        |-dhclient(282)
        |-getty(355)
        |-getty(358)
        |-getty(359)
        |-getty(400)
        |-getty(548)
  :(略)

ホスト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という同じ名前のネットワークデバイスを持つこともできます。

この機能も簡単に体験しておきましょう。

$ sudo unshare --net /bin/bash
# ip link show
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN mode DEFAULT group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
# iptables -L -n -v
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain FORWARD (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

Chain OUTPUT (policy ACCEPT 0 packets, 0 bytes)
 pkts bytes target     prot opt in     out     source               destination

新しいネットワーク名前空間を作成すると、ループバックインターフェースのみを持つ、ルーティングテーブルもフィルタリングテーブルも空の名前空間ができました。

この新しくできた名前空間をどう使うかを理解するには、名前空間以外の知識も必要になってきますので、この先の応用は、この連載の後で説明するコンテナで使うネットワーク関連の機能の説明のところで取り上げる予定です。

ユーザ名前空間

ユーザ名前空間は名前空間機能の中で最も最近実装された機能です。この機能は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/で公開しています⁠。

一度ご覧になってみてください。

※)
現在は公式サイトの日本語訳は公式サイト上でご覧になれます。

おすすめ記事

記事・ニュース一覧