├── 00 └── README.md ├── 01 └── README.md ├── 02 ├── README.md └── examples │ ├── 1.go │ ├── 2.go │ ├── 3.go │ ├── 4.go │ ├── 5.go │ ├── 6.go │ ├── 7.go │ ├── 8.go │ ├── main.go │ └── unchroot.go ├── 03 └── README.md ├── 04 └── README.md ├── LICENSE └── README.md /00/README.md: -------------------------------------------------------------------------------- 1 | # Setup 2 | 3 | 講義を開始する前に、講義で使うためのインスタンスにログインする準備をします。 4 | 皆さんには事前に、インスタンスの IP アドレスと、そのインスタンスにログインするための秘密鍵をお渡しします。 5 | 6 | 受け取った秘密鍵は、SSH ログインで利用可能なように、次の操作を行ってください。 7 | 8 | ``` 9 | $ chmod 600 container-internship-2019.pem 10 | $ mv container-internship-2019.pem ~/.ssh 11 | ``` 12 | 13 | 会社の Wi-Fi に接続の上、次のコマンドを実行してみてください。 14 | 15 | ``` 16 | $ ssh ${インスタンスの IP アドレス} -l ubuntu -i ~/.ssh/container-internship-2019.pem 17 | ``` 18 | 19 | 上手くログインが成功すればセットアップは完了です。 20 | -------------------------------------------------------------------------------- /01/README.md: -------------------------------------------------------------------------------- 1 | # コンテナ概要 2 | 3 | 「コンテナ技術」(あるいは単に「コンテナ」)と言った際には、様々な定義がありました(あります)。 4 | 2019 年現在では、コンテナ技術の標準化が進むと共に、ある程度の共通見解ができてきています。 5 | 6 | ここでは、2019 年の Web の文脈において単純に「コンテナ技術」と言った際にどのような意味合いを持つのか、 7 | また、現在のコンテナ技術がどのようにして成り立っているのかについての概要を説明します。 8 | 9 | ## コンテナ技術とは 10 | 11 | コンテナ技術とは、アプリケーションとそれを動かすために必要な依存関係すべてをパッケージ化し、 12 | そのパッケージをなるべく独立して動作させるような技術のことを指しています。 13 | 14 | コンテナ技術は、大きく分けて「コンテナイメージ」と「コンテナランタイム」という 2 つの技術で成り立っています。 15 | これら 2 つの仕様は、Open Container Initiative (OCI) によって標準化が行われています。 16 | 17 | (refs: [What is a Container?](https://www.docker.com/resources/what-container)) 18 | (refs: [Understanding Linux containers](https://www.redhat.com/en/topics/containers)) 19 | 20 | ## コンテナイメージ 21 | 22 | コンテナ技術における、アプリケーションと依存関係を全て含むようなパッケージのことを「コンテナイメージ」と呼びます。 23 | 24 | コンテナイメージは、アプリケーションの実行に必要な全てのファイルを含んでいるため、 25 | コンテナを動作させる環境さえ用意すれば、様々なアプリケーションをその環境で動かすことができるようになっています。 26 | 27 | コンテナイメージのフォーマットは、[opencontainers/image-spec](https://github.com/opencontainers/image-spec) にて定められています。 28 | 29 | ## コンテナランタイム 30 | 31 | コンテナイメージを動作させる環境のことを「コンテナランタイム」と呼びます。 32 | 33 | コンテナランタイムの仕様は、[opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec) にて定められています。 34 | 35 | コンテナランタイムには様々な実装が存在し、実装ごとに様々な特徴があります。 36 | 37 | - [opencontainers/runc](https://github.com/opencontainers/runc) 38 | - Linux のプロセスとしてコンテナを実行する 39 | - プロセスレベルの分離のため起動は高速 40 | - ホスト OS の脆弱性などの影響を受けやすい 41 | - 2019 年現在 Docker 標準のコンテナランタイムとして利用されている 42 | - [google/gvisor (runsc)](https://github.com/google/gvisor) 43 | - ユーザ空間上にカーネルを提供し、その上でコンテナを実行する 44 | - アプリケーションから呼ばれるシステムコールは監視・制限される 45 | - 起動は高速だがアプリケーションの動作にオーバーヘッドなどがある 46 | - gVisor で実装されていないシステムコールもある 47 | - [kata-containers/runtime](https://github.com/kata-containers/runtime) 48 | - 仮想マシンを起動させ、その上でコンテナを実行する 49 | - 分離度が高く他のコンテナの影響を受けにくい 50 | - 分離度が高くホスト OS の脆弱性の影響などを受けにくい 51 | - 仮想マシン起動時のオーバーヘッドが高い 52 | 53 | (refs: [ユビキタスデータセンターOSの文脈におけるコンテナ実行環境の分類](https://hb.matsumoto-r.jp/entry/2019/02/08/135354)) 54 | 55 | コンテナランタイムの仕様が標準化されたことにより、ニーズに応じて様々なランタイムを選択したり、実装することができるようになってきました。 56 | 今回のインターンシップでは、特に Linux 上のプロセスを用いたコンテナランタイムについて説明し、実装することで、コンテナランタイムへの理解を深めていきます。 57 | -------------------------------------------------------------------------------- /02/README.md: -------------------------------------------------------------------------------- 1 | # Linux プロセス型コンテナ入門 2 | 3 | 初期の Docker や、現在の Docker 標準コンテナランタイムである runc で用いられている、 4 | Linux カーネルの機能を用いたコンテナの実装の概要について説明します。 5 | 6 | ## Linux とプロセス 7 | 8 | Linux では、実行するタスクを「プロセス」と呼ばれる単位に分けて管理をしています。 9 | Linux は、起動時に全てのプロセスの親となる `init` プロセスを起動し、`init` プロセスが更に必要なプロセスを起動していきます。 10 | 11 | これは Linux 上で `pstree -p` などのコマンドを実行することで確認することが出来ます。 12 | (なお、`/sbin/init` は 今回の環境では `/lib/systemd/systemd` へシンボリックリンクが貼られています) 13 | 14 | Linux 環境において、ユーザがプロセスを新たに実行するときには、`fork` 並びに `exec` (`execve`) システムコールを利用するのが一般的です。 15 | 16 | `fork` システムコールでは、実行元のプロセスを複製して新しいプロセスを生成します。 17 | このとき、実行元のプロセスを親プロセス、複製された新しいプロセスを子プロセスと呼びます。 18 | 19 | 親プロセスと子プロセスは、自身の プロセス ID や、自身の親プロセスの ID などのいくつかの点を除いて、全く同じものになっています。 20 | 21 | `exec` (`execve`) システムコールでは、自身のプロセスを、実行するプログラムで上書きして実行します。 22 | 23 | `exec` 系のシステムコールでは、実行した際にプロセスが置き換わってしまうため、プログラムが成功したか・失敗したかなどの判定を行うことができません。 24 | 25 | そのため、Linux 環境の一般的なプロセスの起動では、`fork` を利用し、親プロセスから子プロセスを起動し、起動した子プロセスで `exec` を行い、 26 | プロセス間通信に用いられる `pipe` を利用し、親プロセスと置き換わった後の子プロセスでやりとりを行う、といった方法がよく用いられます。 27 | 28 | (refs: `man 2 fork`, `man 2 execve`, `man 3 exec`, `man 2 pipe`) 29 | 30 | コンテナ環境では、起動しているコンテナがなるべく他のコンテナに影響を与えないように分離されていることが望ましいです。 31 | 通常の Linux のプロセスの起動では、親プロセスと資源を共有したり、他のプロセスの情報が見えたり、場合によっては操作してしまえたりします。 32 | 33 | ここからは、コンテナの実装に必要な、親プロセスや他のプロセスと可能な限り分離して子プロセスを起動するための機能を紹介していきます。 34 | 35 | ## Linux Namespaces 36 | 37 | Linux では、プロセスが使うリソースを分離して提供する、Namespaces という機能があります。 38 | 分離できるリソースは以下のとおりです。 39 | 40 | - IPC 41 | - プロセス間通信で使うリソース(共有メモリ, セマフォ等) 42 | - Network 43 | - ネットワークデバイスや IP アドレス、ルーティングテーブルなど 44 | - Mount 45 | - ファイルシステムツリー 46 | - PID 47 | - プロセス ID 48 | - User 49 | - ユーザ ID / グループ ID 50 | - UTS 51 | - nodename や domainname など 52 | 53 | ### 名前空間の分離 54 | 55 | いくつかに関して、実際に試してみましょう。 56 | 次のような、`/bin/sh` を起動する際に名前空間を利用する Go のプログラムとして、`main.go` を用意します。 57 | Go の `cmd.SysProcAttr` には、`clone(2)` に渡すのと同じような flags を渡すことができます。 58 | (`clone(2)` は `fork(2)` と似たような子プロセスを生成するシステムコール) 59 | 60 | 試しに、IPC, Network, User に対して名前空間を利用するようにしてみます (`Cloneflags` に指定されている値に注目する)。 61 | 62 | (refs: `man 2 clone`) 63 | 64 | ```go 65 | // +build linux 66 | package main 67 | 68 | import ( 69 | "fmt" 70 | "os" 71 | "os/exec" 72 | "syscall" 73 | ) 74 | 75 | func main() { 76 | cmd := exec.Command("/bin/sh") 77 | cmd.SysProcAttr = &syscall.SysProcAttr{ 78 | Cloneflags: syscall.CLONE_NEWIPC | 79 | syscall.CLONE_NEWNET | 80 | syscall.CLONE_NEWUSER, 81 | } 82 | 83 | cmd.Stdin = os.Stdin 84 | cmd.Stdout = os.Stdout 85 | cmd.Stderr = os.Stderr 86 | 87 | if err := cmd.Run(); err != nil { 88 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 89 | os.Exit(1) 90 | } 91 | os.Exit(0) 92 | } 93 | ``` 94 | 95 | このプログラムの `CloneFlags` があるものとないものを作成し、 96 | それぞれを Linux 上で実行して、Namespace を設定する前と後で出力を比較してみてください。 97 | 98 | - `ipcs` 99 | - `ip addr show` 100 | - `id` 101 | 102 | ### UID/GID の設定 103 | 104 | User namespace を分離した後では、sh を実行したユーザ/グループが、nobody/nogroup になってしまっています。 105 | 新しい User 名前空間で実行されるプロセスの UID/GID を設定するためには、`/proc/[pid]/uid_map` と `/proc/[pid]/gid_map` に対して書き込みを行います。 106 | 107 | (refs: `man 7 user_namespaces`) 108 | 109 | Go の `Cmd.SysProcAttr` には、`CLONE_NEWUSER` した際の `UidMappings`, `GidMappings` を渡すことができ、 110 | 渡した値を `/proc/[pid]/uid_map` 並びに `/proc/[pid]/gid_map` に適切に書き込みを行ってくれるようになっています。 111 | 112 | (refs: `https://github.com/golang/go/blob/go1.10.4/src/syscall/exec_linux.go#L438-L516`) 113 | 114 | ここでは、プロセスを実行したユーザが、名前空間を分離した後のプロセスで uid/gid が 0 (root) になるように設定してみましょう。 115 | 116 | ```diff 117 | --- 1.go 2019-03-19 13:46:27.000000000 +0900 118 | +++ 2.go 2019-03-19 13:49:33.000000000 +0900 119 | @@ -14,6 +14,20 @@ 120 | Cloneflags: syscall.CLONE_NEWIPC | 121 | syscall.CLONE_NEWNET | 122 | syscall.CLONE_NEWUSER, 123 | + UidMappings: []syscall.SysProcIDMap{ 124 | + { 125 | + ContainerID: 0, 126 | + HostID: os.Getuid(), 127 | + Size: 1, 128 | + }, 129 | + }, 130 | + GidMappings: []syscall.SysProcIDMap{ 131 | + { 132 | + ContainerID: 0, 133 | + HostID: os.Getgid(), 134 | + Size: 1, 135 | + }, 136 | + }, 137 | } 138 | 139 | cmd.Stdin = os.Stdin 140 | ``` 141 | 142 | このように変更を加えた後、`go run main.go` で実行したシェル内で `id` などを実行して、 143 | 正しく root として認識されていることを確認してください。 144 | 145 | ### UTS の設定 146 | 147 | 次に、hostname や domainname などを管理する UTS について見ていきます。 148 | 149 | hostname を設定してからプロセスを起動するために、次のようなフローでプロセスの起動を行うことにします。 150 | 151 | - プロセスの第一引数が `run` かどうかチェックする 152 | - `run` であれば Namespaces を設定しつつ第一引数を `init` に変えて自分自身を実行する 153 | - プロセスの第一引数が `init` かどうかチェックする 154 | - `init` であれば hostname を設定した後に自分自身を `/bin/sh` に置き換える 155 | 156 | このようにすることで、Namespaces が設定された後に hostname を設定しつつ `/bin/sh` を実行することができるようになります。 157 | コードを見たほうが早いと思うので、実際に見てみましょう。 158 | 159 | ```go 160 | // +build linux 161 | package main 162 | 163 | import ( 164 | "fmt" 165 | "os" 166 | "os/exec" 167 | "syscall" 168 | ) 169 | 170 | func Run() { 171 | cmd := exec.Command("/proc/self/exe", "init") 172 | cmd.SysProcAttr = &syscall.SysProcAttr{ 173 | Cloneflags: syscall.CLONE_NEWIPC | 174 | syscall.CLONE_NEWNET | 175 | syscall.CLONE_NEWUSER | 176 | syscall.CLONE_NEWUTS, 177 | UidMappings: []syscall.SysProcIDMap{ 178 | { 179 | ContainerID: 0, 180 | HostID: os.Getuid(), 181 | Size: 1, 182 | }, 183 | }, 184 | GidMappings: []syscall.SysProcIDMap{ 185 | { 186 | ContainerID: 0, 187 | HostID: os.Getgid(), 188 | Size: 1, 189 | }, 190 | }, 191 | } 192 | 193 | cmd.Stdin = os.Stdin 194 | cmd.Stdout = os.Stdout 195 | cmd.Stderr = os.Stderr 196 | 197 | if err := cmd.Run(); err != nil { 198 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 199 | os.Exit(1) 200 | } 201 | 202 | os.Exit(0) 203 | } 204 | 205 | func InitContainer() error { 206 | if err := syscall.Sethostname([]byte("container")); err != nil { 207 | return fmt.Errorf("Setting hostname failed: %w", err) 208 | } 209 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 210 | return fmt.Errorf("Exec failed: %w", err) 211 | } 212 | return nil 213 | } 214 | 215 | func Usage() { 216 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 217 | os.Exit(2) 218 | } 219 | 220 | func main() { 221 | if len(os.Args) <= 1 { 222 | Usage() 223 | } 224 | switch os.Args[1] { 225 | case "run": 226 | Run() 227 | case "init": 228 | if err := InitContainer(); err != nil { 229 | fmt.Fprintf(os.Stderr, "%+v\n", err) 230 | os.Exit(1) 231 | } 232 | os.Exit(0) 233 | default: 234 | Usage() 235 | } 236 | } 237 | ``` 238 | 239 | このようにプログラムを変更した後、プログラムを実際に `go run main.go run` などで実行してみて、 240 | `uname -n` コマンドの出力を確認してみましょう。 241 | 242 | ### PID と Mount 243 | 244 | 最後に、プロセス ID の分離とファイルシステムツリーの分離について見ていきます。 245 | 246 | `cmd.SysProcAttr` を以下のように変更し、`syscall.CLONE_NEWPID` と `syscall.CLONE_NEWNS` を追加します。 247 | 248 | ```diff 249 | --- 3.go 2019-03-19 13:53:22.000000000 +0900 250 | +++ 4.go 2019-03-19 13:53:28.000000000 +0900 251 | @@ -13,6 +13,8 @@ 252 | cmd.SysProcAttr = &syscall.SysProcAttr{ 253 | Cloneflags: syscall.CLONE_NEWIPC | 254 | syscall.CLONE_NEWNET | 255 | + syscall.CLONE_NEWNS | 256 | + syscall.CLONE_NEWPID | 257 | syscall.CLONE_NEWUSER | 258 | syscall.CLONE_NEWUTS, 259 | UidMappings: []syscall.SysProcIDMap{ 260 | ``` 261 | 262 | `ps` コマンドなどが正しく分離された名前空間の情報を取得できるように、`/proc` ファイルシステムをマウントしてみましょう。 263 | `InitContainer` 関数を次のように変更します。 264 | 265 | ```diff 266 | --- 4.go 2019-03-19 13:54:30.000000000 +0900 267 | +++ 5.go 2019-03-19 13:58:13.000000000 +0900 268 | @@ -49,6 +49,9 @@ 269 | if err := syscall.Sethostname([]byte("container")); err != nil { 270 | return fmt.Errorf("Setting hostname failed: %w", err) 271 | } 272 | + if err := syscall.Mount("proc", "/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 273 | + return fmt.Errorf("Proc mount failed: %w", err) 274 | + } 275 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 276 | return fmt.Errorf("Exec failed: %w", err) 277 | } 278 | ``` 279 | 280 | Go で `syscall.Mount` を呼び出す事でマウントを行えます。渡しているフラグに関しては `man 2 mount` を参考にしてみてください。 281 | 282 | 上記のように変更したあと、`go run main.go run` などでプログラムを実行し、以下のコマンドの実行結果を見てみましょう。 283 | 284 | - `ps aufxw` 285 | - `ls -asl /proc` 286 | 287 | ## chroot / pivot_root 288 | 289 | ここまでで、Namespaces を用いたリソースの分離について見てきました。 290 | 利用するリソースは分離されましたが、ファイルシステムに関してはどうでしょうか? 291 | このままでは、利用するファイルシステムは基本的に同じなため、あるコンテナが他のコンテナのファイルを読み書きすることができてしまいます。 292 | 293 | Linux には、プロセスのルートディレクトリや、ルートファイルシステムを変更する `chroot` や `pivot_root` のような機能があります。 294 | `InitContainer` 関数を次のように変更して、プロセスの実行時に `/root/chroot` をルートディレクトリにしてみましょう。 295 | 296 | (refs: `man 2 chroot`) 297 | 298 | ```diff 299 | --- 5.go 2019-03-19 13:58:13.000000000 +0900 300 | +++ 6.go 2019-03-19 14:00:44.000000000 +0900 301 | @@ -52,6 +52,12 @@ 302 | if err := syscall.Mount("proc", "/root/chroot/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 303 | return fmt.Errorf("Proc mount failed: %w", err) 304 | } 305 | + if err := syscall.Chroot("/root/chroot"); err != nil { 306 | + return fmt.Errorf("Chroot failed: %w", err) 307 | + } 308 | + if err := os.Chdir("/"); err != nil { 309 | + return fmt.Errorf("Chdir failed: %w", err) 310 | + } 311 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 312 | return fmt.Errorf("Exec failed: %w", err) 313 | } 314 | ``` 315 | 316 | 実行前に `/root/chroot` 並びに必要なディレクトリを作成しておきます。 317 | 318 | ```sh 319 | mkdir -p /root/chroot/proc 320 | ``` 321 | 322 | また、chroot 後の環境でも `sh` と `ls` が利用できるように、`sh`, `ls` 並びに必要な静的ライブラリを設置します。 323 | 324 | ```sh 325 | mkdir -p /root/chroot/proc 326 | mkdir -p /root/chroot/bin 327 | mkdir -p /root/chroot/lib 328 | 329 | cp /bin/sh /root/chroot/bin 330 | cp /bin/ls /root/chroot/bin 331 | 332 | ldd /bin/sh 333 | ldd /bin/ls 334 | 335 | cp /lib/x86_64-linux-gnu/libc.so.6 /root/chroot/lib 336 | cp /lib64/ld-linux-x86-64.so.2 /root/chroot/lib 337 | cp /lib/x86_64-linux-gnu/libselinux.so.1 /root/chroot/lib 338 | cp /lib/x86_64-linux-gnu/libpcre.so.3 /root/chroot/lib 339 | cp /lib/x86_64-linux-gnu/libdl.so.2 /root/chroot/lib 340 | cp /lib/x86_64-linux-gnu/libpthread.so.0 /root/chroot/lib 341 | 342 | cd /root/chroot/ 343 | ln -s lib lib64 344 | 345 | cd 346 | ``` 347 | 348 | 上記のように変更したあと、`go run main.go run` などでプログラムを実行してみましょう。 349 | 350 | ### Escaping a chroot 351 | 352 | さて、`chroot(2)` を利用してルートディレクトリを分離しました!これで実行されたプロセスからは上位のディレクトリが見えなくなって安全です! 353 | 354 | ...というのは本当でしょうか? 355 | 356 | chroot するディレクトリである `/root/chroot/` に、`unchroot.go` という次のようなファイルを設置してみます。 357 | 358 | ```go 359 | package main 360 | 361 | import ( 362 | "fmt" 363 | "os" 364 | "syscall" 365 | ) 366 | 367 | func main() { 368 | if _, err := os.Stat(".42"); os.IsNotExist(err) { 369 | if err := os.Mkdir(".42", 0755); err != nil { 370 | fmt.Println("Mkdir failed") 371 | } 372 | } 373 | if err := syscall.Chroot(".42"); err != nil { 374 | fmt.Println("Chroot to .42 failed") 375 | } 376 | if err := syscall.Chroot("../../../../../../../../../../../../../../../.."); err != nil { 377 | fmt.Println("Jail break failed") 378 | } 379 | if err := syscall.Exec("/bin/sh", []string{""}, os.Environ()); err != nil { 380 | fmt.Println(err) 381 | fmt.Println("Exec failed") 382 | } 383 | } 384 | ``` 385 | 386 | 予めこのプログラムをビルドしておきます。 387 | 388 | ```sh 389 | cd /root/chroot 390 | go build unchroot.go 391 | 392 | cd 393 | ``` 394 | 395 | このプログラムは、chroot されたディレクトリ内に単純に `.42` というディレクトリを作成し、まずそこに chroot します。 396 | その後 、おもむろに上位のディレクトリを対象として、もう一度 `chroot` を行います。 397 | 398 | `go run main.go run` などして、`/root/chroot` に chroot した `sh` を実行し、その後 `./unchroot` を実行してみましょう。 399 | そして、`pwd` の出力や、`cd /` の結果、`ls` した結果などを見比べてみましょう。 400 | 401 | これは、`main.go` で chroot したプロセスが、まだ `chroot(2)` を行う権限を持っているために発生しています。 402 | そのため、上位のディレクトリを指定して `chroot(2)` をし直すことが可能になってしまっています。 403 | 404 | これを避けるためには、後述する Linux capabilities の機能を利用して、プロセスが `chroot(2)` できないようにするか 405 | `pivot_root` という、root ファイルシステムを変更するシステムコールを用いて同じような機能を実装することで解決できます。 406 | 407 | ### pivot_root 408 | 409 | `main.go` を `chroot` ではなく `pivot_root` を用いた実装に変更してみましょう。 410 | 411 | `pivot_root` で利用するディレクトリを `/root/rootfs` として `chroot` の時と同じように作成していきます。 412 | 413 | ```sh 414 | mkdir -p /root/rootfs/proc 415 | mkdir -p /root/rootfs/bin 416 | mkdir -p /root/rootfs/lib 417 | 418 | cp /bin/sh /root/rootfs/bin 419 | cp /bin/ls /root/rootfs/bin 420 | 421 | cp /lib/x86_64-linux-gnu/libc.so.6 /root/rootfs/lib 422 | cp /lib64/ld-linux-x86-64.so.2 /root/rootfs/lib 423 | cp /lib/x86_64-linux-gnu/libselinux.so.1 /root/rootfs/lib 424 | cp /lib/x86_64-linux-gnu/libpcre.so.3 /root/rootfs/lib 425 | cp /lib/x86_64-linux-gnu/libdl.so.2 /root/rootfs/lib 426 | cp /lib/x86_64-linux-gnu/libpthread.so.0 /root/rootfs/lib 427 | 428 | cd /root/rootfs/ 429 | ln -s lib lib64 430 | 431 | cd 432 | ``` 433 | 434 | `main.go` を次のように変更します。 435 | 436 | ```diff 437 | --- 6.go 2019-03-19 14:00:44.000000000 +0900 438 | +++ 7.go 2019-03-19 14:10:29.000000000 +0900 439 | @@ -49,11 +49,26 @@ 440 | if err := syscall.Sethostname([]byte("container")); err != nil { 441 | return fmt.Errorf("Setting hostname failed: %w", err) 442 | } 443 | - if err := syscall.Mount("proc", "/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 444 | + if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 445 | return fmt.Errorf("Proc mount failed: %w", err) 446 | } 447 | - if err := syscall.Chroot("/root/chroot"); err != nil { 448 | - return fmt.Errorf("Chroot failed: %w", err) 449 | + if err := os.Chdir("/root"); err != nil { 450 | + return fmt.Errorf("Chdir /root failed: %w", err) 451 | + } 452 | + if err := syscall.Mount("rootfs", "/root/rootfs", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { 453 | + return fmt.Errorf("Rootfs bind mount failed: %w", err) 454 | + } 455 | + if err := os.MkdirAll("/root/rootfs/oldrootfs", 0700); err != nil { 456 | + return fmt.Errorf("Oldrootfs create failed: %w", err) 457 | + } 458 | + if err := syscall.PivotRoot("rootfs", "/root/rootfs/oldrootfs"); err != nil { 459 | + return fmt.Errorf("PivotRoot failed: %w", err) 460 | + } 461 | + if err := syscall.Unmount("/oldrootfs", syscall.MNT_DETACH); err != nil { 462 | + return fmt.Errorf("Oldrootfs umount failed: %w", err) 463 | + } 464 | + if err := os.RemoveAll("/oldrootfs"); err != nil { 465 | + return fmt.Errorf("Remove oldrootfs failed: %w", err) 466 | } 467 | if err := os.Chdir("/"); err != nil { 468 | return fmt.Errorf("Chdir failed: %w", err) 469 | ``` 470 | 471 | `pivot_root` を行うためには、いくつかの条件があります。 472 | 473 | - `pivot_root(new_root, put_old)` では `new_root` と `put_old` が両方ディレクトリである必要がある 474 | - `new_root` と `put_old` は `pivot_root` を実行するディレクトリと同じファイルシステムにあってはならない 475 | - この条件を満たすために、`/root/rootfs` を `rootfs` として `MS_BIND` を利用してマウントしています 476 | - `put_old` は `new_root` 以下に存在しなければならない 477 | - 他のファイルシステムが `put_old` にマウントされていてはならない 478 | 479 | ここで出てくる `new_root`, `put_old` はコード中ではそれぞれ `/root/rootfs`, `/root/rootfs/oldrootfs` に対応しています。 480 | 481 | (refs: `man 2 pivot_root`) 482 | 483 | 単純に `pivot_root` した後では、`/oldrootfs` というパスに元のファイルシステムがマウントされています。 484 | 元のファイルシステムにアクセスできないようにするために、`pivot_root` 行ったあとに、`/oldrootfs` をアンマウントし、`/oldrootfs` は不要なので削除しています。 485 | (余力がある方は `/oldrootfs` をアンマウント・削除する部分のコードをコメントアウトしてみて、元のファイルシステムにアクセスできるか確認してみましょう) 486 | 487 | また、事前に `proc` をマウントしておき、`rootfs` のマウント時に `MS_REC` を併せて付与することで、 488 | `proc` がマウントされた状態のファイルシステムに `pivot_root` が行えるようになっています。 489 | 490 | (refs: `man 2 mount`) 491 | 492 | ## capabilities 493 | 494 | Linux のプロセスに対する権限チェックは、特権プロセスと呼ばれる、実効ユーザ ID (euid) が 0 (つまり root のこと)のプロセスか、 495 | 非特権プロセスと呼ばれる実効ユーザ ID が 0 ではないプロセスかで大きく異なっており、特権プロセスでは全てのカーネルの権限チェックがバイパスされます。 496 | 497 | Linux capabilities では、root が持っていた権限を capability と呼ばれるいくつかのグループに分割しています。 498 | capability は、スレッド単位の属性であり、グループごとに独立に有効化・無効化を行えるようになっています。 499 | 500 | 例えば、"Escaping a chroot" の項で説明した `chroot` 環境からの脱獄は、実行するプロセスから `CAP_SYS_CHROOT` という capability を奪っておく事で回避できます。 501 | (実行するプロセスが `chroot(2)` を発行できなくなるので他のディレクトリや上位のディレクトリに `chroot` し直されることがなくなる) 502 | 503 | Go 言語から capabilities を操作するには、[syndtr/gocapability](https://github.com/syndtr/gocapability) などのライブラリを利用するのが簡単です。 504 | (実際に `runc` でも gocapability を用いて capability の設定を行っています) 505 | 506 | また、シェルから簡単に試すために、`capsh(1)` という capability を操作した上で `/bin/bash` を起動するプログラムも存在しています。 507 | (発展:Ubuntu の環境で `capsh` を利用して capability を制限し、`chown` が行えないシェルを起動してみましょう ) 508 | 509 | スレッドが実際にどのような capability を持つかは、実行ファイルについている File capability とスレッド自体の capability など、 510 | 複雑な要素によって決定されます。計算の方法や、実際にどのような capability があるかは、`man 7 capabilities` などを参考にしてください。 511 | 512 | (発展: `syscall.Exec` で呼ぶコマンドを `capsh` 経由にすることで capabilities を設定しながらコマンドを実行して確認してみましょう) 513 | (また、この方法での capability の制限にはどのような問題点があるか考えてみましょう) 514 | 515 | ## cgroups 516 | 517 | ここまでで、プロセスを起動する際のリソースの分離について手を動かしながら見てきました。 518 | 519 | しかし、プロセスが使う Linux 上のリソースが分離されていても、CPU やメモリなどの計算資源はまだ共有されてしまっています。 520 | 例えば、CPU を常にマシンの 100% 専有し続けるコンテナが起動していたら、他のコンテナや、ホストに影響を及ぼしてしまいます。 521 | 522 | こういった際に利用できるのが、Linux に実装されている cgroups (Control groups) と呼ばれるプロセスの管理機構です。 523 | cgroups では、プロセスをグループ単位でまとめ、そのグループ内のプロセスに対して、CPU やメモリなどの利用量などを制限することができるようになっています。 524 | 525 | システムコールを用いて cgroups を操作する方法ももちろんありますが、今回は簡単のために、ホストでマウント済みの cgroupfs というファイルシステムを用いて、 526 | プロセスにリソース制限を掛けてみましょう。 527 | 528 | cgroupfs は、現在では一般的に `/sys/fs/cgroups` にマウントされており、このファイルシステムに対して読み込み・書き込みの操作を行うことで、 529 | cgroups 内でのリソースの利用状況を確認したり、リソースの利用に制限を掛けることが可能になっています。 530 | cgroup には v1 と v2 があり、v2 がもちろん推奨されているのですが、多くの環境でまだ v1 が使われているという事情もあり、今回は v1 の操作について説明します。 531 | (とはいえ、v1 と v2 で今回説明する範囲での操作自体に大きな変わりはありません) 532 | 533 | 実際に `my-container` という名前の cpu レベルでの cgroup を作るには、以下のようにします。 534 |   535 | ```sh 536 | mkdir /sys/fs/cgroup/cpu/my-container/ 537 | ``` 538 | 539 | このようにすると、作成したディレクトリの配下に様々なファイルが現れます。 540 | 541 | ```sh 542 | ls /sys/fs/cgroup/cpu/my-container/ 543 | cgroup.clone_children cpu.cfs_quota_us cpuacct.stat cpuacct.usage_percpu cpuacct.usage_sys tasks 544 | cgroup.procs cpu.shares cpuacct.usage cpuacct.usage_percpu_sys cpuacct.usage_user 545 | cpu.cfs_period_us cpu.stat cpuacct.usage_all cpuacct.usage_percpu_user notify_on_release 546 | ``` 547 | 548 | どのプロセスをこの cgroup の管理下に入れるかというのを、`tasks` というファイルで管理しています。 549 | 例えば、自分自身が現在起動しているシェルをこの cgroup の管理下に入れたい場合は、次のように出来ます。 550 | 551 | ``sh 552 | echo $$ > /sys/fs/cgroup/cpu/my-container/tasks 553 | `` 554 | 555 | これで今起動しているシェルは `my-container` cgroup の管理下に入りました。 556 | 557 | 実際に CPU 制限を行ってみましょう。例えば `cpu.cfs_quota_us` という設定値は、`cpu.cfs_period_us` マイクロ秒間あたりに、何マイクロ秒間 CPU を利用できるか、という値です。 558 | 559 | `cpu.cfs_period_us` のデフォルト値は、以下の通り 100000 マイクロ秒 (0.1 秒) になっています。 560 | 561 | ```sh 562 | cat /sys/fs/cgroup/cpu/my-container/cpu.cfs_period_us 563 | 100000 564 | ``` 565 | 566 | そのため、CPU 使用率を 1% に制限したい場合は、`cpu.cfs_quota_us` に `1000` と書き込めば良いわけになります。 567 | 568 | 実際に、CPU 利用率を制限する前と後で、CPU をそれなりに利用するコマンド `yes >> /dev/null` を実行して眺めてみましょう。 569 | 570 | ```sh 571 | yes >> /dev/null & # バックグラウンドジョブとして yes >> /dev/null を起動 572 | top # yes コマンドの CPU 使用率を眺めてみる 573 | 574 | echo 1000 > /sys/fs/cgroup/cpu/my-container/cpu.cfs_quota_us 575 | echo $(pgrep yes) > /sys/fs/cgroup/cpu/my-container/tasks 576 | 577 | top # yes コマンドの CPU 使用率を眺めてみる 578 | ``` 579 | 580 | (refs: `man 7 cgroups`) 581 | 582 | ### 自作コンテナで cgroup を利用する 583 | 584 | 紹介した通り、cgroup の操作は、cgroupfs がマウントされていればファイルシステム操作で行えることがわかりました。 585 | 自作コンテナ上で、自分自身の CPU 使用率を 1% に制限した状態でシェルを起動するようにしてみましょう。 586 | 587 | ```diff 588 | --- 7.go 2019-03-19 14:41:07.000000000 +0900 589 | +++ 8.go 2019-03-20 05:50:22.000000000 +0900 590 | @@ -3,6 +3,7 @@ 591 | 592 | import ( 593 | "fmt" 594 | + "io/ioutil" 595 | "os" 596 | "os/exec" 597 | "syscall" 598 | @@ -49,7 +50,18 @@ 599 | if err := syscall.Sethostname([]byte("container")); err != nil { 600 | return fmt.Errorf("Setting hostname failed: %w", err) 601 | } 602 | - if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 603 | + 604 | + if err := os.MkdirAll("/sys/fs/cgroup/cpu/my-container", 0700); err != nil { 605 | + return fmt.Errorf("Cgroups namespace my-container create failed: %w", err) 606 | + } 607 | + if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/tasks", []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil { 608 | + return fmt.Errorf("Cgroups register tasks to my-container namespace failed: %w", err) 609 | + } 610 | + if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/cpu.cfs_quota_us", []byte("1000\n"), 0644); err != nil { 611 | + return fmt.Errorf("Cgroups add limit cpu.cfs_quota_us to 1000 failed: %w", err) 612 | + } 613 | + 614 | + if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV, ""); err != nil { 615 | return fmt.Errorf("Proc mount failed: %w", err) 616 | } 617 | if err := os.Chdir("/root"); err != nil { 618 | ``` 619 | 620 | ## その他のシステムコールや技術 621 | 622 | ここで紹介した以外にも、`seccomp(2)` などを使ってコンテナから実行するシステムコールを制限したり、 623 | overlayfs などを利用して、コンテナ内でのファイルの書き込みがホストの rootfs に影響しなくなるようにするなど、 624 | 実際に利用されているコンテナでは様々な技術が使われています。 625 | 626 | ここまでで、namespace や chroot / pivot_root, capabilities, cgroups などを見てきたのと同じように、 627 | 一つ一つは Linux のカーネルや、ファイルシステムの技術を利用しているというのは共通しています。 628 | 629 | 基本的な調べ方なども共通しているはずなので、興味がある方はぜひ実際に使っている Docker などの実装についてより調べてみて貰えればと思います。 630 | -------------------------------------------------------------------------------- /02/examples/1.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func main() { 12 | cmd := exec.Command("/bin/sh") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWUSER, 17 | } 18 | 19 | cmd.Stdin = os.Stdin 20 | cmd.Stdout = os.Stdout 21 | cmd.Stderr = os.Stderr 22 | 23 | if err := cmd.Run(); err != nil { 24 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 25 | os.Exit(1) 26 | } 27 | os.Exit(0) 28 | } 29 | -------------------------------------------------------------------------------- /02/examples/2.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func main() { 12 | cmd := exec.Command("/bin/sh") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWUSER, 17 | UidMappings: []syscall.SysProcIDMap{ 18 | { 19 | ContainerID: 0, 20 | HostID: os.Getuid(), 21 | Size: 1, 22 | }, 23 | }, 24 | GidMappings: []syscall.SysProcIDMap{ 25 | { 26 | ContainerID: 0, 27 | HostID: os.Getgid(), 28 | Size: 1, 29 | }, 30 | }, 31 | } 32 | 33 | cmd.Stdin = os.Stdin 34 | cmd.Stdout = os.Stdout 35 | cmd.Stderr = os.Stderr 36 | 37 | if err := cmd.Run(); err != nil { 38 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 39 | os.Exit(1) 40 | } 41 | os.Exit(0) 42 | } 43 | -------------------------------------------------------------------------------- /02/examples/3.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func Run() { 12 | cmd := exec.Command("/proc/self/exe", "init") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWUSER | 17 | syscall.CLONE_NEWUTS, 18 | UidMappings: []syscall.SysProcIDMap{ 19 | { 20 | ContainerID: 0, 21 | HostID: os.Getuid(), 22 | Size: 1, 23 | }, 24 | }, 25 | GidMappings: []syscall.SysProcIDMap{ 26 | { 27 | ContainerID: 0, 28 | HostID: os.Getgid(), 29 | Size: 1, 30 | }, 31 | }, 32 | } 33 | 34 | cmd.Stdin = os.Stdin 35 | cmd.Stdout = os.Stdout 36 | cmd.Stderr = os.Stderr 37 | 38 | if err := cmd.Run(); err != nil { 39 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 40 | os.Exit(1) 41 | } 42 | 43 | os.Exit(0) 44 | } 45 | 46 | func InitContainer() error { 47 | if err := syscall.Sethostname([]byte("container")); err != nil { 48 | return fmt.Errorf("Setting hostname failed: %w", err) 49 | } 50 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 51 | return fmt.Errorf("Exec failed: %w", err) 52 | } 53 | return nil 54 | } 55 | 56 | func Usage() { 57 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 58 | os.Exit(2) 59 | } 60 | 61 | func main() { 62 | if len(os.Args) <= 1 { 63 | Usage() 64 | } 65 | switch os.Args[1] { 66 | case "run": 67 | Run() 68 | case "init": 69 | if err := InitContainer(); err != nil { 70 | fmt.Fprintf(os.Stderr, "%+v\n", err) 71 | os.Exit(1) 72 | } 73 | os.Exit(0) 74 | default: 75 | Usage() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /02/examples/4.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func Run() { 12 | cmd := exec.Command("/proc/self/exe", "init") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWNS | 17 | syscall.CLONE_NEWPID | 18 | syscall.CLONE_NEWUSER | 19 | syscall.CLONE_NEWUTS, 20 | UidMappings: []syscall.SysProcIDMap{ 21 | { 22 | ContainerID: 0, 23 | HostID: os.Getuid(), 24 | Size: 1, 25 | }, 26 | }, 27 | GidMappings: []syscall.SysProcIDMap{ 28 | { 29 | ContainerID: 0, 30 | HostID: os.Getgid(), 31 | Size: 1, 32 | }, 33 | }, 34 | } 35 | 36 | cmd.Stdin = os.Stdin 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | 40 | if err := cmd.Run(); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | os.Exit(0) 46 | } 47 | 48 | func InitContainer() error { 49 | if err := syscall.Sethostname([]byte("container")); err != nil { 50 | return fmt.Errorf("Setting hostname failed: %w", err) 51 | } 52 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 53 | return fmt.Errorf("Exec failed: %w", err) 54 | } 55 | return nil 56 | } 57 | 58 | func Usage() { 59 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 60 | os.Exit(2) 61 | } 62 | 63 | func main() { 64 | if len(os.Args) <= 1 { 65 | Usage() 66 | } 67 | switch os.Args[1] { 68 | case "run": 69 | Run() 70 | case "init": 71 | if err := InitContainer(); err != nil { 72 | fmt.Fprintf(os.Stderr, "%+v\n", err) 73 | os.Exit(1) 74 | } 75 | os.Exit(0) 76 | default: 77 | Usage() 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /02/examples/5.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func Run() { 12 | cmd := exec.Command("/proc/self/exe", "init") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWNS | 17 | syscall.CLONE_NEWPID | 18 | syscall.CLONE_NEWUSER | 19 | syscall.CLONE_NEWUTS, 20 | UidMappings: []syscall.SysProcIDMap{ 21 | { 22 | ContainerID: 0, 23 | HostID: os.Getuid(), 24 | Size: 1, 25 | }, 26 | }, 27 | GidMappings: []syscall.SysProcIDMap{ 28 | { 29 | ContainerID: 0, 30 | HostID: os.Getgid(), 31 | Size: 1, 32 | }, 33 | }, 34 | } 35 | 36 | cmd.Stdin = os.Stdin 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | 40 | if err := cmd.Run(); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | os.Exit(0) 46 | } 47 | 48 | func InitContainer() error { 49 | if err := syscall.Sethostname([]byte("container")); err != nil { 50 | return fmt.Errorf("Setting hostname failed: %w", err) 51 | } 52 | if err := syscall.Mount("proc", "/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 53 | return fmt.Errorf("Proc mount failed: %w", err) 54 | } 55 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 56 | return fmt.Errorf("Exec failed: %w", err) 57 | } 58 | return nil 59 | } 60 | 61 | func Usage() { 62 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 63 | os.Exit(2) 64 | } 65 | 66 | func main() { 67 | if len(os.Args) <= 1 { 68 | Usage() 69 | } 70 | switch os.Args[1] { 71 | case "run": 72 | Run() 73 | case "init": 74 | if err := InitContainer(); err != nil { 75 | fmt.Fprintf(os.Stderr, "%+v\n", err) 76 | os.Exit(1) 77 | } 78 | os.Exit(0) 79 | default: 80 | Usage() 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /02/examples/6.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func Run() { 12 | cmd := exec.Command("/proc/self/exe", "init") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWNS | 17 | syscall.CLONE_NEWPID | 18 | syscall.CLONE_NEWUSER | 19 | syscall.CLONE_NEWUTS, 20 | UidMappings: []syscall.SysProcIDMap{ 21 | { 22 | ContainerID: 0, 23 | HostID: os.Getuid(), 24 | Size: 1, 25 | }, 26 | }, 27 | GidMappings: []syscall.SysProcIDMap{ 28 | { 29 | ContainerID: 0, 30 | HostID: os.Getgid(), 31 | Size: 1, 32 | }, 33 | }, 34 | } 35 | 36 | cmd.Stdin = os.Stdin 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | 40 | if err := cmd.Run(); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | os.Exit(0) 46 | } 47 | 48 | func InitContainer() error { 49 | if err := syscall.Sethostname([]byte("container")); err != nil { 50 | return fmt.Errorf("Setting hostname failed: %w", err) 51 | } 52 | if err := syscall.Mount("proc", "/root/chroot/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 53 | return fmt.Errorf("Proc mount failed: %w", err) 54 | } 55 | if err := syscall.Chroot("/root/chroot"); err != nil { 56 | return fmt.Errorf("Chroot failed: %w", err) 57 | } 58 | if err := os.Chdir("/"); err != nil { 59 | return fmt.Errorf("Chdir failed: %w", err) 60 | } 61 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 62 | return fmt.Errorf("Exec failed: %w", err) 63 | } 64 | return nil 65 | } 66 | 67 | func Usage() { 68 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 69 | os.Exit(2) 70 | } 71 | 72 | func main() { 73 | if len(os.Args) <= 1 { 74 | Usage() 75 | } 76 | switch os.Args[1] { 77 | case "run": 78 | Run() 79 | case "init": 80 | if err := InitContainer(); err != nil { 81 | fmt.Fprintf(os.Stderr, "%+v\n", err) 82 | os.Exit(1) 83 | } 84 | os.Exit(0) 85 | default: 86 | Usage() 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /02/examples/7.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "syscall" 9 | ) 10 | 11 | func Run() { 12 | cmd := exec.Command("/proc/self/exe", "init") 13 | cmd.SysProcAttr = &syscall.SysProcAttr{ 14 | Cloneflags: syscall.CLONE_NEWIPC | 15 | syscall.CLONE_NEWNET | 16 | syscall.CLONE_NEWNS | 17 | syscall.CLONE_NEWPID | 18 | syscall.CLONE_NEWUSER | 19 | syscall.CLONE_NEWUTS, 20 | UidMappings: []syscall.SysProcIDMap{ 21 | { 22 | ContainerID: 0, 23 | HostID: os.Getuid(), 24 | Size: 1, 25 | }, 26 | }, 27 | GidMappings: []syscall.SysProcIDMap{ 28 | { 29 | ContainerID: 0, 30 | HostID: os.Getgid(), 31 | Size: 1, 32 | }, 33 | }, 34 | } 35 | 36 | cmd.Stdin = os.Stdin 37 | cmd.Stdout = os.Stdout 38 | cmd.Stderr = os.Stderr 39 | 40 | if err := cmd.Run(); err != nil { 41 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 42 | os.Exit(1) 43 | } 44 | 45 | os.Exit(0) 46 | } 47 | 48 | func InitContainer() error { 49 | if err := syscall.Sethostname([]byte("container")); err != nil { 50 | return fmt.Errorf("Setting hostname failed: %w", err) 51 | } 52 | if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", uintptr(syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV), ""); err != nil { 53 | return fmt.Errorf("Proc mount failed: %w", err) 54 | } 55 | if err := os.Chdir("/root"); err != nil { 56 | return fmt.Errorf("Chdir /root failed: %w", err) 57 | } 58 | if err := syscall.Mount("rootfs", "/root/rootfs", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { 59 | return fmt.Errorf("Rootfs bind mount failed: %w", err) 60 | } 61 | if err := os.MkdirAll("/root/rootfs/oldrootfs", 0700); err != nil { 62 | return fmt.Errorf("Oldrootfs create failed: %w", err) 63 | } 64 | if err := syscall.PivotRoot("rootfs", "/root/rootfs/oldrootfs"); err != nil { 65 | return fmt.Errorf("PivotRoot failed: %w", err) 66 | } 67 | if err := syscall.Unmount("/oldrootfs", syscall.MNT_DETACH); err != nil { 68 | return fmt.Errorf("Oldrootfs umount failed: %w", err) 69 | } 70 | if err := os.RemoveAll("/oldrootfs"); err != nil { 71 | return fmt.Errorf("Remove oldrootfs failed: %w", err) 72 | } 73 | if err := os.Chdir("/"); err != nil { 74 | return fmt.Errorf("Chdir failed: %w", err) 75 | } 76 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 77 | return fmt.Errorf("Exec failed: %w", err) 78 | } 79 | return nil 80 | } 81 | 82 | func Usage() { 83 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 84 | os.Exit(2) 85 | } 86 | 87 | func main() { 88 | if len(os.Args) <= 1 { 89 | Usage() 90 | } 91 | switch os.Args[1] { 92 | case "run": 93 | Run() 94 | case "init": 95 | if err := InitContainer(); err != nil { 96 | fmt.Fprintf(os.Stderr, "%+v\n", err) 97 | os.Exit(1) 98 | } 99 | os.Exit(0) 100 | default: 101 | Usage() 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /02/examples/8.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | ) 11 | 12 | func Run() { 13 | cmd := exec.Command("/proc/self/exe", "init") 14 | cmd.SysProcAttr = &syscall.SysProcAttr{ 15 | Cloneflags: syscall.CLONE_NEWIPC | 16 | syscall.CLONE_NEWNET | 17 | syscall.CLONE_NEWNS | 18 | syscall.CLONE_NEWPID | 19 | syscall.CLONE_NEWUSER | 20 | syscall.CLONE_NEWUTS, 21 | UidMappings: []syscall.SysProcIDMap{ 22 | { 23 | ContainerID: 0, 24 | HostID: os.Getuid(), 25 | Size: 1, 26 | }, 27 | }, 28 | GidMappings: []syscall.SysProcIDMap{ 29 | { 30 | ContainerID: 0, 31 | HostID: os.Getgid(), 32 | Size: 1, 33 | }, 34 | }, 35 | } 36 | 37 | cmd.Stdin = os.Stdin 38 | cmd.Stdout = os.Stdout 39 | cmd.Stderr = os.Stderr 40 | 41 | if err := cmd.Run(); err != nil { 42 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 43 | os.Exit(1) 44 | } 45 | 46 | os.Exit(0) 47 | } 48 | 49 | func InitContainer() error { 50 | if err := syscall.Sethostname([]byte("container")); err != nil { 51 | return fmt.Errorf("Setting hostname failed: %w", err) 52 | } 53 | 54 | if err := os.MkdirAll("/sys/fs/cgroup/cpu/my-container", 0700); err != nil { 55 | return fmt.Errorf("Cgroups namespace my-container create failed: %w", err) 56 | } 57 | if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/tasks", []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil { 58 | return fmt.Errorf("Cgroups register tasks to my-container namespace failed: %w", err) 59 | } 60 | if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/cpu.cfs_quota_us", []byte("1000\n"), 0644); err != nil { 61 | return fmt.Errorf("Cgroups add limit cpu.cfs_quota_us to 1000 failed: %w", err) 62 | } 63 | 64 | if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV, ""); err != nil { 65 | return fmt.Errorf("Proc mount failed: %w", err) 66 | } 67 | if err := os.Chdir("/root"); err != nil { 68 | return fmt.Errorf("Chdir /root failed: %w", err) 69 | } 70 | if err := syscall.Mount("rootfs", "/root/rootfs", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { 71 | return fmt.Errorf("Rootfs bind mount failed: %w", err) 72 | } 73 | if err := os.MkdirAll("/root/rootfs/oldrootfs", 0700); err != nil { 74 | return fmt.Errorf("Oldrootfs create failed: %w", err) 75 | } 76 | if err := syscall.PivotRoot("rootfs", "/root/rootfs/oldrootfs"); err != nil { 77 | return fmt.Errorf("PivotRoot failed: %w", err) 78 | } 79 | if err := syscall.Unmount("/oldrootfs", syscall.MNT_DETACH); err != nil { 80 | return fmt.Errorf("Oldrootfs umount failed: %w", err) 81 | } 82 | if err := os.RemoveAll("/oldrootfs"); err != nil { 83 | return fmt.Errorf("Remove oldrootfs failed: %w", err) 84 | } 85 | if err := os.Chdir("/"); err != nil { 86 | return fmt.Errorf("Chdir failed: %w", err) 87 | } 88 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 89 | return fmt.Errorf("Exec failed: %w", err) 90 | } 91 | return nil 92 | } 93 | 94 | func Usage() { 95 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 96 | os.Exit(2) 97 | } 98 | 99 | func main() { 100 | if len(os.Args) <= 1 { 101 | Usage() 102 | } 103 | switch os.Args[1] { 104 | case "run": 105 | Run() 106 | case "init": 107 | if err := InitContainer(); err != nil { 108 | fmt.Fprintf(os.Stderr, "%+v\n", err) 109 | os.Exit(1) 110 | } 111 | os.Exit(0) 112 | default: 113 | Usage() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /02/examples/main.go: -------------------------------------------------------------------------------- 1 | // +build linux 2 | package main 3 | 4 | import ( 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "os/exec" 9 | "syscall" 10 | ) 11 | 12 | func Run() { 13 | cmd := exec.Command("/proc/self/exe", "init") 14 | cmd.SysProcAttr = &syscall.SysProcAttr{ 15 | Cloneflags: syscall.CLONE_NEWIPC | 16 | syscall.CLONE_NEWNET | 17 | syscall.CLONE_NEWNS | 18 | syscall.CLONE_NEWPID | 19 | syscall.CLONE_NEWUSER | 20 | syscall.CLONE_NEWUTS, 21 | UidMappings: []syscall.SysProcIDMap{ 22 | { 23 | ContainerID: 0, 24 | HostID: os.Getuid(), 25 | Size: 1, 26 | }, 27 | }, 28 | GidMappings: []syscall.SysProcIDMap{ 29 | { 30 | ContainerID: 0, 31 | HostID: os.Getgid(), 32 | Size: 1, 33 | }, 34 | }, 35 | } 36 | 37 | cmd.Stdin = os.Stdin 38 | cmd.Stdout = os.Stdout 39 | cmd.Stderr = os.Stderr 40 | 41 | if err := cmd.Run(); err != nil { 42 | fmt.Fprintf(os.Stderr, "Error: %+v\n", err) 43 | os.Exit(1) 44 | } 45 | 46 | os.Exit(0) 47 | } 48 | 49 | func InitContainer() error { 50 | if err := syscall.Sethostname([]byte("container")); err != nil { 51 | return fmt.Errorf("Setting hostname failed: %w", err) 52 | } 53 | 54 | if err := os.MkdirAll("/sys/fs/cgroup/cpu/my-container", 0700); err != nil { 55 | return fmt.Errorf("Cgroups namespace my-container create failed: %w", err) 56 | } 57 | if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/tasks", []byte(fmt.Sprintf("%d\n", os.Getpid())), 0644); err != nil { 58 | return fmt.Errorf("Cgroups register tasks to my-container namespace failed: %w", err) 59 | } 60 | if err := ioutil.WriteFile("/sys/fs/cgroup/cpu/my-container/cpu.cfs_quota_us", []byte("1000\n"), 0644); err != nil { 61 | return fmt.Errorf("Cgroups add limit cpu.cfs_quota_us to 1000 failed: %w", err) 62 | } 63 | 64 | if err := syscall.Mount("proc", "/root/rootfs/proc", "proc", syscall.MS_NOEXEC|syscall.MS_NOSUID|syscall.MS_NODEV, ""); err != nil { 65 | return fmt.Errorf("Proc mount failed: %w", err) 66 | } 67 | if err := os.Chdir("/root"); err != nil { 68 | return fmt.Errorf("Chdir /root failed: %w", err) 69 | } 70 | if err := syscall.Mount("rootfs", "/root/rootfs", "", syscall.MS_BIND|syscall.MS_REC, ""); err != nil { 71 | return fmt.Errorf("Rootfs bind mount failed: %w", err) 72 | } 73 | if err := os.MkdirAll("/root/rootfs/oldrootfs", 0700); err != nil { 74 | return fmt.Errorf("Oldrootfs create failed: %w", err) 75 | } 76 | if err := syscall.PivotRoot("rootfs", "/root/rootfs/oldrootfs"); err != nil { 77 | return fmt.Errorf("PivotRoot failed: %w", err) 78 | } 79 | if err := syscall.Unmount("/oldrootfs", syscall.MNT_DETACH); err != nil { 80 | return fmt.Errorf("Oldrootfs umount failed: %w", err) 81 | } 82 | if err := os.RemoveAll("/oldrootfs"); err != nil { 83 | return fmt.Errorf("Remove oldrootfs failed: %w", err) 84 | } 85 | if err := os.Chdir("/"); err != nil { 86 | return fmt.Errorf("Chdir failed: %w", err) 87 | } 88 | if err := syscall.Exec("/bin/sh", []string{"/bin/sh"}, os.Environ()); err != nil { 89 | return fmt.Errorf("Exec failed: %w", err) 90 | } 91 | return nil 92 | } 93 | 94 | func Usage() { 95 | fmt.Fprintf(os.Stderr, "Usage: %s run\n", os.Args[0]) 96 | os.Exit(2) 97 | } 98 | 99 | func main() { 100 | if len(os.Args) <= 1 { 101 | Usage() 102 | } 103 | switch os.Args[1] { 104 | case "run": 105 | Run() 106 | case "init": 107 | if err := InitContainer(); err != nil { 108 | fmt.Fprintf(os.Stderr, "%+v\n", err) 109 | os.Exit(1) 110 | } 111 | os.Exit(0) 112 | default: 113 | Usage() 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /02/examples/unchroot.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "syscall" 7 | ) 8 | 9 | func main() { 10 | if _, err := os.Stat(".42"); os.IsNotExist(err) { 11 | if err := os.Mkdir(".42", 0755); err != nil { 12 | fmt.Println("Mkdir failed") 13 | } 14 | } 15 | if err := syscall.Chroot(".42"); err != nil { 16 | fmt.Println("Chroot to .42 failed") 17 | } 18 | if err := syscall.Chroot("../../../../../../../../../../../../../../../.."); err != nil { 19 | fmt.Println("Jail break failed") 20 | } 21 | if err := syscall.Exec("/bin/sh", []string{""}, os.Environ()); err != nil { 22 | fmt.Println(err) 23 | fmt.Println("Exec failed") 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /03/README.md: -------------------------------------------------------------------------------- 1 | # Open Container Initiative Runtime Specification 入門 2 | 3 | ここまでで、コンテナの概要と、実際に Linux 上でプロセスを用いたコンテナがどのように実装されているかを簡単に確認しました。 4 | 5 | 実際には、最初に説明したように、コンテナランタイムの仕様は標準化されており、 6 | それに則って実装を行うことで、様々なプラットフォームで動かすことが可能になっています。 7 | 8 | ここでは、最初に説明した [opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec) 並びに、 9 | [opencontainers/image-spec](https://github.com/opencontainers/image-spec) の詳細について確認していきます。 10 | 11 | また、実際に手を動かして `runtime-spec` に最低限準拠したコンテナランタイムを作成し、 12 | Linux 上の Docker のランタイムとして使うことで、実際の動作を確認していきます。 13 | 14 | ## image-spec 15 | 16 | [opencontainers/image-spec](https://github.com/opencontainers/image-spec/blob/v1.0.1/spec.md) はコンテナイメージに関する標準化の文章です。 17 | 実際の仕様自体は [`spec.md`](https://github.com/opencontainers/image-spec/blob/v1.0.1/spec.md) に書かれています。 18 | 19 | コンテナイメージは、例えば開発環境で動作確認が済んだものを本番環境にデプロイするように、様々な場所に転送されます。 20 | そのため、圧縮を行ったり、`layer` と呼ばれるレイヤ化によって、イメージの更新があった箇所だけダウンロードを行えるようにしたりと、高速に転送を行える工夫などがなされています。 21 | 22 | 今回は image-spec に関して詳細には紹介しませんが、興味がある方はぜひ読んでみてください。 23 | 24 | ### Docker image から image-spec 準拠のコンテナイメージへの変換 25 | 26 | image-spec で標準化が進んでいるものの、現在広く使われている Docker のイメージは、実は image-spec には完全に準拠していません。 27 | Docker イメージは、`docker pull` してきた後に `docker save` などを実行することで tar 形式で書き出すことができます。少し中身を見てみましょう。 28 | 29 | ```sh 30 | docker pull alpine:3.9 31 | docker save alpine:3.9 --output alpine-3.9.tar 32 | 33 | mkdir /root/alpine 34 | tar -xf alpine-3.9.tar -C /root/alpine 35 | ``` 36 | 37 | ```sh 38 | ls /root/alpine 39 | 5cb3aa00f89934411ffba5c063a9bc98ace875d8f92e77d0029543d9f2ef4ad0.json manifest.json 40 | b40d48399b5890827b4252edbd2638b981772678bb1cc096436129f631722047 repositories 41 | ``` 42 | 43 | このように、`manifest.json` や `repositories` などが置かれており、 44 | image-spec における [`image-layout.md`](https://github.com/opencontainers/image-spec/blob/v1.0.1/image-layout.md) に準拠していないことがわかります。 45 | (`image-layout` では `index.json`, `oci-layout` という 2 ファイルと `blobs` というディレクトリが存在する必要があると書かれている) 46 | 47 | さて、これを OCI 標準イメージに変換するために、[containers/skopeo](https://github.com/containers/skopeo) というツールを利用します。 48 | 皆さんにお配りした仮想マシンには既にインストール済みなので、次のように実行してみてください。 49 | 50 | ```sh 51 | cd /root 52 | skopeo copy docker://alpine:3.9 oci:alpine-oci:3.9 53 | ``` 54 | 55 | この状態で、`alpine-oci` ディレクトリを確認してみると、`image-layout` に準拠しているディレクトリ構造になっており、各ファイルも imege-spec に準拠しているものになります。 56 | 57 | ```sh 58 | ls alpine-oci 59 | blobs index.json oci-layout 60 | ``` 61 | 62 | このようにして、広く使われている Docker イメージを、image-spec に準拠したコンテナイメージに変換することができるようになっています。 63 | 64 | ## runtime-spec 65 | 66 | [opencontainers/runtime-spec](https://github.com/opencontainers/runtime-spec) は、コンテナランタイムに関する標準化の文章です。 67 | 実際の仕様自体は [`spec.md`](https://github.com/opencontainers/runtime-spec/blob/v1.0.1/spec.md) に書かれています。 68 | 69 | `linux`, `solaris`, `windows` など、ランタイムが動作する環境ごとに仕様が決定されています。ここでは `linux` の仕様について見ていきます。 70 | 71 | ## Filesystem bundle 72 | 73 | コンテナを実際に実行する際に、必要な全てのデータとメタデータを含むようなファイルシステムの形式を、Filesystem bundle と呼んでいます。 74 | 構成は至ってシンプルで、ファイルシステムの種類にはよらず、次の 2 つで構成されているものです。 75 | 76 | - `config.json` 77 | - bundle directory の root に `config.json` という名前で置かれなければならない 78 | - コンテナのルートファイルシステム 79 | - 基本的には `config.json` に書かれている `root.path` によって参照されるディレクトリ 80 | 81 | 前述の [image-spec に書かれている通り](https://github.com/opencontainers/image-spec/tree/v1.0.1#running-an-oci-image)、 82 | OCI イメージを仕様に基づいて解凍したものがこの Filesystem bundle になっています。 83 | 84 | 試しに、先ほど用意した image-spec に準拠したイメージを、Filesystem bundle に展開してみましょう。 85 | 展開には、[`oci-image-tool`](https://github.com/opencontainers/image-tools) というツールを利用できます。これも皆さんの仮想マシンには既にインストール済みになっています。 86 | 87 | ```sh 88 | cd /root 89 | mkdir alpine-bundle 90 | oci-image-tool create --ref name=3.9 alpine-oci alpine-bundle 91 | ``` 92 | 93 | 展開された `alpine-bundle` ディレクトリを眺めてみると、書かれている通りのファイルシステムツリーになっていることがわかります。 94 | 95 | ```json 96 | ls alpine-bundle/ 97 | config.json rootfs 98 | 99 | cat alpine-bundle/config.json | jq . 100 | { 101 | "ociVersion": "1.0.0", 102 | "process": { 103 | "terminal": true, 104 | "user": { 105 | "uid": 0, 106 | "gid": 0 107 | }, 108 | "args": [ 109 | "/bin/sh" 110 | ], 111 | "env": [ 112 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 113 | ], 114 | "cwd": "/" 115 | }, 116 | "root": { 117 | "path": "rootfs" 118 | }, 119 | "linux": {} 120 | } 121 | ``` 122 | 123 | ## Runtime and Lifecycle 124 | 125 | 実際に、コンテナランタイムの一連の動作を記述しているのが [`runtime.md`](https://github.com/opencontainers/runtime-spec/blob/v1.0.1/runtime.md) です。 126 | コンテナランタイムは、コマンドラインツールになっており、以下のサブコマンドが利用可能になっている必要があります。 127 | 128 | - `state ` 129 | - `create ` 130 | - `start ` 131 | - `kill ` 132 | - `delete ` 133 | 134 | コンテナランタイムのライフサイクルでは、次のように動作が行われていきます。 135 | 136 | - `create` サブコマンドが実行される 137 | - Filesystem bundle への参照と任意の ID が指定される 138 | - 指定された ID はコンテナの固有 ID となる 139 | - `create` で作られるコンテナの実行環境は Filesystem bundle にある `config.json` の設定に従う 140 | - `config.json` に従えない場合はエラーを発生させる 141 | - `config.json` で指定したリソースが作成中の場合指定されているプログラムを実行してはならない 142 | - この手順のあとに Filesystem bundle に存在する `config.json` を変更してもコンテナには影響を及ぼさない 143 | - `start` サブコマンドが実行される 144 | - 実行対象となるコンテナの固有 ID が指定される 145 | - `config.json` に指定されている `.hooks.prestart` に書かれているコマンドが実行される 146 | - `.hooks.prestart` が失敗した場合にはコンテナを停止する必要がある 147 | - `config.json` に指定されている `.process` に従ってプロセスが実行される 148 | - `config.json` に指定されている `.hooks.poststart` に書かれているコマンドが実行される 149 | - `.hooks.poststart` が失敗した場合にはログに警告を出すが処理は継続する 150 | - コンテナプロセスが終了する 151 | - これは処理の完了や、エラーや、`kill` などによって発生する 152 | - `delete` サブコマンドが実行される 153 | - コンテナの固有 ID が渡される 154 | - `create` で作成した全てのリソースを正しく元に戻す 155 | - `config.json` に指定されている `.hooks.poststop` に書かれているコマンドが実行される 156 | - `.hooks.poststart` が失敗した場合にはログに警告を出すが処理は継続する 157 | 158 | ## runc コマンドを使ったコンテナの作成 159 | 160 | それぞれ、実際に `runc` コマンドを実行して動作を確認してみましょう。 161 | Filesystem bundle には先ほど作成した `alpine-bundle` を利用します。 162 | 163 | ```sh 164 | cd /root/alpine-bundle 165 | ``` 166 | 167 | 動作を簡潔にするために、`config.json` を次のものに置き換えます。 168 | これは、`echo alpine` が実行されるような設定ファイルになっています。  169 | 170 | ```json 171 | { 172 | "ociVersion": "1.0.0", 173 | "process": { 174 | "terminal": false, 175 | "user": { 176 | "uid": 0, 177 | "gid": 0 178 | }, 179 | "args": [ 180 | "echo", 181 | "alpine" 182 | ], 183 | "env": [ 184 | "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" 185 | ], 186 | "cwd": "/" 187 | }, 188 | "root": { 189 | "path": "rootfs" 190 | }, 191 | "linux": {}, 192 | "mounts": [ 193 | { 194 | "destination": "/proc", 195 | "type": "proc", 196 | "source": "proc" 197 | } 198 | ] 199 | } 200 | ``` 201 | 202 | この状態で、`runc create ` を実行します。 203 | 204 | ```sh 205 | runc create alpine-test 206 | ``` 207 | 208 | `runc list`, `runc state ` でコンテナの状態を確認します。 209 | 210 | ```sh 211 | runc list 212 | ID PID STATUS BUNDLE CREATED OWNER 213 | alpine-test 29017 created /home/ubuntu/alpine-bundle 2019-03-18T22:18:31.693146066Z root 214 | ``` 215 | 216 | ```sh 217 | runc state alpine-test 218 | { 219 | "ociVersion": "1.0.1-dev", 220 | "id": "alpine-test", 221 | "pid": 29017, 222 | "status": "created", 223 | "bundle": "/home/ubuntu/alpine-bundle", 224 | "rootfs": "/home/ubuntu/alpine-bundle/rootfs", 225 | "created": "2019-03-18T22:18:31.693146066Z", 226 | "owner": "" 227 | } 228 | ``` 229 | 230 | (この状態で `pid` のプロセスは一体どんな状態になっているでしょう?余裕があれば調べてみてください) 231 | 232 | `runc start` で実際にコンテナを実行してみます。 233 | 234 | ```sh 235 | runc start alpine-test 236 | alpine 237 | ``` 238 | 239 | `config.json` に指定されている `args` 通り、`echo alpine` が実行され、`alpine` が出力されました。 240 | 241 | また、`runc list`, `runc state ` で状態を確認してみましょう。 242 | 243 | ```sh 244 | runc list 245 | ID PID STATUS BUNDLE CREATED OWNER 246 | alpine-test 0 stopped /home/ubuntu/alpine-bundle 2019-03-18T22:25:05.041029433Z root 247 | ``` 248 | 249 | ``sh 250 | runc state alpine-test 251 | { 252 | "ociVersion": "1.0.1-dev", 253 | "id": "alpine-test", 254 | "pid": 0, 255 | "status": "stopped", 256 | "bundle": "/home/ubuntu/alpine-bundle", 257 | "rootfs": "/home/ubuntu/alpine-bundle/rootfs", 258 | "created": "2019-03-18T22:25:05.041029433Z", 259 | "owner": "" 260 | }` 261 | `` 262 | 263 | このように、`STATUS` が `stopped` になっていることがわかります。 264 | この状態のコンテナは Lifecycle に従って正しく `delete` を行う必要があるので、`runc delete ` を発行します。 265 | 266 | ```sh 267 | runc delete alpine-test 268 | ``` 269 | 270 | `runc list` でコンテナがなくなったことを確認します。 271 | 272 | ```sh 273 | runc list 274 | ID PID STATUS BUNDLE CREATED OWNER 275 | ``` 276 | 277 | 細かい仕様や動作環境固有の設定などはたくさんありますが、`runtime-spec` で必須の仕様はここまで説明した Filesystem bundle, Runtime and Lifecycle がほとんど全てになっています。 278 | -------------------------------------------------------------------------------- /04/README.md: -------------------------------------------------------------------------------- 1 | # 自作コンテナランタイムの拡張 2 | 3 | 最後に、ここまでの知識を使って自作のコンテナランタイムを作ってみましょう。 4 | 5 | 例えば、サンプルコードを別の言語で書き換えてコンテナランタイムを作ってみたり、 6 | 今日説明した以上の OCI の仕様に準拠してみたり、よりたくさんの Capabilites や、Cgroups の機能を使ってみたり、 7 | Linux のプロセス以外の実装方法でコンテナランタイムを実装したり、Docker から利用可能なように実装したりと、自由に拡張を行ってみてください。 8 | 9 | ## 課題例 10 | 11 | 課題が思いつかない人のために、いくつか案を書いておきます。ここにあるものでもここにないものでも自由に実装を行ってみてください。 12 | 13 | また、実装しようとしているもののアイデアが不安という方や、何を実装していいか分からない方、 14 | 実装したいものはあるがどのように実装すればよいかわからない方などは、相談にのるので積極的に相談してみてください。 15 | 16 | - Linux プロセス型コンテナ改良系 17 | - Go ではない別の言語でシステムコールを呼び Linux のプロセス型コンテナを実装してみる 18 | - Linux Capabilites をいくつか設定できるように改良してみる 19 | - Cgroups の機能をより使えるように改良してみる 20 | - `pivot_root` 時に `/proc/self/fd{/,/0,/1,/2}` を正しく `/dev/{fd,stdin,stdout,stderr}` などにマッピングしてみる 21 | - `pivot_root` 時に rootfs ではない任意のホストディレクトリを扱えるようにしてみる 22 | - Dokcer の `--mount` オプションのようなもの 23 | - `pivot_root` 時に cgroupsfs をマウントしてみる 24 | - alpine linux の Filesystem bundles の `rootfs` に自作コンテナで `pivot_root` してみる 25 | - `apk` で何かをインストールしてみたりして挙動を調べてまとめてみる 26 | - Mount 時に overlayfs などを利用するようにしてみる 27 | - コンテナで実行しているプロセス内でファイルシステムを破壊しても安全なようにしてみる 28 | - seccomp(2) を使ってプロセスが呼べるシステムコールを制限する 29 | - OCI 準拠系 30 | - `config.json` を読み取って動作する自作コンテナを作ってみる 31 | - OCI ランタイムのサブコマンドを実装してみる 32 | - `state ` 33 | - `create ` 34 | - `start ` 35 | - `kill ` 36 | - `delete ` 37 | - Docker の `runtimes` に自作コンテナのバイナリを追加してみる 38 | - 自作コンテナのバイナリで `argv` を出力してみて調べてみる 39 | - `docker` がどのように `runc` を呼び出しているか調べてみる 40 | - `docker` が `runc create ` を呼んだ後にどのようなファイルを期待するか調べてみる 41 | - `image-spec` について読んでまとめてみる 42 | - CRI (Container Runtime Interface) 調査系 43 | - (今回は紹介しなかった)Container Runtime Interface について調べてまとめてみる 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Ryota Yoshikawa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # コンテナインターンシップ 2 | 3 | Linux コンテナに関する理解を深めることを目的としたインターンシップです。 4 | 5 | Linux コンテナを自作してみることで、Linux コンテナの主要な構成要素である、 6 | Linux Namespaces や、Filesystem の取り扱い、Cgroups によるリソース制御などを学びます。 7 | 8 | また、自作コンテナを OCI に従って拡張していくことにより、Docker などがどのようにしてコンテナを起動しているかを学びます。 9 | 10 | ## タイムテーブル 11 | 12 | - 10:00 - 10:15 アイスブレイク 13 | - 10:15 - 10:45 セットアップ 14 | - 10:45 - 11:15 コンテナ概要 15 | - 11:15 - 13:00 自作コンテナ入門 16 | - 13:00 - 14:00 昼食 17 | - 14:00 - 15:00 OCI 入門 18 | - 15:00 - 17:30 自作コンテナ拡張 19 | - 17:30 - 18:00 成果発表 20 | 21 | ## Author 22 | 23 | Ryota Yoshikawa ( @rrreeeyyy ) ( / ) 24 | 25 | ## Contributors 26 | 27 | - @itkq 28 | - @mozamimy 29 | - @hfm 30 | 31 | ## License 32 | 33 | [MIT](https://opensource.org/licenses/MIT) 34 | --------------------------------------------------------------------------------