自作のコンテナもどきをGO言語で作成する

こんにちは。Supership株式会社インフラ基盤統括室のhym17です。

:::note warn
この記事は、[Supershipグループ Advent Calendar 2023](https://qiita.com/advent-calendar/2023/supership)のxx日目の記事になります。
:::
# TL;DR
簡単な自作コンテナもどきについて、紹介したいと思います

もっと発展して色々やりたい！簡単すぎるって方はもっと複雑化したのがあるので[GitHub](https://github.com/hayama17/go_container)を参照してみて下さい！

## コンテナとは

Linuxの機能であるnamespaceとcgroupsによってプロセスのisolationを高めたものです。そのため、コンテナはVMと違ってプロセスであり軽量な実行可能になっています。

この記事では、このnamespaceを使って分離されたプロセスを作る事を目的とします。


### Namespace
Linuxが利用するリソースに一意なnamespaceを割り当てることでリソースの分離を行います。

PID namespaceにおいては、コンテナ内でPID 1のプロセスを実行しても、ホストOS上では異なるPIDが割り当てられます。このため、同じKernel上であっても、異なるnamespaceになればPIDが分離され、同じPIDを利用することが可能になります。

User namespaceにおいても、コンテナ内でUID 0（root）でプロセスを実行しても、ホストOS上では異なるUIDが割り当てられ実行されます。同じKernel上であっても、異なるnamespaceになればUIDが分離されるため、コンテナ内での特権操作がホストOSに影響を与える事は無いです。

#### コンテナでnamespaceで分離するリソース
* pid: プロセスID。システム内での一意なことを表すID
* net: ネットワークの管理
* ipc: プロセス間通信の管理
* mnt: ファイルシステムマウント管理
* uts: バージョン識別子, カーネルの管理

### Control Groups
Control GroupsはCgroupsと呼ばれ、特にコンテナ環境においてリソース管理や制御を行います。

各コンテナに対してCPU、メモリ、ブロックI/Oなどのリソースの利用を制限でき物理的なリソースの分離を行っています。これにより、あるコンテナのリソース使用量が増加しても他のコンテナに影響を与えないようになります。



##  設計
https://amasuda.xyz/post/2020-03-07-create-container-with-golang/
https://medium.com/@ssttehrani/containers-from-scratch-with-golang-5276576f9909

こちらの記事を参考にします

### 実装する機能
* namespaceによるリソースの分離
* pivot rootによるプロセス用のファイルシステムの用意

### 処理の流れ
コンテナエンジンである親プロセスと、コンテナとして利用する子プロセスの二つを作成します。


#### 親プロセス
1. namespaceを指定して、子プロセスを実行

#### 子プロセス
1. pivot rootを使ってファイルシステムを用意


##  実装
go言語を用いて、namespaceの設定を行います。

###  親プロセス、子プロセスの分岐部分
forkした際に自身を再帰的に呼出します。ただ、引数によって親プロセスの処理になるのか、子プロセスの処理になるのか分岐出来るようにします。
```golang
func main() {
	switch os.Args[1] { 
	case "run"://親プロセス
		parent()
	case "child"://子プロセス
		if err := child(); err != nil {
			panic(err)
		}
	default:
		panic("wat should I do")
	}
}
```


### namespaceを分けてforkする
* 引数で指定したnamespaceを指定してfork
```golang
func simple_parent() error {
	cmd := exec.Command("/proc/self/exe", append([]string{"child"}, os.Args[2:]...)...)

	cmd.SysProcAttr = &syscall.SysProcAttr{
		Cloneflags: syscall.CLONE_NEWIPC |
			syscall.CLONE_NEWNET |
			syscall.CLONE_NEWNS |
			syscall.CLONE_NEWPID |
			syscall.CLONE_NEWUTS,
	}

	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("ERROR", err)
		os.Exit(1)
	}

	return nil
}

```

### 子プロセス　コンテナの設定
* コンテナのホスト名の設定
* コンテナのprocのマウント
* Pivot rootを行い、コンテナのrootファイルシステムを切り替える

```golang
func simple_child() error {

	//set hostname
	log.Println("set hostname")
	if err := syscall.Sethostname([]byte("container")); err != nil {
		return fmt.Errorf("Setting hostname failed: %w", err)
	}

	//mount /proc
	log.Println("mount /proc")
	if err := syscall.Mount("proc", "/newroot/proc", "proc", syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV, ""); err != nil {
		return fmt.Errorf("Proc mount failed: %w", err)
	}

	//pivot root
	log.Println("prepare Rootfs")
	if err := syscall.Mount("", "/", "", syscall.MS_PRIVATE|syscall.MS_REC, ""); err != nil {
		return fmt.Errorf("prepare Rootfs: %w", err)
	}

	log.Println("bind mount /newroot")
	if err := syscall.Mount("/newroot", "/newroot", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil {
		return fmt.Errorf("bind mounting /newroot: %w", err)
	}

	log.Println("mkdir /newroot/putold")
	if err := os.MkdirAll("/newroot/putold", 0700); err != nil {
		return fmt.Errorf("creating directory: %w", err)
	}

	log.Println("pivot_root")
	if err := syscall.PivotRoot("/newroot", "/newroot/putold"); err != nil {
		return fmt.Errorf("pivot root: %w", err)
	}

	log.Println("cd /")
	if err := os.Chdir("/"); err != nil {
		return fmt.Errorf("change dir to /: %w", err)
	}

	if err := syscall.Unmount("/putold", syscall.MNT_DETACH); err != nil {
		return fmt.Errorf("unmount old root dir %w", err)
	}

	cmd := exec.Command(os.Args[1], os.Args[2:]...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("ERROR", err)
		os.Exit(1)
	}
	return nil
}
```

## 実行方法
rootのファイルシステムの準備とsimple.goをbuildすれば実行できます。(Linuxのみ)
```
cd ~
git clone https://github.com/hayama17/go_container.git
go build simple.go
sudo mkdir /newroot
sudo cd /newroot
sudo wget https://dl-cdn.alpinelinux.org/alpine/v3.16/releases/x86_64/alpine-minirootfs-3.16.2-x86_64.tar.gz
sudo tar -xzf alpine-minirootfs-3.16.2-x86_64.tar.gz
sudo rm alpine-minirootfs-3.16.2-x86_64.tar.gz
cd ~/go_container
sudo ./simple run /bin/sh
```

## おわりに

作成したコードは下記のURLで公開しています。是非1個上のCgroupやvethにも挑戦してみて下さい。
https://github.com/hayama17/go_container

コンテナは特別な技術を使っているわけでもなく、システムコールを利用すれば簡単にできるんだよと感じたかと思います。また、是非コンテナはプロセスなんだよ～と啓蒙していただくと幸いです。



:::note
**最後に宣伝です。**

Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership 採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。