Rustを使ってコンテナの実装を学ぶ
普段なんとなくコンテナを利用しているのですが、内部でどういったことをしているのかあまり分かっていなかったので勉強しました。
(内部のロジックを知らずに扱えるDockerの抽象度の高さに改めて感心しました。)
クックパッドさんが、Linuxカーネルの機能を用いてコンテナの実装を学ぶための資料をアップしていました。詳細に説明がされており、大変勉強になりました。
GitHub - rrreeeyyy/container-internship
せっかくなので、Rustを使ってコンテナ機能の一部を実装してみました。
名前空間の分離
/bin/shを起動する際にclone(2)に渡すflagsの値によって、IPC,Network,Userなどの名前空間を分離することができます。
改めて、shとは
man sh
の実行結果を確認 (DistributionがDebianなので、dash)shは、ファイルまたはターミナルからコマンドを読み込み、解釈し、実行するコマンド。
shellが実行 -> 完了までの流れはこちらの記事が大変参考になりました。
shell実行のために子プロセスが作られた後はどうなるのだろうと疑問に思っていましたが、子プロセスがexit(0)をcallしてプロセスをtermiateさせ、
親プロセスでcallしたwait()で子プロセスのstateの変更を検知して、子プロセスのために割り当てたリソースがリリースされるということを学びました。
Rustで名前空間の分離を実装
実行環境
GCPでVMインスタンスを1台起動させて、実行環境として利用しました。
$ lsb_release -a No LSB modules are available. Distributor ID: Debian Description: Debian GNU/Linux 10 (buster) Release: 10 Codename: buster $ arch x86_64
実装
syscallを呼び出すために、nixを利用しました。libc crateによるunsafeなAPIをsafeな形で提供してくれているようです。
まずは、flagsに CLONE_IOのみセットしました。
use std::process::Command ; use nix::sys::signal::Signal; use nix::sched; use nix::sys::wait::waitpid; fn run(cmd: &str) -> isize { let exit_status = Command::new(cmd) .spawn().expect("failed to execute container command") .wait().unwrap(); match exit_status.code() { Some(code) => code as isize, None => -1 } } fn main() { const STACK_SIZE: usize = 1024 * 1024; let stack: &mut [u8; STACK_SIZE] = &mut [0; STACK_SIZE]; let cmd :&str = "/bin/sh"; let cb = Box::new(|| run(cmd)); let flags = sched::CloneFlags::CLONE_IO; let child_pid = sched::clone(cb, stack, flags, Some(Signal::SIGCHLD as i32)).expect("failed to create child process"); waitpid(child_pid,None).expect("faile to wait pid"); }
cloneで子プロセスを生成しています。
そして、waitpidメソッドが引数でセットされた子プロセスのstatusを変更を観測しています。
子プロセスの中で、 ip addr show
, id
を実行してみました、
ネットワーキングの名前空間が分離されていないので、NetworkInterface(ens4)が子プロセスも確認できます。
UID, GIDも親プロセスと同じ値が確認できます。
$ ip addr show 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 inet 127.0.0.1/8 scope host lo valid_lft forever preferred_lft forever inet6 ::1/128 scope host valid_lft forever preferred_lft forever 2: ens4: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1460 qdisc mq state UP group default qlen 1000 ... $ id uid=1000(xxxxx) gid=1001(xxxxx) groups=1001(xxxxx),,,
flagsにセットするCloneFlagsを追加し、NetworkingとUser名前空間を分離
let flags = sched::CloneFlags::CLONE_NEWUSER | sched::CloneFlags::CLONE_NEWNET | sched::CloneFlags::CLONE_NEWIPC | sched::CloneFlags::CLONE_IO;
もう一度、ip addr show
, id
を実行してみました。
見事に名前空間が分離されていることを確認できました。
$ ip addr show 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 $ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
残りのコンテナ実装についても引き続きRustで実装してみたいと思います。