コンテナランタイムのことをもっと知ろうとした話(未完成記事)

はじめに

がんばって書いていたのですが、途中で投げ出してしまった記事です。なんで投げ出してしまったかというと、単純に興味のスコープが散らばりすぎて、記事としてまとまりがなくなってしまったからです。ではなぜ公開しているかというと、CS学位なし新卒には、コンテナランタイムってこういう見え方をしているんだよという資料として、案外価値があるんじゃないかと思ったためです。書き直そうとすると面倒だったのでこのままぶん投げてしまえという気持ちになった

コンテナランタイムの記事は、また改めて書いていこうかなと思います。

第一層、コンテナランタイムなんもわからん

 メイドインアビス面白いですよね。ニチアサにアニメ一気観したので軽く鬱ってました。

 第一層ということで、コンテナランタイムの仕組みというより、コンテナランタイムにDeepDiveするための興味関心の準備からします。Docker/Kubernetesは先週勉強を始めたくらいのペーペーなのですが、そのときふと思ったのが、「コンテナランタイムってなんかいっぱいあるよね」ということでした。containerdとかrunCとか、よくわからん。なんだこいつら。

 そもそも「コンテナ」という単語自体とても曖昧だと思うんですよ。コンテナに荷物、つまり、必要なミドルウェアとかいれてrunすると勝手にウェイって動くぜみたいな。定義なく進めるのはアレなので、ここではコンテナを「実行状態のプロセス」とします。なんとなくそういう説明が多かった気がするので。ランタイムは「実行環境」ですね。「コンテナランタイム」っていう単語の整理だけすると、概観は下図の状態になりそう。

図1 コンテナとコンテナランタイムの上下関係?
図1 コンテナランタイムによってプロセスを動かす。画像がでかい。

 なんてお粗末なんだこの図は、と思いはするのですが、この図だけ見ると、コンテナランタイムというのはコンテナの管理をしてそうだねというのはなんとなくわかります。 じゃあコンテナの管理ってなんだろうということを考えると、コンテナの作成や削除とかそういうことしてるのかなーと思うわけです*1。 といっても、作成や削除ってどうやってるのでしょう。それ以前に、コンテナってどこからリソースを引っ張ってくるんでしたっけ。 コンテナ仮想化技術のおさらいからやります。。

第二層、コンテナ仮想化技術もなんもわからん

 コンテナってどこで作られてるの?コンテナってそもそもどういう仕組み?ということで、コンテナ仮想化技術について軽く触れておきます。

 コンテナ仮想化技術について一言で「コンテナ仮想化技術では、OSのリソースを隔離して仮想OSにする」と言われてもピンと来ないですね。僕はピンと来なかったです。 ですので、ここでは「アプリケーションやライブラリなど実行環境の塊をプロセスとして切り出す」という表現にしますね。多少は良くなった?マサカリが怖い。

 言葉だけでは全くわからんので、まずはコンテナを考慮しない場合について図を交えて考えて見ましょう。下図のようになりそうですね

ホストOSで普通にアプリケーションを動かすとき。それはそう、みたいな図になる
ホストOSで普通にアプリケーションを動かすとき。それはそう、みたいな図になる

これを、コンテナにアプリケーションやライブラリ、ミドルウェアなどを突っ込んだ状態にすると、以下のようになります。

図3 コンテナにアプリケーションなどを詰めてプロセスとみなす様子。
独自解釈に塗れた図である。本当はカーネルを間借りしたりするので注意されたし。

 あとはこの塊を、どうやって仮想OSっぽくしていくかについて考えましょう。

 仮想OSなのでOSっぽいものが欲しいですね。例えば、ルートファイルシステムとか*2。特定のコンテナ固有のリソースに無制限にアクセスできてしまうのは問題なので、リソース隔離もされていて欲しい。ネットワーク空間もコンテナ内で隔離させたい。隔離するためには名前空間が必要になりますね。あとは、複数のコンテナを立てたりすると、ホストのOSに対する負荷がどんな状況か見たくなる*3ので、プロセスが使用しているCPU時間やメモリ使用量の監視がしたいですね。ついでに使用量の制限とかできたら最高です*4

 これらの振る舞いの実現を考えると、名前空間を提供したり、プロセスを監視したり、つまり、Linuxカーネルでいうところのnamespaceやcgroupが必要*5になってくるんですね。なるなる。発想としては「別にハードウェアのリソース使って仮想のホストを立てなくても、共有できるところは共有してプロセスとして切り出したほうがコスパいいんじゃないか?」といったところでしょうか*6

 とはいえ、あまりにざっくり過ぎますね。隔離ってじゃあ具体的になにを隔離すればいいの?リソースって具体的になに?リソースの管理ってどこまでやるの?、、など、このままではガン詰めされてしまいそう*7なので、隔離とリソース管理についてもう少しだけ詰めていきましょう。

