CloudNative Days Winter 2025で行われたセッション「一週間で作る低レイヤコンテナランタイム」では、コンテナ技術の抽象の背後にある実装の実像が掘り下げられた。登壇したLINEヤフー株式会社のソフトウェアエンジニア、早坂 絢子氏は、Kotlin/Nativeで自作した低レイヤコンテナランタイムを題材に、namespace分離や権限制御、起動フローの内部構造を丁寧に解説した。コンテナランタイムの役割から実装で直面した課題とその解決までを振り返る。
コンテナの正体と低レイヤランタイムが担う本当の役割
セッションは「そもそもコンテナとは何か」「低レイヤコンテナランタイムは何をしているのか」という基本的な問いを整理するところから始まった。DockerやKubernetesが普及した現在でも、コンテナの内部構造やランタイムの責務は抽象化され、理解されにくい。早坂氏はまずこの前提を明確にすることの重要性を示した。
コンテナは仮想マシンではなく、ホスト上で動作する一つのプロセスである。ただしLinuxのNamespace機能によって、ホストや他のコンテナから隔離された実行環境を持つ点が通常のプロセスと異なる。PID Namespaceを分離すればコンテナ内からは自身のプロセスしか見えず、UTSやNetwork Namespaceを分離すればホスト名やネットワークも独立する。これにより、コンテナは専用環境のように振る舞う。
一方でコンテナが自由に振る舞える状態は危険でもある。早坂氏は「コンテナが悪さをすると困る」という観点から、制限の必要性を強調した。大量のメモリ消費によるホストのリソース枯渇や、rebootのような操作による影響を防ぐため、CPUやメモリは現在主流となっているcgroup v2で制御され、実行可能な操作はcapabilityやseccompなどで制限される。
さらにコンテナはホストとは異なるルートファイルシステムを持つ。pivot_rootを用いてプロセスが認識するルートディレクトリを切り替えることで、ホスト全体のファイルシステムへアクセスできない状態を作り出す。隔離、ルートファイルシステムの分離、リソース制限と権限制御。これらの組み合わせによって、コンテナという実行環境が成立している。
この環境を実際に作り出す役割を担うのが低レイヤコンテナランタイムである。containerdなどの高レイヤランタイムが管理全般を担うのに対し、低レイヤランタイムはOCI Runtime Specificationに従い、createやstartといった指示を受けてNamespace分離やpivot_root、各種制限の適用を実行する。早坂氏は「低レイヤコンテナランタイムって具体的に何をするのか、そこをちゃんと理解したくて今回作ってみました」と語り、自作ランタイムに挑んだ動機を語った。
Kotlin Multiplatformの一角としてKotlin/Nativeを選んだ理由
低レイヤコンテナランタイムの実装にあたり、早坂氏が選択したのがKotlin/Nativeである。一般にコンテナランタイムはGoやRustで実装されることが多く、KotlinはWebアプリケーション向けの言語という印象が強い。その中で、あえてKotlin/Nativeを用いるという選択自体が、セッションの大きな特徴となっている。
Kotlin/Nativeは、Kotlin Multiplatformを構成する技術の一つであり、同一のKotlinコードをJVM、JavaScript、ネイティブといった複数の実行環境で動かすことを目的としている。その中でKotlin/Nativeは、JVMを介さず、Kotlinコードをネイティブバイナリとしてコンパイル・実行するための仕組みを提供する。Kotlin IRを経てLLVM IRへ変換され、最終的にLinux上で直接動作するバイナリが生成される。
この仕組みにより、Kotlinで記述したコードからlibc関数を呼び出したり、syscallを直接実行したりすることが可能になる。早坂氏は選定理由について「『Kotlin/Nativeって何なんだ』という疑問にまず答えたかったんです」と語る。「普段はWebバックエンドを書いているので、慣れた言語でどこまで低レイヤに踏み込めるのか試してみたかった」と述べ、技術的な関心と実験的な動機があったことを明かした。
一方でKotlin/Nativeは単純にC言語の代替になるわけではない。生成されたバイナリにはKotlinランタイムが同梱され、起動時にはGC用スレッドを含むマルチスレッド構成で動作する。この特性は、後述するNamespace分離やfork処理と相性が悪く、実装上の制約として顕在化していく。Kotlin Multiplatformの一要素としてKotlin/Nativeを選ぶことは、利便性と同時に独自の課題を引き受ける判断でもあった。
createとstartの裏側で立ちはだかったNamespace分離の壁
ここからは自作した低レイヤコンテナランタイムの具体的な実装へと踏み込んでいく。早坂氏が実装したランタイムは、OCI Runtime Specificationに準拠し、create、start、kill、delete、stateといった基本操作を備える。ここではcreateとstartを中心に、コンテナ生成の流れが解説された。
低レイヤコンテナランタイムは、高レイヤランタイムからバイナリとして呼び出され、bundleディレクトリとconfig.jsonを受け取る。createでは、このconfig.jsonに従ってNamespace分離やpivot_rootなどを行い、コンテナとなるプロセスを準備する。ただし、この時点ではまだ起動コマンドは実行されず、あくまで「コンテナとして実行可能な状態」を作る段階にとどまる。続くstartで、あらかじめ準備されていたプロセス内で指定されたコマンドが実行され、初めてコンテナが起動する。
実装で最も苦労したのが、Namespace分離の扱いである。Kotlin/Nativeで生成されるバイナリは、起動時からGC用スレッドを含むマルチスレッド構成で動作する。この状態でNamespace分離やforkを行うと、期待どおりに動作しないケースが多発した。とくにUser Namespaceでは、マルチスレッドプロセスからの分離がエラーになる問題や、PID Namespace分離時にGCロックがコピーされてプロセスがハングする問題が発生した。
早坂氏はこの状況について「Kotlin/Nativeは起動した瞬間からマルチスレッドなので、素直にNamespaceを切ろうとしても全然うまくいきませんでした」と振り返る。そこで採られたのが、Kotlinランタイムが起動する前にC言語で処理を行うアプローチである。constructor属性を用いてKotlinのmain関数より前に実行される処理を定義し、シングルスレッドの状態でNamespace分離やforkを実行する構成とした。
さらにUser NamespaceのUID/GIDマッピングでは、分離後のプロセス自身が設定を書き込めないという制約がある。そのため、メインプロセスと分離後のプロセスがソケット通信を行い、タイミングを合わせてマッピングを設定する仕組みが実装された。Namespace分離後は、pivot_rootによるルートファイルシステムの切り替えや権限の放棄、seccompフィルタの適用といった処理を順に実行し、起動可能な状態を整えたうえでstartシグナルを待つ。
このように、自作ランタイムではプロセスを段階的に分け、createからstartに至るまでの流れを明示的に制御している。ここで示されたのは、低レイヤコンテナランタイムが単に「コマンドを実行するツール」ではなく、Namespace分離後の世界を安全に構築し、起動まで導く制御の塊であるという事実である。
低レイヤに踏み込むことで見えたコンテナ技術の本質
セッションでは、低レイヤコンテナランタイムの内部構造を題材に、コンテナという仕組みがLinuxカーネルの機能をどのように組み合わせて成立しているのかが丁寧に解き明かされた。とくに、自作ランタイムの実装を通じて示されたのは、Namespace分離や権限制御、起動フローが単なる概念ではなく、実装上の制約と向き合いながら成立しているという現実である。
Kotlin/Nativeという選択は、Webバックエンドエンジニアにとって低レイヤ領域への距離を縮める一方で、マルチスレッドという言語特性が新たな課題を生むことも明らかにした。それでも早坂氏は「意外といける、というのが正直な感想です」と語り、Kotlinのモダンな表現力を活かしたシステムプログラミングの可能性を示した。
低レイヤを理解することは、コンテナを正しく使いこなすための土台となる。セッションは、抽象の背後にある実装へ一歩踏み込むことで、コンテナ技術への理解をより確かなものにする機会を提供したと言える。
