Dockerが隔離環境を実現している仕組みについてさくっと解説

# はじめに
こんばんは。本記事は[Progaku Advent Calendar 2022](https://qiita.com/advent-calendar/2022/progaku) 23日目担当のきよすけと申します。
早速本題ですが、みなさん、Docker使ってますでしょうか？昨今、AWSだとECS/Fargateを使ったコンテナデプロイがデファクトスタンダードになってきたので開発環境でもDockerを使っている方は多いかと思います。本記事はDockerがどうやって隔離環境を実現しているのか簡単にまとめてみました。

# 環境
- Ubuntu 20.04.1
- docker-compose v2.7.0
- Docker 20.10.21
- Docker Desktop 4.11.0

# 結論
Linuxカーネルのnamespaceをつかって隔離環境を実現している。

# namespaceとは
[コチラ](https://www.mtioutput.com/entry/docker-namespace-tutorial)の記事の内容がとてもわかりやすかったです。
Docker関連を体系的に知りたいかたは、こちらをひと通り読んでみるのも良いと思います。

>名前空間とは、要素名が重複しても識別できるように名前の集合へ与えられる識別名を指します。
>
>要は、Dockerのようなコンテナ技術においては一つのカーネルの上で隔離環境を用意する必要があるのでプロセスIDはもちろんのこと、ファイルシステムやユーザID諸々の名前空間を分離することが求められるわけです。

Linuxのカーネルの機能にもnamespaceというものがあります。
Dockerなどのコンテナ技術はVirtualBoxなどの仮想マシンと違って、全てのコンテナがひとつのホストOSのカーネルを共有します。
なので何も考えずに同時に2つのアプリ(プロセス)、例えばLaravelとMySQLを同じOS上で動かしたら同じパスやファイル名を取れないですし、同じUIDを取ることも出来ません。これで開発するというのはあまり現実的ではないですよね。

そのため、Dockerのコンテナ技術を実現するためにはプロセスID、ファイルシステム、UIDやGID、ネットワークなどもろもろの名前空間を分離させる必要があります。
それに用いられるのが上記で引用したLinuxカーネルのnamespaceという機能です。(これは2006年に実装されたもので古くからある技術です。)

このnamespace機能を使うことでプロセス空間やファイルシステムを一つのOSの中で分離できるのため、namespaceで区切られた部分は互いに影響を与えることなく全く異なるOSのように振る舞うことが可能です。

そしてこの分離したnamespaceを持たせることによってほかのプロセスと実行環境が分かれているプロセスこそ、私たちが普段使コンテナと呼んでいるものの正体です。

## namespaceを試す
では実際にnamespaceがどういったものなのか新規namespaceをつくって簡単に試してみましょう。

namespaceには、

- pid namespace(pid ns): 独立したpid(プロセスID)名前空間を見せる
- use namespace(user ns): 独立したuid、gidを見せる
- mount namespace(mount ns): 独立したファイルシステムマウント状況を見せる

のようなものがありますが、今回はpid namespaceについて試してみたいと思います。


### pidが属するroot pid nsを確認する
まず、適当にターミナルを開き今起動しているシェルのpidを確認してみます。

```
$ echo $$
340808
```

現状、このプロセスはpid=`340808`であることがわかります。

次に、このプロセスが属するpid nsを確認してみましょう。
下記ディレクトリを調べることでpid nsを確認できます。

```bash
$ ls -l /proc/$$/ns/pid
lrwxrwxrwx 1 kiyosuke kiyosuke 0 12月 21 21:27 /proc/340808/ns/pid -> 'pid:[4026531836]'
```

明示的に指定しないかぎりすべてのプロセスのpid nsはroot pid nsに属します。
よって、このプロセスはpidが`4026531836`のroot pid nsに所属していることがわかります。

### 新規namespaceを作成
では、今度は新規pid nsをつくりその上でコマンドを実行してみましょう。
これには`unshare`というコマンドにオプションをつけて下記のように実行します。

```bash
$ sudo unshare --fork --pid --mount-proc bash
```

新規namespaceが作成が起動し下記のような画面になります。
この感じはDockerのコンテナへ入ったときの似たような感じですね!
ここで、pidを確認します。

```bash
root@kiyosuke-ThinkPad-T15-Gen-1:/home/kiyosuke# echo $$
1
```

pidは`1`であることがわかります。
ここで同様に所属するpid nsを確認します。

```bash
root@kiyosuke-ThinkPad-T15-Gen-1:/home/kiyosuke# ls -l /proc/$$/ns/pid
lrwxrwxrwx 1 root root 0 12月 21 21:32 /proc/1/ns/pid -> 'pid:[4026533299]'
```

pidは`4026533299`であることがわかります。
最初に確認したpid ns(`4026531836`)とは違うpid nsに所属していることがわかりました。

psコマンドでプロセス一覧を確認してみると、

```bash
root@kiyosuke-ThinkPad-T15-Gen-1:/home/kiyosuke# ps ax
    PID TTY      STAT   TIME COMMAND
      1 pts/9    S      0:00 bash
      9 pts/9    R+     0:00 ps ax
```

新規pid ns上ではbashとpsコマンドのプロセスしか確認できず、ホストOS上のプロセスを見ることはできません。
なぜかと言うと、新しくつくられたpid nsはroot pid nsとは異なる独自のns(上記で確認したpid=`4026533299`のns)に属しているからです。

これはDockerでも同じですね。DockerからホストOS上のプロセスは確認できないです。
このnamespaceを機能つかってDockerは隔離環境を実現しているというわけです。

ではこの起動したbashプロセスをホストOS上から見るとどうなるでしょう？
新規ターミナルを開きpstreeコマンドで確認してみます。

```bash
$ pstree -p | grep unshare
           |               |                    |-zsh(340808)---sudo(341460)---unshare(341498)---bash(341499)
```

unshareの子プロセスとしてbash(id=`341499`)があることがわかります。
ホストOS上でこのbashプロセスが所属するpid nsを確認すると

```bash
$ sudo ls -l /proc/341499/ns/pid
lrwxrwxrwx 1 root root 0 12月 21 21:34 /proc/341499/ns/pid -> 'pid:[4026533299]'
```

たしかに、先程つくった新規pid nsであるこのbashプロセスはpid=`4026533299`のpid nsに属しているということがわかります。
また、root pid nsから見たpid(id=`341499`)とunshareコマンドで新規作成したpid ns(id=`1`)は違うことがわかります。

以上を図にすると下記のようなイメージです。
親pid(rood pis ns)のプロセスからはbashプロセスが見えますが、子プロセスからは親pidのプロセスは見えません。

![linux_namespace.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/632678/3691ef16-45ee-a2aa-129e-69810f4fbd4e.jpeg)


# まとめ
- DockerはLinuxカーネルのnamespaceを利用してコンテナを実現している。
- コンテナを実現するためにはファイルシステムなどのnamespaceを分離する必要があるが、Dockerコマンドでこの辺のめんどくさいことをラップしてくれている。
- すべてのプロセスはroot pid nsに属する。
- 新しくnamespaceをつくると、このnamespace上で実行されるプロセスはroot pid nsとは異なる独自のpid nsに属する。
- 親pid ns(今回の例ではroot pid ns)から子プロセスは参照できるが、子プロセスから親プロセスは確認できない。
- 親プロセスから見るpidと子プロセスから見えるpidは違う。


# 参考
- [Linuxのしくみ　―実験と図解で学ぶOS、仮想マシン、コンテナの基礎知識【増補改訂版】](https://www.amazon.co.jp/%EF%BC%BB%E8%A9%A6%E3%81%97%E3%81%A6%E7%90%86%E8%A7%A3%EF%BC%BDLinux%E3%81%AE%E3%81%97%E3%81%8F%E3%81%BF-%E2%80%95%E5%AE%9F%E9%A8%93%E3%81%A8%E5%9B%B3%E8%A7%A3%E3%81%A7%E5%AD%A6%E3%81%B6OS%E3%80%81%E4%BB%AE%E6%83%B3%E3%83%9E%E3%82%B7%E3%83%B3%E3%80%81%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E3%81%AE%E5%9F%BA%E7%A4%8E%E7%9F%A5%E8%AD%98%E3%80%90%E5%A2%97%E8%A3%9C%E6%94%B9%E8%A8%82%E7%89%88%E3%80%91-%E6%AD%A6%E5%86%85-%E8%A6%9A-ebook/dp/B0BG8J5QJ1)
- [【連載】世界一わかりみが深いコンテナ & Docker入門](https://tech-lab.sios.jp/archives/18811)





以上、[Progaku Advent Calendar 2022](https://qiita.com/advent-calendar/2022/progaku)23日目の記事でした。
明日の担当は、@inakuuun です!よろしくお願いします。