第三層、「一生Namespaceに閉じ込めてかわいがってあげたい」

 かぐや様二期はあざといハーサカが出てきたり雑魚ミコちゃんが出てきたり面白くなってきましたね。萌葉ちゃんドぎつくていい。コンテナの一生は短いからあまり楽しめなさそうだけども。

 上層で、コンテナを仮想OSとしてみなすためのざっくり条件を掲示しました。あとは仮想OSとして何が必要なのか、もう少し考えていくことにしましょう。Dockerによるコンテナの起動を考えます。ざっくりイメージですが、以下の図のような具合でしょうか。

f:id:hagityann224:20200503115018p:plain
例として、コンテナでNginxを動かす場合を考えてみる。
pullしてrunしておけい!みたいなざっくり感を表しています

 とりあえず例をNginxにしましたが、なにはともあれコンテナを動かすためには、そのコンテナに何を突っ込むか、つまりコンテナイメージが必要ですね。次は、いざNginxを動かすときですね。 そもそもプログラムの実行はプロセスとみなされているわけですが、アプリケーションの実行はコンテナの中で閉じていればいいので、コンテナのプロセスだけ見えればいいですね。ということプロセスツリーを隔離します。プロセスツリーがコンテナ内に隔離されたので、プロセス間通信ができる範囲(プロセス集合)も隔離します。コンテナの中にプロセス集合を閉じ込めることができました。これが、PID namespaceとIPC namespaceによる隔離です。

f:id:hagityann224:20200503215621p:plain
プロセスツリーはコンテナごとに独立させる。同じIDのプロセスをコンテナごとに持つこともできるわけだが、ホストOS側から見ると混乱のもとになるので、ホストOSが見るときのために別のプロセスIDが付与される

 一旦、プロセスがコンテナ内だけで動くようになった、はず、です。しかし仮想OSであるわけですからroot権限を持ったユーザーが要りそう。でもこのユーザにホストOSの権限も持てちゃったら危険ですよね。なので、ユーザーも隔離してしまいます。これがUser namespaceによる隔離です。

f:id:hagityann224:20200503220857p:plain
隔離されたコンテナの中でのみrootとして振る舞うことができる。
   コンテナ内でのroot権限はゲットしましたが肝心のファイル操作がまだできそうにないです。なにせ、ファイルシステムがマウントされていないので。なければマウントしましょう。もちろんそのファイルシステムツリーもコンテナ内に閉じるよう隔離させます。他のプロセスからファイルが自由に見れてしまうのはよくない。ここがMount namespaceによる隔離になります。

f:id:hagityann224:20200503222230p:plain
manページの説明ではあまり良くわからなかったが図の通りで合っていると思う

 これでだんだん仮想OSっぽくなってきました。

 コンテナ実行のイメージを整理しました。  隔離対象は以下ですね

こいつらが隔離対象なので、あとはこれをコンテナランタイムがどうやって表現するかということに関心が向いてきましたね。とはいえ、コンテナ作成にあたって、コンテナランタイムがどこまで担当するのかというスコープがはっきりしないと整理できないので、まずはコンテナの作成フローを、Kubernetesを交えながら整理しましょう

 

