TT Lab
はじめる
学ぶ 学習パス コース

KCNA — Kubernetes・クラウドネイティブ入門

仮想マシンからコンテナへ — 何を捨てて何を得たのか

TT Labで続きを見る

一言でいうと

コンテナは「軽量な仮想マシン」ではありません。カーネルを共有したまま、Linuxの2つの機能(namespaceとcgroup)でプロセスを閉じ込めたものです。そのため高速な代わりに、分離の強さはVMより弱くなります。この一文がKCNAの全範囲の根っこです。

なぜ必要なのか

仮想マシンはハードウェアを模倣します。ゲストOSがもう1つ丸ごと起動し、起動に数十秒かかり、カーネルイメージとシステムファイルがディスクとメモリを消費します。サービス1つをデプロイするために、オペレーティングシステム1つを一緒にデプロイしていたようなものです。

問題は「重さ」ではなく、デプロイの単位と実行の単位がずれていることでした。開発者が作ったのはアプリケーションなのに、運用に渡すのはVMイメージでした。その間に「私のノートPCでは動きます」が生まれます。

コンテナは問いを逆にしました。カーネルはすでにホストにあるのでそのまま使い、アプリケーションから見える世界だけを変えれば済むのではないか、と。その「見える世界」を変えるカーネル機能は、すでに存在していました。

どう動くのか

namespace: 何が見えるか

namespace 隠すもの コンテナで体感される姿
PID プロセス一覧 自分のプロセスがPID 1に見えます
NET ネットワークインターフェース・ポート コンテナごとに独立したIPとポート空間
MNT マウントツリー イメージのファイルシステムがルートに見えます
UTS ホスト名 コンテナ名がホスト名になります
IPC 共有メモリ・セマフォ 他のコンテナのIPCが見えません
USER UID/GIDのマッピング 中ではroot、外では非特権ユーザー

cgroup: どれだけ使えるか

namespaceが「視野」を切り分けるなら、cgroupは「取り分」を切り分けます。CPU時間、メモリの上限、ブロックIO、PIDの数をグループ単位で制限します。メモリの上限を超えると、カーネルのOOM killerがそのcgroup内のプロセスを強制終了します。Kubernetesのresources.limits.memoryは、最終的にこのcgroupの値に落ちていきます。

どちらか一方だけではコンテナになりません。 namespaceだけだと隣のコンテナがメモリを使い尽くして巻き添えで落ち、cgroupだけだとお互いのプロセスやファイルが丸見えになります。

OCI: 標準を握っているのは誰か

初期は「コンテナ = Docker」でした。1つのベンダーが形式と実行方式をすべて握っていると、エコシステムが育たないため、OCI(Open Container Initiative)が3つを規格として切り出しました。

ランタイムは2層構造

kubelet --(CRI/gRPC)--> containerd --(OCI Runtime Spec)--> runc --> 리눅스 커널

containerdは高レベルランタイムです。イメージを取得して展開し、スナップショットを管理し、ライフサイクルを追跡します。 runcは低レベルランタイムです。config.jsonを読んでnamespaceとcgroupを作り、プロセスをexecしたらすぐに消えます。コンテナが動いている間、runcのプロセスは存在しません。

containerdの設計原則は4つにまとめられます。シンプルさ(1つのことをうまくやる)、プラグインベース、OCI互換、gRPC APIです。すべての機能がプラグインなので、スナップショッターをoverlayfsからdevmapperへ差し替えることもできます。

12-factor: コンテナに載せやすいアプリの形

12-factorはコンテナより先に登場しましたが、今では事実上「コンテナでうまく回るアプリの条件」として読まれています。KCNAで頻出する項目は3つです。

現場での姿

筆者のホームラボのクラスターは、もともとランタイムとしてcri-dockerdを使っていました。クラスターを作り直す際にcontainerd 1.7.27へ移行しましたが、これは好みの問題ではなく、階層を1つ取り除いたということです。cri-dockerd経由ではkubelet → cri-dockerd → dockerd → containerd → runcと、途中に中継が2つ余分にありました。containerdを直接使えばkubelet → containerd → runcで完結します。同じ仕事をするのに、プロセスが2つ減ります。

もう1つ。そのホームラボは再構築のあと、カーネルを6.14にそろえました。コンテナがカーネルを共有するという事実が運用で実際に意味するのは、これです。ノードのカーネルバージョンが、そのままコンテナの使える機能の上限になります。eBPFベースのネットワーキングのようにカーネル機能に頼るスタックは、カーネルが古いとそもそも有効になりません。

続けて読むこと

このモジュールにはラボがありません。次の読み物でkubeletとcontainerdの間の通訳であるCRIを見て、そのあとのモジュールで本物のクラスターに接続し、kubectl api-resourcesから叩いてみます。