コンテナの歴史:chrootからDocker、OCI、Kubernetesまで
Dockerはコンテナ化をどう実現しているのか。chroot、FreeBSD jail、Solaris Zones、OpenVZ、cgroupsと名前空間、LXC、OCI、Kubernetes、Podmanまでを一次資料の日付でたどり、コンテナ型VPSとKVMの違いも解説します。
Dockerはコンテナ化をどのように行うのですか
Docker はコンテナ化を一から発明したわけではありません。Linux カーネルの名前空間 (namespaces) でプロセスから見える範囲を区切り、cgroups (control groups) でそのプロセスが使える CPU やメモリを区切ります。Docker はその上に、イメージの層 (layer) と、イメージを配るレジストリと、短いコマンドで操作できる CLI を載せました。
Docker の公式ドキュメントも、コンテナという隔離された作業空間は namespaces という技術で作る、と説明しています。つまり Docker の中心にある仕組みは、Docker より前から少しずつ積み上がってきた部品です。chroot がファイルシステムの見え方を変えました。FreeBSD jail が root ごと閉じ込めました。Solaris Zones と OpenVZ が「OS の中に別の OS 環境を作る」ことを製品にしました。その後 Linux カーネル本体に cgroups と名前空間が入り、LXC がそれをまとめ、Docker が配り方を整えました。
以下の日付は、各プロジェクトの man ページ、リリース告知、公式ブログ、ソースコードで確認したものです。日本語の解説記事では jail や Zones の年がばらばらに書かれていることが多いので、出典の種類も添えています。
Docker ≠ コンテナ:先に言葉を分ける
日本語の記事では「Docker」と「コンテナ」がほぼ同じ意味で使われがちです。しかし二つは別のものです。コンテナは、OS のカーネルが用意する隔離機能で囲まれた、普通のプロセスです。この記事で扱う Linux のコンテナなら、Linux カーネルの名前空間と cgroups で囲まれた Linux プロセスです。ただし、コンテナは Linux だけのものではありません。Microsoft のドキュメントは「Windows ベースまたは Linux ベースのコンテナ」を挙げ、Windows Server でもコンテナをネイティブに動かせると書いています。同じページは、コンテナはホスト OS のカーネルの上で動くとも説明しています。Docker の公式ドキュメント自身は、コンテナを「実行に必要なファイルをすべて持った、隔離されたプロセス」と説明しています。Docker は、そのコンテナを作って動かすための道具の一つです。同じ Linux カーネルの機能は LXC からも使えます。OCI (Open Container Initiative) の仕様に沿ったコンテナは、runc や containerd、Podman でも動き、Docker のデーモンは要りません。だから「Docker を使わない」ことと「コンテナを使わない」ことは同じ意味ではありません。Kubernetes が 1.24 で外したのも Docker Engine と話すためのアダプターで、コンテナを動かす役目は containerd や CRI-O などのランタイムが担っています。
chroot:ファイルシステムの見え方だけを変える
最初の部品は chroot です。chroot() システムコールは、プロセスにとってのルートディレクトリ / を、指定したディレクトリに付け替えます。FreeBSD と OpenBSD の chroot(2) man ページは、この呼び出しが Version 7 AT&T UNIX で登場したと記録しています。NetBSD の chroot(2) は、BSD には 4.2BSD で入ったと書いています。どの man ページも年を書いていないので、ここでも年は書きません。
chroot が変えるのは、ファイルシステムの見え方だけです。プロセスの一覧やユーザー、ネットワークは、ホストと共有したままです。使える CPU やメモリにも上限はかかりません。後の時代の技術は、この「見え方を変える」という考え方を、ファイル以外のものにも広げていきました。
FreeBSD jail はいつ登場し、何を閉じ込めたのか
次の大きな一歩は FreeBSD の jail です。jail(2) と jail(8) の man ページは、どちらも FreeBSD 4.0 で登場したと記録しています。FreeBSD 4.0 の公式リリース告知は 2000年3月14日付けです。jail(2) の AUTHORS 欄によると、jail は Poul-Henning Kamp が R&D Associates のために書き、同社が FreeBSD に寄贈しました。
FreeBSD ハンドブックは、jail が chroot のモデルを広げ、ファイルシステムに加えて、ユーザーの集合とネットワークのサブシステムへのアクセスも仮想化すると説明しています。Architecture Handbook は、jail が環境を chroot し、その中で生まれるプロセスに制限をかける、と書いています。大事なのは root の扱いです。同じ文書は、jail の中の root が乗っ取られても「devastation (壊滅) ではなく annoyance (迷惑) で済む」と表現しています。ただし同じページは、jail は強い隔離境界だが絶対的なものではない、とも書いています。
jail(8) によると、階層型で拡張可能な jail は FreeBSD 8.0 で入りました。jail(2) は、拡張可能な jail パラメーターと階層型 jail を後から加えたのは James Gritton だと記しています。Docker とは設計の考え方がかなり違うので、FreeBSD jail と Docker コンテナの違いで詳しく比べています。
Solaris Zones:OS の中の隔離された環境
Sun の Solaris でも同じ流れがありました。Oracle が公開している Solaris 10 の文書は、Zones を「Solaris Express 2/04 リリースで新しく入った機能」と書き、Solaris Express 7/04 で機能が追加されたと続けています。同じ文書群は、Solaris Zones を Solaris 10 の重要な機能として紹介しています。
Oracle の説明では、Zones はアプリケーションを動かすための隔離された環境で、Solaris Container 環境の部品の一つです。OS ベンダーの公式文書で「Container」が製品の名前として使われている点は、この後の歴史を読むうえで覚えておく価値があります。Solaris そのものの流れと、その後の illumos についてはSolaris と illumos の歴史にまとめてあります。
Linux-VServer、Virtuozzo、OpenVZ:コンテナ型 VPS の正体
Linux では、カーネル本体に機能が入る前に、外部のパッチと商用製品が先に進みました。早い例の一つに Linux-VServer があります。
商用製品では SWsoft の Virtuozzo が代表です。OpenVZ wiki の History ページによると、2001年12月に Virtuozzo for Windows のプロジェクトが始まり、2002年1月に SWsoft が Linux 向けの製品 Virtuozzo を出しました。SWsoft は現在の Virtuozzo 社です。2005年、SWsoft は Virtuozzo の中核を GNU GPL で公開するために、OpenVZ プロジェクトを作りました。
OpenVZ wiki は OpenVZ を「Linux のためのコンテナ型仮想化」と呼び、コンテナを VE や VPS とも呼んでいます。Features ページによると、コンテナは常にホストと同じ OS カーネルで動きます。カーネルが一つだけなので、オーバーヘッドはほぼゼロだと説明しています。一台の物理サーバーでカーネルを一つだけ動かし、その上を多数の VPS に分ける。これがコンテナ型 VPS の仕組みです。メインフレームからクラウドまでのコンピューターの歴史にも OpenVZ の名前は出てきますが、中身はこの「カーネル一つを共有する」という一点に尽きます。
OpenVZ はカーネル本体にも部品を返しています。同じ History ページには、当時の OpenVZ のカーネルへの貢献は PID、IPC、ネットワークの名前空間で、いちばん大きかったのはネットワーク名前空間だった、と書かれています。
cgroups と名前空間は Linux にいつ入ったのか
Docker の土台になる部品は、Linux 2.6.24 でそろいました。kernelnewbies の変更点まとめによると、2.6.24 は 2008年1月24日に出て、Task Control Groups と PID 名前空間、基本的なネットワーク名前空間を取り込みました。kernelnewbies はコミュニティのまとめですが、同じことは v2.6.24 のソースツリーそのものでも確かめられます。
名前空間は、プロセスから見える世界を区切る機能です。v2.6.24 の include/linux/sched.h には、新しい PID 名前空間を作る CLONE_NEWPID と、新しいネットワーク名前空間を作る CLONE_NEWNET が定義されています。マウント (CLONE_NEWNS)、ホスト名 (CLONE_NEWUTS)、プロセス間通信 (CLONE_NEWIPC) のフラグも、この版にはすでにあります。
cgroups は、プロセスの集まりに資源の上限や配分をかける機能です。v2.6.24 の Documentation/cgroups.txt は Google の Paul Menage が書いたもので、cpusets の文書を元にしていると明記しています。その文書は cgroups を、タスクとその子孫を階層化されたグループにまとめる仕組みとして説明しています。この文書には「container」という語は一度も出てきません。現在のカーネルの kernel/cgroup/cgroup.c の冒頭には、2006年の Google の著作権表示と、cpuset から Paul Menage が切り出したという一文が残っています。マージ前の議論は、LWN の 2007年5月29日の記事「Process containers」で読めます。
名前空間が「何が見えるか」を決め、cgroups が「どれだけ使えるか」を決めます。この二つが本体のカーネルに入ったので、OpenVZ のような独自パッチのカーネルがなくても、コンテナの基本的な部品が使えるようになりました。
自分の VPS で名前空間と cgroups を確かめる
この仕組みは、普通の Ubuntu や Debian の VPS ですぐに確かめられます。まず、サーバー自体が何の上で動いているかを見ます。
systemd-detect-virt
systemd-detect-virt --containerKVM の VPS なら、最初のコマンドは kvm と表示します。OpenVZ や Virtuozzo のコンテナなら openvz、LXC なら lxc です。--container は、カーネルを共有する型の仮想化だけを検出します。何も検出しなかったときは、終了コードが 0 以外になります。
次に、今のシェルが属している名前空間を見ます。
ls -l /proc/self/ns
readlink /proc/self/ns/net
sudo unshare --net readlink /proc/self/ns/netls は net や pid など、名前空間ごとのリンクを並べます。二つの readlink は、角かっこの中に違う番号を表示するはずです。unshare --net が新しいネットワーク名前空間を作り、その中で readlink を動かしたからです。同じ名前空間にいるプロセスどうしなら、この番号は同じになります。
PID 名前空間も試せます。
sudo unshare --pid --fork --mount-proc ps -ef見出しの下には ps -ef 自身の1行だけが出て、その PID は 1 です。--pid が新しい PID 名前空間を作り、--fork が ps をその中の子プロセスとして起動し、--mount-proc がその名前空間用の /proc をマウントするからです。--mount-proc を付け忘れると、ps はホストの /proc を読むので、ホストのプロセスがすべて見えてしまいます。名前空間が区切るのは「見え方」なので、見る道具がどの /proc を読むかで結果が変わります。
最後に cgroups です。
cat /proc/self/cgroupカーネルの cgroup v2 の文書によると、cgroup v2 のエントリーは常に 0::$PATH の形です。0::/user.slice/... のような1行だけが出れば、そのサーバーは cgroup v2 だけで動いています。複数の行が出るなら、古い cgroup v1 の階層も使われています。Docker や Podman がコンテナを作るときも、裏ではこれと同じカーネル機能を呼んでいます。違うのは、その手順を毎回人が打たなくて済むようにした点です。
LXC:カーネル機能をまとめて使う道具
カーネルに部品が入っても、それを正しく組み合わせるのは手間がかかります。LXC はその仕事を引き受けました。linuxcontainers.org は LXC を「Linux カーネルの隔離機能に対するユーザー空間のインターフェース」と説明しています。使う機能として、カーネルの名前空間 (ipc、uts、mount、pid、network、user) や cgroups、seccomp、AppArmor と SELinux のプロファイル、capabilities を挙げています。
LXC の git リポジトリでは、最初のリリースのタグ lxc_0_1_0 が 2008年8月6日の日付です。2.6.24 の約半年後です。LXC は今も開発が続いていて、現在の長期サポート版は 2026年4月30日に出た LXC 7.0 です。告知によると 7.0 は 2031年6月までサポートされ、それまでの 6.0 系は重大なバグ修正とセキュリティ更新だけを受ける、ゆっくりした保守に移りました。KVM や Xen のような仮想マシンと、LXC のようなコンテナが VPS でどう違うかは、KVM、Xen、LXC の VPS での違いで比べています。
Docker が足したもの:イメージとレジストリ
Docker 自身の変更履歴では、最初の公開版 0.1.0 は 2013年3月23日です。その項目は「Initial public release」で、イメージを push と pull で受け渡すためのレジストリの実装も含まれていました。
Docker が新しく持ち込んだのは、隔離の仕組みではなく、配り方と使い方です。公式ドキュメントでは、イメージはコンテナを作るための読み取り専用のテンプレートです。Dockerfile の命令一つが、イメージの層を一つ作ります。層は積み重ねで、上の層は下の層に中身を足していきます。イメージはレジストリに置かれ、既定の公開レジストリは Docker Hub です。Docker はクライアント・サーバー型で、docker コマンドがデーモンに指示を出し、デーモンがコンテナを作ります。隔離は名前空間と cgroups に任せ、Docker は「同じものを、どのサーバーでも同じ手順で動かす」ことを受け持ちました。
初期の Docker は、コンテナを作る処理を LXC に任せていました。2014年3月10日の Docker 公式ブログによると、Docker 0.9 で実行ドライバー (execution driver) の仕組みが入り、Docker 自身が書いた libcontainer が既定のドライバーになりました。ブログは、libcontainer で安定性が大きく上がったことを理由に挙げ、「Docker 0.9 から LXC はオプションになった」と書いています。LXC ドライバーは消えたのではありません。docker -d -e lxc で選べる別のドライバーとして残りました。
Docker を自分のサーバーで動かす具体的な手順は、VPS で Docker を動かす方法にまとめています。
OCI、runc、containerd:Docker の中身が標準になる
2015年6月22日、Docker は Linux Foundation の下で進める Open Container Project を発表しました。同じ日の Docker のブログは、コンテナの基本フォーマットとランタイムの runC を寄贈すると書いています。runC は、libcontainer の中身を Docker なしで単独で動くようにしたものです。現在の OCI の公式サイトは、OCI が 2015年6月22日に Docker や CoreOS など、コンテナ業界の主要な企業によって発足したと説明しています。OCI は今、ランタイム、イメージ、配布の各仕様を持っています。配布の仕様は 2021年5月に v1.0.0 になりました。
runc は長いあいだ候補版 (rc) のまま使われました。GitHub のリリースでは、1.0.0-rc1 が 2016年6月3日、1.0.0 が 2021年6月22日です。1.0.0 のリリースノートは、5年ぶりの rc ではないリリースだと書いています。資料によっては runc 1.0 を 2017年としていますが、正式な 1.0.0 は 2021年です。
containerd は、CNCF (Cloud Native Computing Foundation) が扱うコンテナランタイムです。CNCF のプロジェクトページによると、2017年3月29日に Incubating として受け入れられ、2019年2月28日に Graduated になりました。
Kubernetes はなぜ dockershim を外したのか
Kubernetes v1 は、2015年7月21日の Google 公式ブログで発表されました。記事の題は「Kubernetes V1 Released - Cloud Native Computing Foundation to Drive Container Innovation」で、CNCF の設立も同時に伝えています。
Kubernetes のノードでコンテナを動かすのは kubelet です。kubelet は CRI (Container Runtime Interface) という仕様でランタイムと話します。Kubernetes のドキュメントによると、Docker は CRI より前からあったので、Kubernetes は Docker Engine を CRI 対応のランタイムのように扱うアダプター、dockershim を作りました。dockershim の削除は v1.20 (2020年12月8日) の時点で予告され、v1.24 (2022年5月3日) で実際に削除されました。1.24 で非推奨になったのではなく、1.24 で消えたのです。現在のドキュメントは、ランタイムの選択肢として containerd、CRI-O、Docker Engine、Mirantis Container Runtime を挙げています。
Podman:デーモンなしで同じような CLI
Podman 1.0.0 は、2019年1月11日に GitHub で公開されました。README は Podman を「OCI コンテナとポッドを管理するツール」と説明しています。特徴としては、管理デーモンを持たないことと、Docker 互換の CLI、setuid バイナリなしで一般ユーザーとして動かせることを挙げています。ここでも隔離そのものは同じカーネル機能で、変わったのは誰がそれを呼ぶかです。VPS でどちらを選ぶかは、VPS での Podman と Docker の比較で検討しています。
年表:どの時代が何を足したか
各項目の出典は、本文の該当する節で挙げた資料です。
- Version 7 AT&T UNIX:chroot() が登場。プロセスのルートディレクトリを付け替える。
- 4.2BSD:chroot() が BSD に入る。
- 2000年3月、FreeBSD 4.0:jail が登場。ユーザーとネットワークまで区切り、jail の中の root を閉じ込める。
- 2002年1月:SWsoft が Linux 向けの Virtuozzo を出す。
- Solaris Express 2/04:Solaris Zones が新機能として入り、Solaris 10 の主要機能になる。
- 2005年:SWsoft が Virtuozzo の中核を OpenVZ として GPL で公開する。
- 2008年1月24日、Linux 2.6.24:cgroups と PID 名前空間、基本的なネットワーク名前空間が本体に入る。
- 2008年8月6日:LXC の最初のタグ
lxc_0_1_0。 - 2013年3月23日:Docker 0.1.0。レジストリ付きの最初の公開版。
- 2014年3月、Docker 0.9:libcontainer が LXC に代わって既定になる。
- 2015年6月22日:Open Container Project の発表。Docker が runC を寄贈。
- 2015年7月21日:Kubernetes v1。
- 2017年3月29日:containerd が CNCF に受け入れられる。
- 2019年1月11日:Podman 1.0.0。
- 2021年6月22日:runc 1.0.0。
- 2022年5月3日:Kubernetes 1.24 で dockershim を削除。
コンテナはホストのカーネルを共有する:VPS にとっての意味
この歴史から、サーバーを選ぶときに効く事実が一つ出てきます。コンテナはカーネルを持たない、ということです。Docker の公式ドキュメントは、複数のコンテナを動かしても、すべて同じカーネルを共有すると書いています。一方で仮想マシンは、自分のカーネルやハードウェアドライバーを持つ OS 全体だと説明しています。Proxmox のドキュメントも、コンテナはホストのカーネルを使うと書いています。そして、コンテナを QEMU の仮想マシンの中に入れて動かすことを、今も推奨の運用としています。
カーネルが共有なので、カーネルに関わることはホスト側が決めます。Docker を例にとると、カーネルモジュールを読み込む権限 SYS_MODULE は、既定ではコンテナに与えられません。--cap-add で足すか、すべての権限を与える --privileged を使う必要があります。たとえ権限があっても、読み込まれるのはホストのカーネルへのモジュールです。コンテナの中でカーネルの版を選ぶことはできません。
コンテナ型 VPS も同じ構造です。OpenVZ の wiki が書くとおり、OpenVZ のコンテナは常にホストと同じカーネルで動きます。つまり、特定のカーネル機能やモジュール、カーネルの版が必要なソフトは、ホストの設定しだいで動いたり動かなかったりします。コンテナ型 VPS の中で Docker を動かせるかどうかも、プロバイダーとその製品の版によって変わります。契約の前に、プロバイダーのドキュメントでモジュールの読み込みと Docker の対応を確認してください。
KVM の VPS は仮想マシンなので、自分のカーネルを持っています。Docker を入れるときも、物理サーバーに入れるのと同じ手順で、自分のカーネルの名前空間と cgroups を使えます。今のサーバーがどちらなのかは、先ほどの systemd-detect-virt で確かめられます。kvm なら仮想マシン、openvz や lxc ならホストとカーネルを共有するコンテナです。
FAQ
Dockerはコンテナ化をどのように行うのですか?
Docker は、Linux カーネルの名前空間でプロセスから見える範囲を区切り、cgroups で CPU やメモリの使用量を区切ります。この二つは Linux 2.6.24 (2008年1月) などで本体に入ったカーネル機能で、Docker が発明したものではありません。Docker が足したのは、読み取り専用の層を積み重ねたイメージと、それを配るレジストリ、docker コマンドとデーモンによる操作の形です。
Docker とコンテナは同じものですか?
同じではありません。コンテナは、カーネルの隔離機能で囲まれた普通のプロセスです。Linux のコンテナなら名前空間と cgroups で囲まれた Linux プロセスで、Windows Server にはネイティブに動く Windows コンテナもあります。Docker はそれを作って動かす道具の一つです。Linux の同じ機能は LXC からも使え、OCI 仕様のコンテナは runc や containerd、Podman でも Docker のデーモンなしで動きます。
最初のコンテナ技術は chroot と FreeBSD jail のどちらですか?
chroot が先です。chroot() は Version 7 AT&T UNIX で登場し、4.2BSD で BSD に入りました。ただし chroot が変えるのはファイルシステムの見え方だけです。プロセスやユーザー、ネットワークまで区切ったのは、2000年3月の FreeBSD 4.0 で登場した jail です。どちらを「最初のコンテナ」と呼ぶかは、どこまで区切ればコンテナと呼ぶかで変わります。
Kubernetes は Docker を使えなくなったのですか?
Kubernetes が v1.24 (2022年5月3日) で削除したのは、kubelet が Docker Engine と話すためのアダプター dockershim です。削除は v1.20 の時点で予告されていました。kubelet は CRI でランタイムと話し、ドキュメントは containerd、CRI-O、Docker Engine、Mirantis Container Runtime を選択肢として挙げています。
コンテナ型 VPS で Docker は動きますか?
プロバイダーと製品の版によります。OpenVZ のようなコンテナ型 VPS はホストとカーネルを共有するので、カーネルモジュールやカーネルの版はホスト側が決めます。Docker の対応状況は、プロバイダーのドキュメントで確認してください。KVM の VPS は自分のカーネルを持つので、物理サーバーと同じ手順で Docker を入れられます。今のサーバーの種類は systemd-detect-virt で確かめられます。