第四層、コンテナを作っていいのは誰

 コンテナ仮想化技術について触れたところで、話は「コンテナの作成や削除ってどうやってやってるの」という話題に戻ります。 Kubernetesも勉強したてなので、Kubernetesにおけるコンテナの作成もからめていきます。Kubernetesに触れた*8ことがある方のほぼ100%が「kubectl」というコマンドを叩いていると思います。kubectlでPodのデプロイを指示したときはコンテナが作成されるわけですが、実際どんなふうになっているのでしょう。

 kubectlを叩くと、そのリクエストはAPI Serverに飛んでいきます。今回は「Pod作って」という依頼を投げます。そうすると、API Serverはkubectlによって依頼された情報をもとにetcdを更新します。とetcdはクラスタの構成情報を管理するKVSです。ここに、Podがいくつ必要なのか、どのNodeに割り当てているのかといった情報を格納していきます*9。 とりあえず、クラスタのあるべき姿を先に更新しておいて、あとは各々よしなによろしくお願いします、という具合ですね。

f:id:hagityann224:20200502232902p:plain
まず、etcdにPodのあるべき姿を登録しておく。本来であれば、ControllerManager内にあるDeploymentController、ReplicaSetControllerとのやり取りが存在するが、ここでは割愛*10

 そして、APIServerをウォッチしていたSchedulerが構成情報の更新を検知して、Podをどこに置くか決めます。ちょうど図5のような感じでやりとりをします。 SchedulerはAPIを介してetcd上のPodの情報、ここでは配置するNodeの情報を更新します。

f:id:hagityann224:20200503080100p:plain
図5 ラベルなどのメタデータやCPU・メモリのリソース上限などによってNodeが選択される

 Schedulerはあくまでetcd上の情報の依頼をするだけで、直接Pod作成の依頼をするわけではありません。では作成はどうするのかというと、NodeにあるのkubeletがAPIServerを常に監視しているのでPodの情報が更新されたらそれに気づけるようになっており、変化を検知したkubeletがPodの作成を開始します。

f:id:hagityann224:20200503083556p:plain
図5 変化を検知できるよう、Kubeletも常にAPIServerを監視している。

 とはいえ、kubeletは監視と指示出しがメインの仕事です。ですから、kubelet自身はPod(=コンテナ)を作成しません。代わりに、コンテナランタイムにPodの作成を依頼します。やっとここでコンテナランタイムが登場します。

f:id:hagityann224:20200503083939p:plain
図6 コンテナランタイムにコンテナ作成の依頼をする

 コンテナランタイムがコンテナを作成していますね。1層で想像した感じになりました。

 ここまで、コンテナに必要なものはなにかという材料と、コンテナを作るのはだれかということについて整理ができました。次は、実際にコンテナランタイムがどのようにしてコンテナを作成しているのかという話題に移りましょう。ここまで長かった。

第五層、コンテナ

 上層でいろいろ見てきたので、ここから本格的にコンテナランタイムそのものについて*11です。コンテナに必要なモノは以下でしたね

  1. コンテナ実行環境のイメージファイル
  2. ルートファイルシステム
  3. Pod用の仮想NIC
  4. 隔離環境

 ここで整理したいのは、Linuxカーネルの技術とどこで交わるかという点です。Linuxカーネルの機能として使いたいのは、namespaceとcgroupです*12。イメージファイルの獲得や仮想NICのセットアップは、このLinuxカーネル機能と直接的に関わるものではありません。なので、namespaceとcgroupを使うタイミングとその下準備で処理のレイヤーは分ける必要があります。実際にnamespace、cgroupを使うのが低レベルランタイム、使う準備をするのが高レベルランタイムになります。図にすると以下のようになります。

f:id:hagityann224:20200503170711p:plain
ざっくりではあるが、高レベルランタイムと低レベルランタイムの役割分担
高レベルのコンテナランタイムにはそれぞれ個性がある。Dockerにはビルド機能がついているし、CRI-Oにはログ収集・監視用のデーモンが動くような仕組みがある。containerdはplugableで汎用的。)

 高レベルランタイムの処理から低レベルランタイムの処理まで、コンテナの作成を例にして処理を追っていきます。まず、イメージの取得からです。以下の図のように、イメージはレジストリに置いてあるのでそれを取得してきます。kubectlはあくまで指示を出すだけで、pullしてくるのはコンテナランタイムなのがポイントです。

