コンテナはどのようにして作られるのか

# はじめに

```
docker run hello-world /bin/bash
```
このコマンドを打てばhello-worldイメージがレジストリからpullされ、そのイメージからコンテナが作成され、その後にコンテナ内でbashが開かれます。
今回はこのコマンドを打つことでどのようにコンテナが作成されていくのか調べてみました。

# Docker Engineの全体像
以下の図は公式ドキュメントで紹介されているDocker Engineの全体像です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/b6620e6b-c11f-f765-8e61-98f0bf57f334.png)
(出典:[Docker 概要](https://docs.docker.jp/get-started/overview.html))

図にもあるようにDocker Engineはクライアント・サーバーモデルのアプリケーションです。
そして3つのコンポーネントによって作成されます。

- **Docker CLI** (クライアント)
- **Dockerデーモン** (サーバー)
- **Docker Engine API** (REST API)

以下でそれぞれについて説明します。

# Docker CLI
Docker CLIはDockerのクライアントです。

```docker
docker run hello-world /bin/bash
```

などdockerコマンド以降にこの後説明するDockerデーモンに行って欲しいこと(コンテナ立ち上げ、イメージのビルド)やイメージの指定、コマンドの指定などを書き加えます。
Docker CLIでコマンドを打つことで、Docker Engine APIというものを通じてDockerデーモンに指示が渡り、Docker CLIで指示した内容を処理してくれます。
# デーモンとは
Dockerデーモンについて説明する前にそもそもデーモンとはなんなのでしょうか?

デーモンのスペルは**daemon**で守護神の意味です。
デーモンはバックグラウンドで動いており、基本的にユーザーが意識することはありませんが縁の下の力持ちとしての役割を担っています。
バックグラウンドで動いているようなWebサーバーもデーモンの１種と言えます。
また、慣例としてデーモンの最後にはdを付けます。(httpdなど)

そしてデーモンは**プロセス**の１種です。処理要求があるのを待ち続け、要求があった時に自分自身をコピーしたプロセスを作り、処理を行います。アクセスがあるまではずっと待機の状態です。
しかし、アクセスが多いWebサーバーなどはデーモンはアクセスの度にプロセスを作成するためプロセスが多くなります。結果として負担がかかりサーバーが落ちることがあります。

## スーパーデーモン
それを防ぐために**スーパーデーモン**というものがあります。
これはデーモンを管理するデーモンで、複数のデーモンの代わりに待ち続けます。特定のデーモンが必要になった場合はそのデーモンのみを立ち上げるように指示を出します。これによって無駄なデーモンが常に立ち上がることがなくなるためサーバーへの負荷を減らすことができます。

# Dockerデーモン 
Dockerデーモンはその名の通りDocker用のデーモンです。
デーモンなので**dockerd**というプロセスで動きます。dockerdはDocker Engine APIが呼び出されるのを待ち続けます。
Dockerデーモンは呼び出されたDocker Engine APIに応じてイメージのビルドやコンテナの起動などを行います。
Dockerクライアント(Docker CLI)とDockerデーモンの間の通信にはDocker Engine APIが利用され、UNIXドメインソケットやネットワークインタフェースを介して行われます。
デフォルトは**unixドメインソケット**を使用して通信を行います。

## unixドメインソケット
ここでunixドメインソケットと言う単語が出てきましたので説明します。
unixドメインソケットはファイルシステム(.sock)を介してプロセス間通信を行うものです。
似たような物として**ソケット**がありますが、unixドメインソケットとは異なります。
どちらも**プロセス間のやり取り**を行う手法の１つですが、ソケットは**異なるホスト間**での通信をし、unixドメインソケットは**同じホスト間**での通信を行います。
そのためunixドメインソケットはコンピューター内部でしか使用できませんが、高速にデータをやり取りすることができます。通信先にはIPアドレスやドメイン名は指定せずに**ソケットファイルファイルのパス**を指定します。

Dockerでは/var/docker/docker.sockというソケットファイルを使用してプロセス間通信を実現しています。
そのため、このソケットファイルを変更することでDockerクライアントからリモートにあるDockerデーモンへのアクセスを行うこと
リモートのDockerデーモンと接続する場合はTCPソケット(一般的なソケット)を使用するため設定を変更する必要があります。

# Docker Engine API
このAPIはDocker Remote API, REST APIなどいろいろな呼び方がされていますが、今回はDocker Engine APIに統一して説明します。Docker CLIはDocker Engine APIを通して、スクリプトやコマンドの実行を行い、Dockerデーモンを制御したり入力を行ったりします。

実はDocker CLIは単にクライアントにすぎないため、Docker CLIを使わないでもDocker Engine APIを叩くことができ、コンテナの作成を行うことができます。しかし、Docker Engine APIのプロセス通信にはUNIXドメインソケットがデフォルトで使われていることからそのままでは使うことができません。
そのためこのAPIを使うにはTCPが使えるように設定を変更することでAPIを直接叩くことができます。

また、Docker Engine APIのバージョンはDockerデーモンとDockerクライアントのバージョンによって異なります。
docker versionコマンドでバージョンを確認できます。私の環境のAPIは1.40が使われていました。

```shell
$ docker version

Client: Docker Engine - Community
 Cloud integration: 1.0.1
 Version:           19.03.13
 API version:       1.40
 Go version:        go1.13.15
 Git commit:        4484c46d9d
 Built:             Wed Sep 16 16:58:31 2020
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.13
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.13.15
  Git commit:       4484c46d9d
  Built:            Wed Sep 16 17:07:04 2020
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          v1.3.7
  GitCommit:        8fba4e9a7d01810a393d5d25a3621dc101981175
 runc:
  Version:          1.0.0-rc10
  GitCommit:        dc9208a3303feef5b3839f4323d9beb36df0a9dd
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
```

# 全体像
こちらの図は公式のものですが、大まかな流れがわかるかと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/487a6882-cc45-3647-42cd-0ee5293ba588.png)
(出典:[Docker アーキテクチャの理解](https://docs.docker.jp/engine/introduction/understanding-docker.html#:~:text=Docker%20%E3%81%AF%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%83%BB%E3%82%B5%E3%83%BC%E3%83%90%E5%9E%8B,%E4%B8%8A%E3%81%A7%E3%82%82%E5%AE%9F%E8%A1%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82))

# Dockerデーモンの動作
先ほどDockerデーモンはDocker CLIから受け取った指示に基づいて処理を行うと説明しましたが、実はコンテナの作成などを行っているのはDockerデーモンではありません。
Dockerデーモンは、コンテナの起動、イメージのビルド、ネットワーク、ボリュームなどコンテナ全体の**管理**を行います。
コンテナ作成などを行っているのは**コンテナランタイム**が行っています。

# コンテナランタイム
コンテナランタイムは２種類存在しています。
・高レベルランタイム
・低レベルランタイム
それぞれ役割が異なります。以下で詳しく説明していきます。

## 高レベルランタイム
高レベルランタイムはデーモンとして常駐しており、Dockerデーモンと直接対話して、低レベルランタイムに内容を渡します。
高レベルランタイムは管理しているイメージの情報を低レベルランタイムに渡して、コンテナ作成などの指示を低レベルランタイムに渡します。

### containerd
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/d2e7b890-d412-af8e-a38f-694ef1805d7a.png)

Docker内部でデフォルトで用いられているのは**containerd**という高レベルランタイムで、**コンテナイメージ**を管理する役割を担っています。
Dockerではcontainerdはコンテナの実行・管理を担当します。イメージのpullなどはdockerdが行います。
(k8sになるとcontainerdがレジストリとの通信を行います。[こちらの資料](https://www.slideshare.net/KoheiTokunaga/containerd-241949715)でわかりやすく説明されています。)
containerdはイメージの情報を**Filesystem bundle**と呼ばれるコンテナの素を作成して低レベルランタイムに渡します。
Filesystem bundleはイメージをpullした後、そのイメージを構成するファイルをruncなどの低レベルランタイムに渡すときの格納方法を定めたものです。
また、containerd用のCLIコマンドはctrを使用してコンテナを作成することも可能です。
containerdが低レベルランタイムを呼び出す時は、shimと呼ばれるバイナリコンポーネントを通じて行います。
低レベルランタイムはそれぞれの持つアーキテクチャにあったshimを実装し、それをcontainerdにプラグインすることで、containerdを通じてそれら低レベルランタイムを操作することができます。

## 低レベルランタイム
高レベルランタイムからの指示を受け、コンテナ作成などを行います。
低レベルランタイムは高レベルランタイムとは異なりデーモンではないため常駐はしません。
低レベルランタイムはOCIによって決められた**OCI Runtime Specification**に基づいた実装がされているため、入れ替えることが可能です。

## OCI Runtime Specification
OCI Runtime Specificationでは標準的なコンテナの５原則が定義されています。

1. **Standard operations**  
標準的なコンテナは一連の標準的な操作が実装されています。
2. **Content-agnostic**  
どんな標準的な操作も内容にかかわらず同様の結果を得ることができます。
3. **Infrastructure-agnostic**  
OCIがサポートしたインフラ上であれば動かすことができます。
4. **Designed for automation**  
コンテンツやインフラに関係なく同じ操作を提供することができるため自動化に向いている。
5. **Industrial-grade delivery**  
どんな企業でもソフトウェアの配布を行うことができる。

加えてスタンダード(標準的な)コンテナは以下の３つの仕様が揃っているとされています。
・ ファイルの構成形式  (Filesystem Bundle)
・ 基本的な操作のセット (create, start, kill, delete, etc..)
・ 実行環境 (config)
以下の図のようにコンテナのライフサイクルを定義し、標準化することで一貫性のあるプロセスを定義することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/92df6fef-01c3-31d1-bfe6-f4cfda54f40f.png)
(出典:https://www.slideshare.net/KoheiTokunaga/ocirunc?ref=https://cdn.embedly.com/)

そして今回はDockerで用いられているOCIのリファレンス実装の低レベルランタイム**runc**について少し触れていこうと思います。
### runc
runcはOCIによるリファレンス実装の低レベルコンテナランタイムです。runcコマンドを使ってコンテナの操作を行うことができます。
また、現在は修正されていますがruncには脆弱性が報告されたことがあります。これは悪意のあるイメージを実行した際に、そのイメージがruncバイナリを上書きし、rootとしてコードが実行できてしまったというものでした。
これはruncがコンテナを作成するのがホストOSのkernel上に作成するため、１つの悪意のあるイメージがrootを使用できると他のコンテナにも影響があるというものでした。
![runc (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/f8a80d82-ef10-d0f3-b620-a2c4209f10ae.jpeg)

# まとめ
以上、これまでの流れをまとめると以下のようになります。
１つ１つのコンテナはruncが管理し、コンテナ群をcontainerdで管理し、dockerコマンドの受け付けや全体の処理をdockerdが行います。
![docker.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/324574/bd6becd4-c428-9f68-e792-584adb4c9448.jpeg)

# 参考文献
[Docker アーキテクチャの理解](https://docs.docker.jp/engine/introduction/understanding-docker.html#:~:text=Docker%20%E3%81%AF%E3%82%AF%E3%83%A9%E3%82%A4%E3%82%A2%E3%83%B3%E3%83%88%E3%83%BB%E3%82%B5%E3%83%BC%E3%83%90%E5%9E%8B,%E4%B8%8A%E3%81%A7%E3%82%82%E5%AE%9F%E8%A1%8C%E3%81%A7%E3%81%8D%E3%81%BE%E3%81%99%E3%80%82)
[コンテナの作り方「Dockerは裏方で何をしているのか？」](https://www.slideshare.net/zembutsu/what-isdockerdoing)
[Docker Engine API](https://docs.docker.com/engine/api/)
[コンテナランタイムの仕組みと、Firecracker、gVisor、Unikernelが注目されている理由。 Container Runtime Meetup #2](https://www.publickey1.jp/blog/20/firecrackergvisorunikernel_container_runtime_meetup_2.html)
[OCI Runtime Specificationを読んだので概要を書く](https://gkuga.hatenablog.com/entry/2020/01/24/032122)
[OCIランタイムの筆頭「runc」を俯瞰する](https://www.slideshare.net/KoheiTokunaga/ocirunc?ref=https://cdn.embedly.com/)
**書籍**
[イラストでわかるDockerとKubernetes (Software Design plus)](https://www.amazon.co.jp/gp/product/4297118378/ref=ppx_yo_dt_b_asin_title_o00_s00?ie=UTF8&psc=1)