こんにちは.緊急事態宣言がのびたので相変わらずの外出自粛中です.最近は外出自粛にも少し慣れましたがやはり退屈です.僕は作業中のBGMとして何度か見たことのあるアニメなどを流しているんですがそれらのストックも無くなってきています.ループしようかな.ちなみにおすすめはガンダムUCですね.音楽が素晴らしいですし,SFアニメはモチベが上がっていいです. さて,今回は自作コンテナランタイムに挑戦したという話です.前回のポスト (opens new window)ではruncを使ってみましたが,今回はruncを参考に挑戦してみました.

ちなみにコード書いて試してたときにrm -rfで書いてたコード全消去して萎えました.

gitで管理するのって大事ですね. リポジトリはこちら

https://github.com/terassyi/mycon (opens new window)

# タイトルについて

タイトルにつまずいているとつけましたが,つまずいてます.長い間同じ箇所でエラーがでて前に進めていません. 僕の魂の叫びがこちら.

このあと力付きこの記事を書き始めております. ツイートの通り,マウントでつまずいております. どなたか有識者の方に助けていただきたいです.

# 問題

発生している問題は

rootfs/dev/ptsdevptsでマウントできない

という問題です. コンテナプロセスの設定ファイルであるconfig.jsonでいうと以下の部分です.

{
	"destination": "/dev/pts",
	"type": "devpts",
	"source": "devpts",
	"options": [
		"rw",
		"mode=0620",
		"gid=5"
	]
},
1
2
3
4
5
6
7
8
9
10

実際にマウントを行うのは以下のコードです. 標準パッケージのマウントシステムコールのラッパー関数を呼び出しています.

if err := unix.Mount(m.Source, target, m.Type, uintptr(flags), data); err != nil {
	return err
}
1
2
3

unix.Mountメソッドに

  • m.Source = devpts
  • target = rootfs/dev/ptsへの絶対パス
  • m.Type = devpts
  • flags = 0(オプションから得られるフラグ)
  • data = mode=0620,gid=5

という感じで引数を渡しています. するとInvalid argumentエラーを発生させます.

DEBU[0000] source=devpts                                
DEBU[0000] target=/usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs/dev/pts 
DEBU[0000] mtype=devpts                                 
DEBU[0000] flags=0                                      
DEBU[0000] options=mode=0620,gid=5                      
DEBU[0000] invalid argument 
1
2
3
4
5
6

# 問題の実験環境

詳しくは後述しますが,実験しているVMのイメージはubuntu/xenial64 (opens new window)です. ルート直下の構成は以下の様な感じ

$ ls /
bin   dev  home        initrd.img.old  lib64       media  opt   root  sbin  srv  tmp  vagrant  vmlinuz
boot  etc  initrd.img  lib             lost+found  mnt    proc  run   snap  sys  usr  var      vmlinuz.old
1
2
3

また,コンテナプロセスとして起動しようとしているのはdockerイメージからエクスポートしたcentosです. プロジェクトから./bundle/rootfs/以下にファイルを配置しています.

# マウントされているファイルシステム

マウントされているファイルシステムは以下の様な感じ.