f:id:hagityann224:20200503143123p:plain
イメージをレジストリから取得する。マネージドのkubernetesやその他コンテナオーケストレーションサービスの場合は、そのベンダーのマネージドのレジストリサービスを使うことが多い。

層 ランタイムのつかいわけ

 ここまで、コンテナ仮想化技術からPod作成を経由してコンテナランタイムを理解するところまで行いました。これらを踏まえて、各ランタイムの特徴を抑えていきます。

Docker

 Dockerが高レベルランタイムという位置づけになるのは少し不思議な感覚ですね。とはいえ、コンテナイメージを持ってきてコンテナを実行するのですからコンテナランタイムですよね。Dockerはビルドの機能やSwarmによるオーケストレーション機能など、これ自体でかなり高機能です。とはいえ、Kubernetesなどによるオーケストレーションの場合、そもそもコンテナランタイムがイメージのビルドをする必要はないので、もう少し軽くていいよねとは思います。

containerd

 例の、軽くていいよね、がこいつです。もともとDockerの一部でした。containerdの最大の特徴は「改造できる」ことです。プラグインを入れていくことでユースケースに対応していくようなアーキテクチャになっています。 AWSのFirecrackerもcontainerdにプラグインを足していく形態をとっているそう。

低レベルランタイム

 低レベルランタイムではホストのカーネルを借りるわけですが、ちゃんと隔離されていないと、他のコンテナで動いている悪いアプリケーションにNodeごとふっとばされたりするかもしれないのが怖いですね。なので、このレイヤーでは、「コンテナの隔離の方法」ということへの関心が強くなります。どうやってセキュアにしていくかという問題に対するアプローチは様々なので、低レベルランタイムの標準仕様さえ守っておけばいろんな実装が考えられます。今後も動向を見守りたい分野ですね。

runC

 Dockerのデフォルトランタイムです。Linuxカーネルのnamespace, cgroupを使って隔離しています。セキュリティの方ですが、AppArmorやSELinuxなどによってコンテナの挙動制限をするにとどまっていそう。runCの面白いところは、中にあるlibcontainerというディレクトリです。これ実はもともとDockerの中にあるライブラリでした。DockerとCoreOSとでコンテナランタイムの標準化戦争やっていたみたいですがなんやかんや落ち着き、runCという標準化されたランタイムになったという歴史があります。このlibcontainerがコンテナ操作用のライブラリですから核はこちらになります。あとはOCI標準*13のインターフェースに合わせるようにコマンドが拡張されています。createコマンドやrunコマンドなど、馴染みのあるインターフェースの実装が追加されてくるわけですね。

gVisor

 Google発コンテナランタイムです。runCとの違いは、ユーザー空間にカーネルを持ってきているところです。なんでこんなことしているのかというと、そもそも使えるシステムコールを安全なものに制限してしまいたいから。

gvisor.dev

f:id:hagityann224:20200503215107p:plain
ホストOSへのシステムコールは68個にまで減らされる

アプリケーションから飛んできたシステムコールは、本来であればホストのカーネルに行くはずなんですが、ユーザ空間にあるカーネルがそのシステムコールを横取りして代わりにコールするという具合。こうすれば、セキュアなシステムコールだけ飛ばしてホストOSを守るということができますね。ただ欠点もあって、システムコールが制限されているので、一部互換性に問題があるらしい。

f:id:hagityann224:20200503214116p:plain
Google色で激しい。SentryというLinuxカーネルエミュレータシステムコールを横取りしているのがわかる。加えて、ファイルアクセスはGoferというまた別のプロセスを介して行われる。

Kata Containers

 OpenStack Foundationで開発を推しているコンテナランタイムです。こちらはさらにセキュリティ志向が高いです。なんと仮想マシンを乗っけてしまいます。たしかにカーネルごと隔離されてしまえば、ホストOSとの隔離はかなり強くなりますね、しかも仮想マシンはコンテナではなくPodとして扱うので、Podごとにカーネルを持つという贅沢 。ただ仮想化してさらに仮想化してなので、パフォーマンスは落ちてしまうそう*14

f:id:hagityann224:20200503212650p:plain

About Kata Containers | Kata Containers

