コンテナユーザなら誰もが使っているランタイム「runc」を俯瞰する[Container Runtime Meetup #1発表レポート]
こんにちは、NTTの徳永です。本稿では、コンテナユーザなら誰もが使っていると言っても過言ではない、コンテナランタイムの筆頭「runc」に注目し、その概要を仕様と実装の両面から俯瞰します。本稿は私が主催者の一人として参加した「Container Runtime Meetup #1」で発表した内容をベースにしています。詳しい内容は発表資料もぜひご参照ください。
コンテナランタイムとは
Kubernetes等のコンテナオーケストレータを用いてアプリケーションをコンテナ(Pod)として実行するとき、実際にコンテナの作成をしているのは誰でしょうか。実はKubernetesはコンテナを直接触らず、あるソフトウェアを用います。まさにそれがコンテナランタイム(以降、ランタイム)です。つまり、ランタイムはクラスタを構成する各ノード上で稼動し、上位のオーケストレータからの指示に応じ実際にコンテナを作成・管理します。よく使われるものにはDockerやcontainerd、runc等があり、耳にしたことのある方も多いと思います。
コンテナランタイムの2つのレイヤ
ランタイムの実装には様々なものがありますが、それらは役割に応じて下図に示すように高レベルランタイム(CRIランタイム)と低レベルランタイム(OCIランタイム)という2つのレイヤに分類されます。
このようにランタイムには様々なものがあり、それぞれに特徴があります。それらの比較については拙過去記事「今話題のいろいろなコンテナランタイムを比較してみた」もご参照ください。
低レベルランタイムと標準仕様
本稿で注目する低レベルランタイムはしばしばOCIランタイムと呼ばれます。この「OCI」とは、コンテナ技術の標準仕様を策定する団体であるOpen Container Initiativeの略称です。runcを含む低レベルランタイムは、その仕様がOCIにより標準として定められており、それにちなんでOCIランタイムと呼ばれることがあります。この仕様のお陰で、様々な低レベルランタイムが存在しながらも、それらを高レベルランタイムから仕様に沿った統一的なインタフェースで利用することができています。runcに注目する前に、まずこの標準仕様から眺めてみましょう。
Standard Containers
OCIはコンテナに関する仕様策定にあたり、まずはそもそも「コンテナとは何か」に関する原則を、Standard Containersとして定義しています。その概要を下図に示しますが、その多くは日頃私達がコンテナを扱う際によく目にする、馴染み深い性質ばかりです
これらのコンテナの原則を実現するために、策定中のものも含めOCIは以下のような標準仕様を定めています。
- Image Specification:コンテナイメージの標準仕様。
- Runtime Specification:コンテナランタイムの標準仕様。
- Distribution Specification:コンテナレジストリの標準仕様。
ここからは特に、runcを含むOCIランタイムの仕様であるRuntime Specificationに注目します。
OCI Runtime Specification
Runtime SpecificationはOCIが定めるランタイムの標準仕様です。
この仕様は主に以下のような仕様を定義しています。
コンテナを作成するには、まず「コンテナの素」を用意する必要があり、これがFilesystem bundleとして定義されています。さらに、仕様ではコンテナに対して行うことのできる操作(create、start、kill、delete等)とそれによってコンテナが辿るライフサイクルが定義されています。OCIランタイムはそれらの操作に対応するインタフェースを持っており、ユーザ(または高レベルランタイム)の視点から見ると、これらの操作をOCIランタイムに指示することでコンテナを管理していくことになります。
この仕様に沿って実装された低レベルランタイムが、OCIランタイムと呼ばれます。以降、特にruncに着目して、その概要を俯瞰します。
OCIランタイムの筆頭runcを俯瞰する
OCI Runtime Specificationを実装するランタイムにはさまざまなものがありますが、その中でも筆頭と言えるのがruncです。コンテナ環境において広く利用されているDockerも、デフォルトでruncを用いているため、意識せずとも多くの人がruncを利用していると言えるでしょう。
runcの概要
runcはOCIにより開発が進められているランタイムで、Runtime specificationのリファレンス的実装と言えます。面白いことに、runcは多くのコンテナユーザに使われているものの、2019年10月18日現在でバージョンはv1.0.0-rc9で、まだv1.0.0には到達していません。とはいえ、現状クラウド領域のユースケースで求められる機能は一通り実装されており、これまで多くのユーザに利用されてきた実績もありますので、利用に差し支えは無いでしょう。以下に示すように、runcはLinuxの提供する隔離機能やセキュリティ関連の機能を用いて、コンテナの隔離環境を作成します。
前節で、コンテナは、仕様で定義される操作(create、start、kill、delete等)を用いて作成・管理されると述べましたが、runcにおいてそれがどう実装されているのか、実際にruncを触りながら見てみましょう。
コンテナの素、Filesystem bundleの作成
まず、コンテナの素となるFilesystem bundleを作成します。それには、まずコンテナを構成するrootfsデータを得る必要がありますが、これはdocker export
コマンド等を利用することで手軽に得られます。
$ docker run --rm -d --name ubuntu ubuntu:18.04 tail -f /dev/null
$ docker export ubuntu > rootfs.tar
$ docker kill ubuntu
次に、このrootfsデータ(rootfs.tar
)を用いてFilesystem bundleを作成します。ここでコンテナ内の環境設定を記述するファイルが必要になりますが、本稿では簡易的にrunc spec
コマンドで得られるものを利用します。
$ mkdir -p bundle/rootfs
$ tar xf rootfs.tar -C bundle/rootfs
$ runc spec -b bundle
この時点で、bundle
ディレクトリにはrootfsデータ、環境設定ファイルが格納されており、これをコンテナの素であるFilesystem bundleとして用いることができます。
コンテナの作成
次に、Filesystem bundleからコンテナを作成します。仕様ではコンテナの作成と実行がそれぞれ独立の操作として定義されており、runcにもそれに対応するcreate
、start
サブコマンドが実装されています。今回は、簡単のためにこれらサブコマンドを一つにまとめたrun
サブコマンドを用います。このサブコマンドに--bundle
オプションを付与し、先程作成したFilesystem bundleをコンテナの素としてruncに与えます。下記コマンドをrootユーザで実行すると、コンテナを起動しそのシェルを利用することができます。
# runc run --bundle bundle demo1
(コンテナ内)# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
...
このように、runcを用いてコンテナを起動できることが確認できました。実行したコンテナは以下のようにkill
サブコマンドを用いて停止することができます。必要に応じてdelete
サブコマンドでコンテナを削除します。
# runc kill demo1 KILL
# runc delete demo1
runc runによるコンテナ作成の流れ
ここまででruncの概要とOCI仕様との関係について見てきました。それでは、ここからはruncによるコンテナ作成の流れをもう少し詳しく見てみましょう。上述したように、runcのrun
サブコマンドはコンテナの作成・実行操作であるcreate
サブコマンドとstart
サブコマンドを組み合わせて実装されているため、コンテナ作成の流れを俯瞰する題材としてちょうど良いサブコマンドです。本稿では、run
サブコマンドに注目し、runcがコンテナを作成・実行する流れを俯瞰します。
コンテナ作成においては登場人物が2人います。ひとつは、ユーザが実行したrunc run
コマンドです。もうひとつはrunc init
コマンドで、これはrunc run
の実行中に起動されます。runc init
はLinuxの環境隔離機能であるnamespace等を用いて隔離環境を作成・初期化しrunc run
から指示されたタイミングでコンテナのエントリポイントとなるプログラムを実行するという役割を持ちます。つまり、runc run
とrunc init
が並列に協調して動作することになります。そのコンテナ起動の流れはおおよそ以下の通りです。
runc run
コマンドがrunc init
コマンドを実行する。runc init
がnamespace等を用いて隔離環境を作成し、その環境をコンテナ実行に適するよう初期化する。ひととおり初期化したら、エントリポイントを実行する直前で待ち状態になる。runc run
がrunc init
にエントリポイント実行の指示を出す。runc init
は指示を受け取ったらエントリポイントを実行する。
このように、隔離環境の「内と外」それぞれでコマンドが実行されつつ、それらが連携して最終的にコンテナのエントリポイントの実行に至ります。本稿ではコマンド起動の流れは概要にとどめますが、その実装の概要も俯瞰したい方は冒頭で紹介した資料をご覧ください。
runc への理解をさらに深めるのに役立つ資料
本稿でご紹介した資料は、私が主催者の一人として参加したContainer Runtime Meetup #1での発表資料です。ミートアップでは、本稿冒頭で紹介したもの以外にも、runcやOCIランタイム全般、Linuxカーネルへのより深い理解を得るのに役立つ、面白い発表がいくつも行われました。ぜひ、それらの資料もご参照ください。以下にその概要をご紹介します(発表者名は敬称略)。
runc init process(double fork) by Kunal Kushwaha
runcがLinuxのnamespace機能を用いてコンテナの隔離環境を作成する流れ(double fork)を詳しく紹介しています。runcはnamespaceを用いて隔離環境を作成する際、多段階で子プロセスをフォークしながら様々な初期化処理を実行していくという、少し複雑なフローを辿ります。発表ではこれをコードや図を用いて分かりやすく解説いただきました。
runc & User Namespaces by Akihiro Suda
runcが隔離可能なnamespaceのひとつであり、コンテナがシステムリソースに対して持てる権限を制限する等の機能を持つUser namespaceを解説いただきました。また、User namesapceを応用しコンテナを非rootユーザで実行するrootlessを実現するために必要となる要素技術についてもご紹介いただきました。
NOTIFY_SOCKET環境変数について by うなすけ
runcから作られるコンテナプロセスが外部とコミュニケーションをとるための機能のひとつであるNOTIFY_SOCKET環境変数に焦点を合ててご紹介いただきました。発表では、発表者のみならず参加者もそれぞれgithub上を駆け巡り、NOTIFY_SOCKETがどのように応用され得るかなど熱く議論を交わしました。
ftraceを使ったコンテナ内デバッグの準備 by KentaTada
プログラムをデバッグをする際、カーネルを含め挙動を把握するのに役立つLinuxのftrace機能を、コンテナと組み合わせて利用する上での知見を発表いただきました。コンテナ技術はクラウド領域での応用を多く耳にしますが、この発表は組込み領域での応用から得られた知見であり新鮮な発表でした。
まとめ
本稿では、コンテナユーザの多くが利用しているであろうruncの概要を、仕様と実装の両面から俯瞰しました。ランタイム領域への理解を深める一助となれば幸いです。
おわりに
私たちNTTは、オープンソースコミュニティで共に活動する仲間を募集しています。ぜひ弊社 ソフトウェアイノベーションセンタ紹介ページ及び、採用情報ページをご覧ください。