$ mount
sysfs on /sys type sysfs (rw,nosuid,nodev,noexec,relatime)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
udev on /dev type devtmpfs (rw,nosuid,relatime,size=498852k,nr_inodes=124713,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,mode=600,ptmxmode=000)
tmpfs on /run type tmpfs (rw,nosuid,noexec,relatime,size=101576k,mode=755)
/dev/sda1 on / type ext4 (rw,relatime,data=ordered)
securityfs on /sys/kernel/security type securityfs (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev/shm type tmpfs (rw,nosuid,nodev)
tmpfs on /run/lock type tmpfs (rw,nosuid,nodev,noexec,relatime,size=5120k)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
pstore on /sys/fs/pstore type pstore (rw,nosuid,nodev,noexec,relatime)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
mqueue on /dev/mqueue type mqueue (rw,relatime)
systemd-1 on /proc/sys/fs/binfmt_misc type autofs (rw,relatime,fd=33,pgrp=1,timeout=0,minproto=5,maxproto=5,direct)
hugetlbfs on /dev/hugepages type hugetlbfs (rw,relatime)
debugfs on /sys/kernel/debug type debugfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)
lxcfs on /var/lib/lxcfs type fuse.lxcfs (rw,nosuid,nodev,relatime,user_id=0,group_id=0,allow_other)
vagrant on /vagrant type vboxsf (rw,nodev,relatime)
usr_local_go_src_github.com_terassyi_mycon on /usr/local/go/src/github.com/terassyi/mycon type vboxsf (rw,nodev,relatime)
tmpfs on /run/user/1000 type tmpfs (rw,nosuid,nodev,relatime,size=101576k,mode=700,uid=1000,gid=1000)
binfmt_misc on /proc/sys/fs/binfmt_misc type binfmt_misc (rw,relatime)
devpts on /usr/local/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
usr_local_go_src_github.com_terassyi_mycon on /usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs type vboxsf (rw,nodev,relatime)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
$ df -aH
Filesystem                                  Size  Used Avail Use% Mounted on
sysfs                                          0     0     0    - /sys
proc                                           0     0     0    - /proc
udev                                        511M     0  511M   0% /dev
devpts                                         0     0     0    - /dev/pts
tmpfs                                       105M  3.3M  101M   4% /run
/dev/sda1                                    11G  1.8G  8.7G  17% /
securityfs                                     0     0     0    - /sys/kernel/security
tmpfs                                       521M     0  521M   0% /dev/shm
tmpfs                                       5.3M     0  5.3M   0% /run/lock
tmpfs                                       521M     0  521M   0% /sys/fs/cgroup
cgroup                                         0     0     0    - /sys/fs/cgroup/systemd
pstore                                         0     0     0    - /sys/fs/pstore
cgroup                                         0     0     0    - /sys/fs/cgroup/perf_event
cgroup                                         0     0     0    - /sys/fs/cgroup/freezer
cgroup                                         0     0     0    - /sys/fs/cgroup/memory
cgroup                                         0     0     0    - /sys/fs/cgroup/devices
cgroup                                         0     0     0    - /sys/fs/cgroup/cpu,cpuacct
cgroup                                         0     0     0    - /sys/fs/cgroup/net_cls,net_prio
cgroup                                         0     0     0    - /sys/fs/cgroup/pids
cgroup                                         0     0     0    - /sys/fs/cgroup/blkio
cgroup                                         0     0     0    - /sys/fs/cgroup/hugetlb
cgroup                                         0     0     0    - /sys/fs/cgroup/cpuset
mqueue                                         0     0     0    - /dev/mqueue
systemd-1                                      -     -     -    - /proc/sys/fs/binfmt_misc
hugetlbfs                                      0     0     0    - /dev/hugepages
debugfs                                        0     0     0    - /sys/kernel/debug
fusectl                                        0     0     0    - /sys/fs/fuse/connections
lxcfs                                          0     0     0    - /var/lib/lxcfs
vagrant                                     500G  370G  131G  74% /vagrant
usr_local_go_src_github.com_terassyi_mycon  500G  370G  131G  74% /usr/local/go/src/github.com/terassyi/mycon
tmpfs                                       105M     0  105M   0% /run/user/1000
binfmt_misc                                    0     0     0    - /proc/sys/fs/binfmt_misc
devpts                                         0     0     0    - /usr/local/pts
usr_local_go_src_github.com_terassyi_mycon  500G  370G  131G  74% /usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

また,コマンドラインからmountを実行した場合はうまくいっている様です.

$ sudo mount -vt devpts devpts ./dev/pts/ -o mode=0620,gid=5
mount: devpts mounted on /usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs/dev/pts.
1
2

確認してみます.

