Dockeコンテナの仕組みを作って理解する

## はじめに
今ではアプリケーションの実行環境として当たり前となったDockerですが、コンテナの実体は何なのか、内部の仕組みを理解したいと思ったことはありませんか？dockerコマンドさせ覚えていれば、いい感じのDockerイメージを引っ張ってきて、すぐにコンテナを起動できて、本来のやりたいことに注力できます。その便利さ故、コンテナとは何者なのかということを普段は意識しないと思います。


ソースコードだけを書くプログラマーであれば、Dockerコンテナがどうやって動くのかを知らなくても困らないと思います。しかし私のようなインフラエンジニアにとっては、コンテナ上で動くアプリケーションを管理するために、コンテナ間通信やリソース管理といったコンテナの内部にも関心を持つ必要があります。

本記事では、Dockerコンテナの中身を理解するために、実際にコンテナを作る過程を踏みながらコンテナの仕組みを紐解いていきたいと思います。

## コンテナとは
コンテナは一言で表現すると、ホストから独立しているプロセスです。各コンテナを独立させることでホストOS上で様々なアプリケーションを稼働させることができて、これが最大のメリットとなっています。

このホストから独立しているというのがすべてと言っても良いくらいです。もう少し紐解くと、コンテナは3つのLinuxカーネルの標準機能を使って「独立」を実現しているのです。

1. ルートディレクトリから分離している
1. プロセスが分離している
1. ハードウェアリソースが制限されている

つまり、コンテナは上記3つの条件を満たしているプロセスということになります。

### 1. ルートディレクトリから分離している
/var/container/配下に testA コンテナと testB コンテナを作る場合を例に考えます。コンテナは独立したプロセスですので、各コンテナからホストOSのファイルシステムが見えるのは好ましくありません。もちろん、testAからtestBにもアクセスすることもできません。このようにルートディレクトリを分離することで、ファイルシステムレベルでプロセスが分離しています。

![isolate_rootdirectory.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506008/cd61a743-358d-c706-3bb5-ae443e668f56.jpeg)

### 2. プロセスが分離している
ホストOSから見たとき（青色）は、各コンテナのプロセス/bin/initは通常通りPID1000 や2000 が割り当てられます。一方、コンテナ内から見たとき（オレンジ色）の/bin/init は親プロセスPID1となり、ホストOSの親プロセスから分離されます。PID1は最上位のプロセスですので、コンテナ内では自分以外のプロセスは見えなくなり、プロセス空間が独立しています。

![isolate_process.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506008/e2525332-8e0d-f24b-44e1-d37c9f069086.jpeg)

### 3. ハードウェアリソースが制限されている
コンテナはホストOSのハードウェアリソースを利用しますが、利用制限がないとハードウェアリソースを食いつぶしてしまい、他のプロセスに影響をきたす可能性があります。1つのコンテナがハードウェアリソースを使い果たすのを防ぐために、コンテナごとに使用できるハードウェアリソースを制限しています。

## 実際につくってみる
上で説明したコンテナの3つの特徴はすべてLinuxカーネル機能を使っているので、Dockerをインストールしなくてもコンテナっぽいものを作ることができます。

たったの9個のコマンドで簡易なコンテナを作ってみましょう。

全体の流れを先に説明すると、まずはホストOSから独立させるコンテナ用の**実行環境を作成**します。

次にcgroups を使って**リソースを制限**するグループの設定を行います。さらにプロセスとファイルシステムを分離し、リソースを制限した**コンテナを起動**しています。

最後に動作確認のために使用するpsコマンドを実行できるように、起動した**コンテナ内で設定**を加えています。


### 実行環境作成
```shell
1: mkdir -p /var/lib/container/test
2: cp -r /bin /lib /lib64 /var/lib/container/test
```

コンテナとしてプログラムを実行するために最低限必要なライブラリはホストOSから持ってきます。/var/lib/container/配下に test ディレクトリを作成して、その中に実行環境を作ります。

![4.3_container_overall.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/506008/94744b0c-038e-bafc-3132-15734108bc00.jpeg)



### cgroups のインストールと設定
cgroupsと呼ばれる技術を使うとCPUやメモリなどのハードウェアリソースを制限できます。具体的にはあらかじめリソースの使用量を制限したグループを作成し、そのグループにプロセスを割り当てることでプロセスごとのリソース使用量をコントロールできます。

```shell
3: yum install -y libcgroup libcgroup-tools
4: cgcreate -g cpu:/cpu_limit_70
5: cgset -r cpu.cfs_quota_us=70000 cpu_limit_70
```

