コンテナの基盤技術 Namespace 入門

コンテナは Namespace と cgroups という Linux の仕組みで構成されます[^1]。
[^1]:https://github.com/docker/docker.github.io/blob/v17.06-release/engine/docker-overview.md#the-underlying-technology

コンテナ毎にプロセスやファイルシステムを分離したり、
CPU やメモリなどのリソースを分け合えたりできるのは、これらの技術のおかげです。

この記事では、Namespace を取り上げ、概要説明を交えつつ、実際の動きにも触れてみます。

# 概要 
コンテナを複数立てたとき、プロセスはコンテナ間で分離されます。
コンテナ A で動くプロセスは、コンテナ B では見ることはできません。
同様にファイルシステムにおいても、コンテナ A からコンテナ B のファイルを見ることはできません。

これを実現しているのが Namespace という機能です。
Docker がコンテナを作成すると、下記のような Namespace を作成します。

* IPC Namespace
* Mount Namespace
* Network Namespace
* PID Namespace
* User Namespace
* UTS Namespace

以下では、ubuntu のドキュメント[^2]を参考に Namespace を掘り下げてみます。
[^2]:http://manpages.ubuntu.com/manpages/jammy/ja/man7/namespaces.7.html

### Namespace とは

>名前空間は、 グローバルシステムリソースを抽象化層で覆うことで、 名前空間内のプロセスに対し
       て、    自分たちが専用の分離されたグローバルリソースを持っているかのように見せる仕組みであ
       る。  グローバルリソースへの変更は、 名前空間のメンバーである他のプロセスには見えるが、 そ
       れ以外のプロセスには見えない。 名前空間の一つの利用方法はコンテナーの実装である。

「グローバルシステムリソース」がホストマシンが持つリソースのことになります。
Namespace という”部屋”を分けることによって、互いに独立したリソースを持つことができます。

これはシェアハウスのイメージに近いかもしれません。
互いの部屋の壁が厚いので、まるで一軒家と勘違いしてしまうような感じです。

### Namespace の種類

|  Namespace  |  分離対象  |
| ---- | ---- |
|  IPC  |  System V IPC, POSIX メッセージキュー  |
|  Network  |  ネットワークデバイス、スタック、ポートなど  |
|  Mount  |  マウントポイント  |
|  PID  |  プロセス ID  |
|  User  |  ユーザー ID とグループ ID  |
|  UTS  |  ホスト名と NIS ドメイン名  |

中でも PID は直感的に理解しやすい Namespace かと思います。
コンテナ上のプロセスは基本的に PID1 が振られますが、コンテナ A でも コンテナ B でも PID1 を利用できます。
Namespace が別であるため、通常一意であるべき PID が重複していても問題がないわけです。

### Namespace を操作する API

|  システムコール  |  内容 |
| ---- | ---- |
|  clone(2)  |  プロセスを生成し呼び出し元プロセスと同じ Namespace に所属させる。フラグを使って新しい Namespace を作成することもできる。  |
|  setns(2)  |  呼び出したプロセスを指定した既存 Namespace に参加させることができる。 |
|  unshare(2)  | 呼び出したプロセスを新しい Namespace に移動する。  |

### Namespace の確認方法
`/proc/[pid]/ns/` ディレクトリを見に行くことで、
現在のプロセスが所属している Namespace を確認できます。

```
$ ls -l /proc/$$/ns
total 0
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 net -> net:[4026531956]
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 pid -> pid:[4026531836]
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 user -> user:[4026531837]
lrwxrwxrwx. 1 mtk mtk 0 Jan 14 01:20 uts -> uts:[4026531838]
```

# 実際に触ってみる
試しに PID Namespace を分離してみます。

### 環境準備
Linux 環境を用意します。
今回は ubuntu の公式イメージを利用します。
```
$ docker pull ubuntu
$ docker run --privileged --name ubuntu -it ubuntu /bin/bash # 特権モードで起動
```

### 現在のプロセスの状態を確認してみる
bash が起動したら、現在のプロセスが所属している Namespace を確認します。
`/proc/self/ns` 配下のファイルを参照して確認できます。
```
$ cd /proc/self/ns
$ ls -la
total 0
dr-x--x--x 2 root root 0 May  1 06:04 .
dr-xr-xr-x 9 root root 0 May  1 05:46 ..
lrwxrwxrwx 1 root root 0 May  1 06:04 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 May  1 06:04 ipc -> 'ipc:[4026532376]'
lrwxrwxrwx 1 root root 0 May  1 06:04 mnt -> 'mnt:[4026532374]'
lrwxrwxrwx 1 root root 0 May  1 06:04 net -> 'net:[4026532379]'
lrwxrwxrwx 1 root root 0 May  1 06:04 pid -> 'pid:[4026532377]'
lrwxrwxrwx 1 root root 0 May  1 06:04 pid_for_children -> 'pid:[4026532377]'
lrwxrwxrwx 1 root root 0 May  1 06:04 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 May  1 06:04 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 May  1 06:04 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 May  1 06:04 uts -> 'uts:[4026532375]'
```
pid に注目すると `4026532377` という Namespace に所属していると分かります。

続いて、現在のプロセスを確認します。
```
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 05:46 pts/0    00:00:00 /bin/bash
root        15     1  0 06:05 pts/0    00:00:00 ps -ef
```
PID1 で /bin/bash が動いています。

### PID Namespace を分離してみる
次に、PID Namespace を分離してみます[^options]。
分離には unshare[^unshare] という新しい Namespace を作るためのコマンドを使用します[^privileged]。

[^unshare]:http://manpages.ubuntu.com/manpages/jammy/ja/man2/unshare.2.html
[^options]: 付属オプションは[こちら](https://blog.amedama.jp/entry/linux-pid-namespace_1)を参考にしました。
[^privileged]:docker run に `--privileged` を付けて特権モードで起動したのはこのコマンドを打つためです。

```
$ unshare --pid --fork --mount-proc sh
```

先程と同様に Namespace を確認します。
```
$ ls -la /proc/self/ns
total 0
dr-x--x--x 2 root root 0 May  1 06:04 .
dr-xr-xr-x 9 root root 0 May  1 06:04 ..
lrwxrwxrwx 1 root root 0 May  1 06:04 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx 1 root root 0 May  1 06:04 ipc -> 'ipc:[4026532376]'
lrwxrwxrwx 1 root root 0 May  1 06:04 mnt -> 'mnt:[4026532455]'
lrwxrwxrwx 1 root root 0 May  1 06:04 net -> 'net:[4026532379]'
lrwxrwxrwx 1 root root 0 May  1 06:04 pid -> 'pid:[4026532456]'
lrwxrwxrwx 1 root root 0 May  1 06:04 pid_for_children -> 'pid:[4026532456]'
lrwxrwxrwx 1 root root 0 May  1 06:04 time -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 May  1 06:04 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx 1 root root 0 May  1 06:04 user -> 'user:[4026531837]'
lrwxrwxrwx 1 root root 0 May  1 06:04 uts -> 'uts:[4026532375]'
```
すると、pid は `4026532456` となっており、さっきと異なっていることが分かります。

プロセスも見てみましょう。
```
$ ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root         1     0  0 06:04 pts/0    00:00:00 sh
root         3     1  0 06:05 pts/0    00:00:00 ps -ef
```

すると、こちらでは PID1 で sh が動いており、元々あった bash が見えなくなっています。

以上から、PID Namespace を分離することで
* 別 Namespace のプロセスが見えないこと
* Namespace 間で PID を重複して利用できること

の2点を確認できました。