$ mount | grep devpts
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
devpts on /usr/local/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000)
devpts on /usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs/dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000)
devpts on /usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs/dev/pts type devpts (rw,relatime,gid=5,mode=620,ptmxmode=000)
1
2
3
4
5
/usr/local/go/src/github.com/terassyi/mycon/bundle/rootfs$ ls dev/pts
0  1  ptmx
1
2

マウントリストにも出てきて,かつlsコマンドでdev/ptsを覗くと/dev/pts/と同様のファイルが見えるのでマウントが完了している様に見えます. しかし,作成したプログラムから実行するとエラーを発生させます. マウントする順番の問題やその他プログラムの問題である可能性も考えつつ調査をしていましたが,なかなか解決策が見当たりません. Linuxについて理解不足であることは間違いないので,もし原因や解決策に心当たりのある方がいらっしゃったらご教授お願いしたいです.

# 自作コンテナの動機

皆さんdocker好きですか?僕は好きです. 普段Macを使用しているのですが,Linuxをターゲットにしたプログラムを書くことが多いです.そのようなときにdockerは非常に簡単にLinuxの環境を構築でき,また,リポジトリに一緒に入れておくことでもし誰かが試してみたいと思ったときにコマンド一つで環境が再現できます.最近はVagrantを使用して環境構築を行うこともありますが基本的にdockerの方が便利ですよね. さてここで気になるのはdockerがどのように仮想環境を実現しているかです. ざっくりLinuxのnamespacecgroupなどの機能を使用して実現しているという理解はあったのですが,詳しくはわかりませんでした. そこで,仕組みを理解するには作ってみることが一番ということで自作してみるか,となりました. しかし,現実はそう甘くないです.

# 実験環境

今回はVagrantを使用してMac上にubuntu VMを起動して実行しました.IDEはGolandを使用しました. Mac上でLinuxをターゲットとして定義ジャンプなどできるので便利です.

Vagrantfile

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/xenial64"
  config.vm.synced_folder "./", "/usr/local/go/src/github.com/terassyi/mycon"
  config.vm.provision :shell, :path => "./install.sh"
end
1
2
3
4
5
6
7

install.sh

#! /bin/sh
sudo apt update
# install golang
wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz
sudo tar -C /usr/local -xzf go1.14.2.linux-amd64.tar.gz
rm go1.14.2.linux-amd64.tar.gz
echo "export PATH=$PATH:/usr/local/go/bin" >> .bashrc
# install docker
sudo apt -y install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
sudo apt update
sudo apt -y install docker.io
sudo systemctl start docker
# add docker user group
sudo groupadd docker
sudo gpasswd -a $USER docker
sudo systemctl enable docker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

さぁやりましょう.

# runcについて

まずはruncについて. runcでコンテナプロセスを作成するにはrunc create [container id]コマンドを実行します. 起動するにはrunc start [container id]ですね. 詳しくは前回のポスト (opens new window)を参照してください.

# runcがどのようにコンテナプロセスを起動するか