コマンドの説明をすると、3つ目のyumでインストールすると/sys/fs/cgroupが作成され、中にはサブシステムと呼ばれるリソースを表すディレクトリなどが格納されています。

```
# ls /sys/fs/cgroup/
 blkio
 cpu,cpuacct freezer net_cls
 cpu
 cpuset
 cpuacct devices
 perf_event
 hugetlb net_cls,net_prio pids
 memory net_prio
 systemd
 ```

lssubsys コマンドで現在利用可能なサブシステムを確認できます。今回のコンテナ作成ではCPUのサブシステムを使用します。以下のようにcgget コマンドで設定されているパラメータを確認することができます。

```
# cgget-g cpu /
 /:
 cpu.cfs_period_us: 100000
 cpu.stat: nr_periods 0
 nr_throttled 0
 throttled_time 0
 cpu.shares: 1024
 cpu.cfs_quota_us:-1
 cpu.rt_runtime_us: 950000
 cpu.rt_period_us: 1000000
 ```

上記がデフォルトの設定です。新しくグループを作成するには4つ目のcgcreateコマンドを使用します。CPUサブシステムに関して、cpu_limit_70という名称のコントロールグループを作成します。
 
そして最後にcgsetコマンドで設定変更します。cgsetは-rでパラメータ名＝値を指定して、スペースの後に対象のコントロールグループのパスを指定します。

### コンテナ作成
cgexecでコンテナを作っていますが、unshareとchrootでプロセス分離、ファイルシステム分離も同時に行ってる盛りだくさんなコマンドです。
```shell
6: cgexec-g cpu:/cpu_limit_70 unshare --mount --pid --fork chroot /var/lib/container/test
```

1つずつ説明していくと、まずcgexecは指定したコントロールグループに属するプロセスを起動することができます。cpu_limit_70を指定しているので、CPUを70%まで使用できるプロセスが起動できます。

次に```unshare --mount --pid --fork```はunshareコマンドによりmntとpidの名前空間を分離しています。名前空間（Namespace）はプロセスID 番号やマウントポイントなどカーネルリソースを他のプロセスと隔離し、OS環境を独立させたように見せるLinuxカーネル機能です。

最後にchrootによりファイルシステムを分離します。今回だと/var/lib/container/testがルートディレクトリとなり何も指定しないので、/var/lib/container/test/bin/bashが実行されます。

> chroot ＜新しいルートディレクトリ＞ ＜ルートディレクトリ変更後に実行するコ
マンド ＞

### コンテナ内の設定
```shell
7: mkdir /proc /dev/pts
8: mount-t proc proc /proc/
9: mount-t devpts devpts /dev/pts
```
7と8のコマンドは、コンテナ内から見たときのプロセスIDを独立させるために実行しています。これがないとホストOSからPIDが6534の場合、コンテナから見ても6534になります。これはpsコマンドがprocファイルシステムを参照していることが原因です。これを回避するために、コンテナ起動後にmount-t proc proc /procを実行します。

## 作ったコンテナの確認
本当にホストから独立したプロセスになっているのか確認していきます。

まず1つ目は、chroot コマンドによりルートディレクトリが分離されていることです。cdコマンドでルートディレクトリに移動しても、元々の/var/lib/container/test が最上位となっていて/etc などの上位階層に遷移することができません。
```shell
bash-4.2# cd /
 bash-4.2# ls
 bin dev lib lib64 opt proc usr
```

 2 つ目は、unshare コマンドによるプロセスIDの分離です。psコマンドでコンテナ内に割り当てられているプロセスIDを確認します。起動中の/bin/bash に PID1 が割り当てられています。ホストOSのPID1とは異なりますので、プロセスIDを分離することができています。

 ```shell
bash-4.2# ps
 PID TTY TIME CMD
 1 ? 00:00:00 bash
 5 ? 00:00:00 ps
 ```

3 つ目は、cgroups による CPU 利用の制限です。本来、使えるだけのCPUを使用して"y"を出力し続けるコマンドyesを実行します。別のプロントからtopコマンドでyesコマンドのCPU使用率を確認すると70%付近を遷移し、CPUの利用を制限することができています。

 ```shell
 bash-4.2# yes > /dev/null
 # top
 PID   USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
 25747 root 20 0 114636 216 172 R 69.8 0.0 0:02.54 yes
 ```

 ## さいごに
 実際にコンテナを作って手を動かしてみると「ホストから独立している」という特徴とそのメリットを実感できました。今回はcgroupsにより使用するメモリやネットワーク帯域を制限したり、名前空間によりホスト名の設定を分離させたりすることでより独立性の高いコンテナを作ることができます。もし、ご興味があれば是非調べて試してみてください。