Kata ContainersのアーキテクチャVMの中にカーネルを置いてありコンテナはその上においてある。この図だけだとコンテナが一つなので贅沢感が伝わらないのが残念。

「コンテナランタイムなんかいっぱいあるよね」というなんとも情けないスタートでしたが、それぞれのコンテナランタイムの特徴を抑える前提知識を一通り揃えることができたような気がします。

終わりに

 最後まで読んでいただいた方、インプットをごった煮にした文章を読んで頂きありがとうございました。構造化された文章はわかりやすいですが、無味乾燥としていて読んでいて面白くないなぁと思い、関心を深く掘り下げていくスタイルをとりました。関心が分散しないよう努めましたが、結局この長さになってしまいました。また本音を言えばもう少し実務によった話が書けるようになりたいなぁと思ってます。とはいえ実務でKubernetesもDockerも使っていないので現場志向の記事が書けるはずもなく。転職したい。次はrunCについて書きたいですが、デレステフルコンリレーをやっているせいかキーボードが打てないのでまた今度。

*1:もう少し細かい解釈を加えます。コンテナのライフサイクルとそれに対する操作を考えると、コンテナの状態の取得 / コンテナの作成 / コンテナ内でのアプリケーションの実行 / コンテナの終了/ コンテナの削除の5つの操作があります。これらを踏まえ、コンテナ作成の下準備である高レベルランタイムと実際のコンテナ操作を行う低レベルランタイムをまとめてコンテナランタイムと呼ぶのが一般的なのでしょう

*2:さも「当然じゃ~ん」みたいな物言いですが、筆者はこのへんあまりわかってないです。入門記事程度の知識しかありません。 www.linuxmaster.jp

*3:同一ホスト上で何個もコンテナを動かすわけですから、上限超えてコンテナ動かそうとしてマシンが飛んだりしようもんなら大変なわけです。リソース監視をして、どこまで使えるかは常にウォッチしていないといけない。

*4:この段階ではかなりざっくり解釈です。次の章でもう少しだけ掘ります

*5:namespaceやcgroupについて、日本語記事でわかりやすかったのはこちらです。コンテナ作ってます。 employment.en-japan.com

*6:コンテナ仮想化についてざっくり理解するときに一番しっくり来たのは、この本のP3の「Linuxコンテナ技術とDocker」の部分です。

コンテナはOSレベルのリソースの分離に過ぎず、ホストOSのカーネルを共有している。

この「大した話じゃない」感がきっかけになって理解がバチッとハマりました。

*7:曖昧でふわっとしたこと言いがちなのでガン詰めされることがすごい多いんですよね。怖い。人類みなしゅがーはぁと☆の精神でいてほしい。

*8:Kubernetes触れたことがない!?!?今すぐポチってください。

*9:あくまでコンテナランタイムの記事なのでetcdの詳細については省きますが、

*10:Kubernetesにおける「イベントチェーン」を調べるのがわかりやすいかと思います。Reconciliation Loopsの意味を理解するためにもイベントチェーンの理解は大事だと思いました(Kubernetes歴1週間程度の感想)

*11:基本的には高レベルランタイムはcontainerd、低レベルランタイムはrunCを前提にしています。誤解が起きそうな表現には注釈をつけます。

*12:ただし、gVisorはユーザー空間カーネルを使っていたりするのでちょっと話が変わったりする。gVisorはユーザー空間にLinuxシステムコールの実装を置いているので、コンテナとホストOSの間は強く隔離されていてセキュリティ志向。

*13:Open Container Initiativeというコンテナ技術の標準化団体があります。先のDockerとCoreOSの標準化戦争によってコンテナ界隈はギスギスしていたそうですが、LinuxFoundationがさすがにコンテナ技術を潰すわけにはいかないと、標準化団体の設立プロジェクトを発足したそうです。 cloud.watch.impress.co.jp

*14:結論として、KubernetesよりセキュアにしたければKata Containersは選択肢としてありだぜという話。一応AutoScaleもRoolingUpdateもいける。ただコンテナ起動のオーバーヘッドが発生したりなどするので、パフォーマンスとのトレードオフubuntu.com