runc create [container id]を実行した後,runcはどのような処理を行ってコンテナプロセスが生成されるのでしょうか. ここら辺を調べるためにruncのコードと格闘しました. こちらの資料がすごく参考になりました. コンテナユーザなら誰もが使っているランタイム「runc」を俯瞰する[Container Runtime Meetup #1発表レポート] (opens new window) こちらの資料ではrunc runコマンドの実装について述べられています.runコマンドは新しいコンテナプロセスを生成して実行するcreate + startのようなコマンドなので基本的なフローはcreateの場合も同じです. createコマンドが実行されると,内部でrunc initというコマンドが名前空間を分離した上で別プロセスで実行されるようになっています. その後,initプロセスにおいて,cgroupcapabilitiespivot_rootなどのリソース分離作業を行っています. リソースの分離作業を終えるとstartコマンドからの起動シグナルを待ち受けてシグナルを受けるとセットされているコマンドを実行します.

# リソースの分離について

さて,リソースの分離とはどういったものでしょう.namespacecgroupchrootを使用しています. コンテナ技術入門 - 仮想化との違いを知り、要素技術を触って学ぼう (opens new window)では,仮想マシンとコンテナの違いからLinuxコマンドを使用したコンテナの作成まで丁寧に説明されています.(僕が説明するより断然わかりやすいのでこちらを覗いてみてください) 一度手を動かしてみると非常に理解が進みます. chrootpivot_rootの違いなどわかりやすかったです.

# config.json

作成するコンテナの設定は全てconfig.jsonに記述されています.このファイルはrunc specを実行するとテンプレートが作成されます. 基本的には変更せずに使用します. ファイルの中身は前回のポストを参照してください. config.json (opens new window)

コンテナプロセスを作成する際にconfig.jsonからコンテナ起動時のコマンドやマウントするディレクトリ,cgroupやlinux capabilitiesなどの設定を読み込んで作成します.

# 開発

それでは作成したプログラムをみていきます. CLIアプリケーションとして作成するのでgoogle/subcommands (opens new window)を使用しました. こちらがmain.go

func main() {
	subcommands.Register(subcommands.FlagsCommand(), "")
	subcommands.Register(new(cmd.Create), "")
	subcommands.Register(new(cmd.Start), "")
	const internalOnly = "internal only"
	subcommands.Register(new(cmd.Init), internalOnly)
	flag.Parse()
	setDebugMode(debug)
	ctx := context.Background()
	os.Exit(int(subcommands.Execute(ctx)))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

subcommands.Commandインターフェースを満たした型のインスタンスを登録することでcreateなどのサブコマンドを扱えるようにします. initサブコマンドは内部のみ呼び出されるべきなのでinternal onlyというラベルをつけています. 各種サブコマンドの実装はcmd以下に配置しています.

# create

まずはcreateをみてみましょう. コマンドの中身はExecuteメソッドに記述します. 主な処理は以下です.

  • バンドルディレクトリ(config.jsonとコンテナのルートファイルシステムを配置する)を指定してconfig.jsonを読み込んでspecs.Spec構造体にマッピング
  • initサブコマンドを内部で呼び出すためのFactory構造体のインスタンスを生成
  • Factory.Createメソッドでinitサブコマンドを別プロセスとして実行

順に処理内容をみてみます.

# config.jsonとspces.Spec

config.jsonopencontainer/runtime-spec/specs-go (opens new window)specs.Spec構造体にマッピングできます. フィールドが大量にあるので今回はこれを流用しました. そしてこれらをConfig構造体にマッピングします.

type Config struct {
	Id     string
	Bundle string
	Spec   *specs.Spec
}
1
2
3
4
5

# initプロセスを作成するFactory構造体

Factory型にはcreateを実行しているプロセスからinitプロセスを起動するための構造体です.

factory := &Factory{
		Id:       id,
		Pid:      -1,
		Root:     root,
		InitPath: "/proc/self/exe",
		InitArgs: []string{os.Args[0], "-debug", "init", id}, // path to mycon init
	}
1
2
3
4
5
6
7

InitPathには/proc/self/exeという文字列を渡していますが,これは現在実行中のプロセスへのパスを指すシンボリックリンクとなっています. またInitArgsos.Args[0]はコマンドライン引数の0番目なのでこの場合./myconという実行ファイルを指していることとなります. その後デバッグオプションをつけてinitをサブコマンドとして指定しています.

# Factory.Createメソッド

さて,どのようにinitプロセスを起動するのでしょう. Createでは以下のような処理をしています.

  • コンテナのルートディレクトリを作成
  • bundleディレクトリに移動
  • initプロセスとstartコマンドのプロセス間でシグナルをやり取りするfifoファイル作成
  • 実行するコマンドの作成と実行

具体的な処理は以下の様になってます.

# コンテナルートディレクトリの作成

/run/mycon/[container id]というディレクトリを作成します.この中にコンテナ作成時に必要なファイルなどを配置します.これはコンテナに対して固有のディレクトリとなるので既に存在する場合はエラーを返します.

containerRootPath := filepath.Join(f.Root, f.Id)
	if _, err := os.Stat(containerRootPath); err == nil {
		return nil, fmt.Errorf("container root dir is already exist")
	}
	// make container dir
	if err := os.MkdirAll(containerRootPath, 0711); err != nil {
		return nil, err
	}
1
2
3
4
5
6
7
8

# bundleディレクトリに移動

if err := os.Chdir(config.Bundle); err != nil {
		logrus.Debug("failed to chdir bundle dir: %v", err)
		return nil, err
	}
1
2
3
4

bundleで指定されたディレクトリに移動します.bundleはデフォルトではカントディレクトリです.

# fifoファイルの作成

mkfifoで名前付きパイプを作成します. 名前付きパイプについてはこちら (opens new window)を参照してください. initプロセスとstartプロセスで通信を行うのに使用します.

if err := unix.Mkfifo(path, 0744); err != nil {
		return fmt.Errorf("failed to create fifo file: %v", err)
	}
1
2
3

# mycon initを実行するコマンドを作成して実行

initプロセスを起動するためのコマンドを作成します. Factory型インスタンスに登録されているInitPath, InitArgsを渡して*exec.Cmdを返します. その際に各種名前空間を分離して,標準入力などを指定しています.

// buildInitCommand builds a command to start init process
func (f *Factory) buildInitCommand() *exec.Cmd {
	cmd := exec.Command(f.InitPath, f.InitArgs[1:]...)
	cmd.SysProcAttr = &unix.SysProcAttr{
		Cloneflags: unix.CLONE_NEWIPC | unix.CLONE_NEWNET | unix.CLONE_NEWNS |
			unix.CLONE_NEWPID | unix.CLONE_NEWUSER | unix.CLONE_NEWUTS,
		UidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: os.Getuid(), Size: 1},
		},
		GidMappings: []syscall.SysProcIDMap{
			{ContainerID: 0, HostID: os.Getgid(), Size: 1},
		},
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	logrus.Debugf(cmd.String())
	return cmd
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

また,作成したcmdにfifoファイルへのファイルディスクリプタを環境変数として格納します.

func (f *Factory) setFifoFd(cmd *exec.Cmd) (int, error) {
	path := filepath.Join(f.Root, f.Id, fifoName)
	fd, err := unix.Open(path, unix.O_PATH|unix.O_CLOEXEC, 0)
	if err != nil {
		logrus.Debug(err)
		return -1, err
	}
	defer unix.Close(fd)
	cmd.ExtraFiles = append(cmd.ExtraFiles, os.NewFile(uintptr(fd), fifoName))
	cmd.Env = append(cmd.Env, fmt.Sprintf("_MYCON_FIFOFD=%v", fd+3+len(cmd.ExtraFiles)-1))
	return fd, err
}
1
2
3
4
5
6
7
8
9
10
11
12

その後,実行します. これでinitプロセスが起動することとなります.

# init

さて,initプロセスを起動しました. 次はこちらをみてみます. initサブコマンドで実行される処理の実体はcmd/init.goにあります. ここではFactory.Initializeメソッドで具体的処理を行います. Factory型のメソッドとしてInitializeを定義しているのはruncがそうしていたからなんですが,今回のコードではあまりFactory型のメソッドである必要はないですね. Initializeメソッドでは先ほど保存した環境変数を取得して,config.json*specs.SpecにマッピングしてInitializer型のインスタンスを作成してInitializer.Initメソッドを呼び出すという具合です.

# Initializer型

Initializer型は以下の様になっています.

type Initializer struct {
	Id           string
	FifoFd       int
	Spec         *specs.Spec
	Cgroups      *cgroups.Cgroups
	Capabilities *capabilities.Capabilities
}
1
2
3
4
5
6
7

Cgroup型やCapabilities型については後述します.

# Initializer.Initメソッド

このメソッドがコンテナ作成のコアとなるメソッドで問題の処理を行います. 順を追って処理をみていきます. Initメソッドで行っているのは以下の様な処理です.

  • prepareRootfs(root file systemの準備)
    • コンテナのroot filesystemをbindマウント
    • config.jsonで指定されているデバイス周りをマウント(問題が起きている箇所)
    • cgroupでハードウェアリソースを制限
    • pivot_root
  • capabilityのセット
  • startサブコマンドからの合図を待ち受ける
  • コンテナをスタート

という具合で処理が行われます.

# rootfsのbindマウント

バインドマウントを行うことで,コンテナのルートファイルシステムをbundle/rootfsにします.

func (i *Initializer) prepareRoot() error {
	// mount
	if err := unix.Mount("", "/", "", unix.MS_SLAVE|unix.MS_REC, ""); err != nil {
		return err
	}
	return unix.Mount(i.Spec.Root.Path, i.Spec.Root.Path, "bind", unix.MS_BIND|unix.MS_REC, "")
}
1
2
3
4
5
6
7

# config.jsonで指定されているデバイスをマウントする

次はconfig.jsonで指定されているデバイスをマウントします.ここで問題が発生しました. デフォルトのconfig.jsonで指定されているデバイスは以下の様になっています.

"mounts": [
		{
			"destination": "/proc",
			"type": "proc",
			"source": "proc"
		},
		{
			"destination": "/dev/shm",
			"type": "tmpfs",
			"source": "shm",
			"options": [
				// ...
			]
		},
		{
			"destination": "/dev/mqueue",
			"type": "mqueue",
			"source": "mqueue",
			"options": [
				// ...
			]
		},
		{
			"destination": "/dev/pts",
			"type": "devpts",
			"source": "devpts",
			"options": [
				// ...
			]
		},
		{
			"destination": "/dev",
			"type": "tmpfs",
			"source": "tmpfs",
			"options": [
				// ...
			]
		},
		{
			"destination": "/sys",
			"type": "sysfs",
			"source": "sysfs",
			"options": [
				// ...
			]
		},
		{
			"destination": "/sys/fs/cgroup",
			"type": "cgroup",
			"source": "cgroup",
			"options": [
				// ...
			]
		}
	],
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

マウントするデバイスのスライスを*specs.Spec.Mountsから取り出してunix.Mountメソッドで逐一マウントしていく感じです. コードがこちら.(デバッグ出力などの不要なものを削っています.) 後述しますが,この部分で問題が発生しています.

func Mount(root *specs.Root, mounts []specs.Mount) error {
	wd, err := os.Getwd()
	if err != nil {
		return err
	}
	rootfsPath := root.Path
	if !filepath.IsAbs(rootfsPath) {
		rootfsPath = filepath.Join(wd, rootfsPath)
	}
	for _, m := range mounts {
		target := filepath.Join(rootfsPath, m.Destination)
		if _, err := os.Stat(target); err != nil {
			if err := os.MkdirAll(target, 0755); err != nil {
				return err
			}
		}
		flags, _, data, _ := parseMountOptions(m.Options)
		if err := unix.Mount(m.Source, target, m.Type, uintptr(flags), data); err != nil {
			logrus.Debug(err)
			return err
		}
	}
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

この様にしてスライスから取り出した各要素に対してマウントを繰り返す様にしています.基本的にruncのコードもその様になっていました.

# cgroup

cgroupに関しても,config.jsonで指定された値をセットするという感じです. 一例がこちら.

func (cg *Cgroups) limitCpu() error {
	if cg.Resources == nil || cg.Resources.CPU == nil {
		logrus.Debugf("cpu limitation is not set")
		return nil
	}
	dir := filepath.Join(cg.Root, "cpu", "mycon")
	if err := os.MkdirAll(dir, 0700); err != nil {
		return err
	}
	if cg.Resources.CPU.Shares != nil {
		if err := writeFile(dir, cpuShares, strconv.FormatUint(*cg.Resources.CPU.Shares, 10)); err != nil {
			return err
		}
	}
	// ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

この場合だと,/sys/fs/cgroup/cpu/mycon/cpu.sharesに値を書き込むことでセットしています.

# pivot_root

最後にpivot_rootを行って,コンテナプロセスのルートファイルシステムを隔離します. chrootpivot_rootの違いに関してはこちら (opens new window)を参照してください. pivot_rootを実際に行うコードがこちら.

func (i *Initializer) pivotRoot() error {
	oldroot, err := unix.Open("/", unix.O_DIRECTORY|unix.O_RDONLY, 0)
	if err != nil {
		logrus.Debugf("failed to open old root")
		return err
	}
	defer unix.Close(oldroot)
	newroot, err := unix.Open(i.Spec.Root.Path, unix.O_DIRECTORY|unix.O_RDONLY, 0)
	if err != nil {
		logrus.Debug("failed to open new root: ", i.Spec.Root.Path)
		cd, _ := os.Getwd()
		logrus.Debug("now in ", cd)
		return err
	}
	defer unix.Close(newroot)
	// fetch new root file system
	if err := unix.Fchdir(newroot); err != nil {
		logrus.Debug("failed to fetch new root")
		return err
	}
	if err := unix.PivotRoot(".", "."); err != nil {
		logrus.Debugf("failed to pivot_root: %v", err)
		return err
	}
	if err := unix.Fchdir(oldroot); err != nil {
		logrus.Debug("failed to fetch old root")
		return err
	}
	if err := unix.Mount("", ".", "", unix.MS_SLAVE|unix.MS_REC, ""); err != nil {
		logrus.Debug("failed to mount .")
		return err
	}
	if err := unix.Unmount(".", unix.MNT_DETACH); err != nil {
		logrus.Debug("failed to unmount .")
		return err
	}
	if err := unix.Chdir("/"); err != nil {
		logrus.Debug("failed to chdir /")
		return fmt.Errorf("failed to chdir: %v", err)
	}
	return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

まず,oldrootに現在のファイルシステムのrootを開いたfdを保存,また,newrootに新しいファイルシステムのルートとなるポイントを開いたfdを保存します. その後,newrootに移動してpivot_rootを行います.

pivot_rootが行えるにはいくつか条件があります.こちら (opens new window)を参照してください. pivot_rootに関してはまだ僕も理解が浅いのでLinuxのファイルシステムなどについてもっと勉強する必要がありそうです.

# capability

capabilitiesのセットにはsyndtr/gocapability/capability (opens new window)を使用しています. config.jsonに設定されたcapabilitiesを次の構造体にマッピングしています.

type Capabilities struct {
	CapMap      map[string]capability.Cap
	Pid         capability.Capabilities
	Bounding    []capability.Cap
	Inheritable []capability.Cap
	Effective   []capability.Cap
	Permitted   []capability.Cap
	Ambient     []capability.Cap
}
1
2
3
4
5
6
7
8
9

# startを待ち受ける

リソースの分離などが完了したあと,スタートするためにシグナルを待ち受けます.

if err := <- i.waitToStart(); err != nil {
		logrus.Debug("failed to wait to start: ", err)
		return err
	}
1
2
3
4

waitToStartでは/proc/self/fd/%dで環境変数に渡されたファイルディスクリプタの値でファイルを開きます.

スタートの合図があった場合,セットされていたコマンドを実行します.

# 実装まとめ

以上がこれまで僕が実装した部分になります.とりあえずプロセス起動,cgroup, capabiltiesと段階を踏んで実装してきました.ここら辺はコミットを辿ってみてください. また,上述したマウントできない問題によりプロセス間でシグナルを送受信することができず,createコマンドを実行するとwaitToStartでエラーを吐くためwaitToStartの部分をコメントアウトしてそのままプロセスを起動している状態です. また,プロセスを起動しても/dev/ptsがマウントできていないせいか,入力を受け付けてくれず,すぐにプロセスからログアウトするという状況になってます.

# まとめ

今回は自作コンテナに挑戦しましたが,エラーを解決できず,いったん断念してLinuxやその他の知識をもっとつけてから続きをしようかなと思っています. なかなかうまくいきませんね.

# 参考

# OCI runtime specification