はじめてのDocker（基本編）

この記事は[iRidge Advent Calendar 2018](https://qiita.com/advent-calendar/2018/iridge)の22日目の記事です。

## はじめに

2018年8月に転職し、初めてDockerなるものを触ることになりました。
https://docker-curriculum.com/ に沿ってキャッチアップし、
その中で学んだ知識を整理しましたので共有したいと思います。私と同じようにDockerに入門する方の役に立てば幸いです。

## 用語

* イメージ - コンテナの基礎をなすアプリケーションの設計図。
* コンテナ - イメージから作成され、実際にアプリケーションが稼働する。
* Dockerデーモン - Dockerコンテナの構築、実行、及び配布を管理する、ホスト上で実行されるバックグラウンドサービス。デーモンはクライアントと通信するOSで実行されるプロセス。
* Dockerクライアント - ユーザがデーモンと対話できるようにするCLIツール。
* DockerHub - Dockerイメージのレジストリ。必要に応じて独自のDockerレジストリをホストし、イメージを取得するために使用できる。


## コマンド

* `docker pull`コマンドで`busybox`という名称のimageをDockerレジストリから取得する。

```zsh
$ docker pull busybox
```

* `docker image`コマンドで、取得したイメージをリスト表示する。
* `TAG`はイメージの特定のスナップショットを参照していて、`IMAGE ID`はそのイメージのユニークな識別子となっている。

```zsh
$ docker images
REPOSITORY                    TAG                 IMAGE ID            CREATED             SIZE
nginx                         alpine              d3dcc25e0dc4        12 days ago         17.8MB
busybox                       latest              59788edf1f3e        6 weeks ago         1.15MB
hello-world                   latest              4ab4c602aa5e        2 months ago        1.84kB
ubuntu                        latest              735f80812f90        3 months ago        83.5MB
nginx                         latest              c82521676580        3 months ago        109MB
python                        2.7.14-slim         a7804e68bf72        6 months ago        139MB
```

* `ubuntu`の特定バージョンをpullしたい場合は、以下のようにする。

```zsh
$ docker pull ubuntu:12.04
12.04: Pulling from library/ubuntu
d8868e50ac4c: Pull complete
83251ac64627: Pull complete
589bba2f1b36: Pull complete
d62ecaceda39: Pull complete
6d93b41cfc6b: Pull complete
Digest: sha256:18305429afa14ea462f810146ba44d4363ae76e4c8dfc38288cf73aa07485005
Status: Downloaded newer image for ubuntu:12.04
$ docker images ubuntu
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
ubuntu              latest              735f80812f90        3 months ago        83.5MB
ubuntu              12.04               5b117edd0b76        19 months ago       104MB
```

* `docker run`コマンドで`busybox`イメージのDockerコンテナを起動する。

```zsh
$ docker run busybox
$
```

このとき、Dockerクライアントはイメージを探し、コンテナ内でコマンドを実行する。
でも、上のコマンドでは実行するコマンドを与えていないので、コンテナが立ち上がったあと何もせずに終了した。

以下のコマンドは上と違って、`echo`コマンドが実行された後に終了した。

```zsh
$ docker run busybox echo "hello from busybox"
hello from busybox
$ 
```

* `docker ps`コマンドは現在起動中の全てのコンテナを表示する。

```zsh
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS              PORTS               NAMES
```

* `-a`オプションを付けると実行済みのコンテナも含め全てを表示する。

```
$ docker ps -a
CONTAINER ID        IMAGE                         COMMAND                   CREATED             STATUS                        PORTS                  NAMES
43daaba225dd        busybox                       "echo 'hello from bu…"    5 minutes ago       Exited (0) 5 minutes ago                             quizzical_ptolemy
04c83ee35767        busybox                       "sh"                      30 minutes ago      Exited (0) 30 minutes ago                            clever_swirles
7952dfa41dc4        hello-world                   "/hello"                  33 minutes ago      Exited (0) 33 minutes ago                            reverent_euler
```

* コンテナ内で複数コマンドを実行したい場合は`-it`オプションを使用する。インタラクティブttyをコンテナ内でアタッチする。

```zsh
$ docker run -it busybox sh
/ # ls
bin   dev   etc   home  proc  root  sys   tmp   usr   var
/ # uptime
 03:19:03 up  9:28,  0 users,  load average: 0.02, 0.06, 0.02
/ #
```

* `docker run --help`で、全フラグの一覧を見ることが出来る。

* `docker run`を何度も実行してそのままにしておくと、ディスク領域を食いつぶしてしまうので、`docker rm`コマンドを実行すればコンテナを削除できる。

```zsh
$ docker rm b99636fc4c19
b99636fc4c19
```

* 終了済みのコンテナをいっぺんに削除したいなら、`docker rm $(docker ps -a -q -f status=exited)`とする。
    * `-q`はIDだけを返却し、`-f`は指定した状態のみでフィルタリングしている。

```zsh
$ docker rm $(docker ps -a -q -f status=exited)
f28e03403d45
2f12ba39987b
e942ae377ecf
43daaba225dd
04c83ee35767
34efcecec10d
7952dfa41dc4
8a911e50170a
14656bae8192
020dff00d96e
```

* 実は`docker container prune`コマンドで同様の結果が得られる。

```zsh
$ docker container prune
WARNING! This will remove all stopped containers.
Are you sure you want to continue? [y/N] y
Deleted Containers:
bb45579d3020722abc88fd570400c10641e3db0a51cad0a6afc34421af67963e
41c78c7ee3fcc88ec1cff64ca69e827b36fdeb433cc7832dea518dde5e8593d3

Total reclaimed space: 0B
```

* また、`docker run`コマンドに`--rm`フラグを付与して起動すると、終了時にコンテナが削除される。

* もう必要ないimageは`docker rmi`で削除できる。

```zsh
$ docker rmi hello-world
Untagged: hello-world:latest
Untagged: hello-world@sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Deleted: sha256:4ab4c602aa5eed5528a6620ff18a1dc4faef0e1ab3a5eddeddb410714478c67f
Deleted: sha256:428c97da766c4c13b19088a471de6b622b038f3ae8efa10ec5a37d6d31a2df0b
```

## Webアプリケーションをデプロイしてみる

このデモ用のアプリケーションがレジストリにホストされているのでそれをダウンロードして`docker run`してみる。

```
$ docker run --rm prakhar1989/static-site
```

イメージがローカルになければ、クライアントはまずレジストリからイメージをフェッチしてきて、それから起動する。
うまく行けば`Nginx is running...`とターミナルに表示される。
ただ、この場合、どのポートも公開されていないので、ポートを公開するために以下の通り`docker run`コマンドを再実行する必要がある。

```zsh
$ docker run -d -P --name static-site prakhar1989/static-site
721f9bed9efbe569e17361fa327d2719ef01246de8f8894850896ff13ace34ff
```
* `-d`：ターミナルのデタッチ
* `-P`はランダムなポートを公開し
* `--name`は起動するコンテナに名前を付ける。

* `docker port [CONTAINER]`コマンドで、ポートを確認することが出来る。

```zsh
$ docker port static-site
443/tcp -> 0.0.0.0:32768
80/tcp -> 0.0.0.0:32769
```

* `-p 8888:80`のようにすることで、クライアントが接続を転送するカスタムポートを指定することも出来る。

```zsh
$ docker run -p 8888:80 prakhar1989/static-site
Nginx is running...
```

* `docker stop`コマンドにコンテナIDを与えて実行すると、デタッチしたコンテナの停止が出来る。このとき、起動時に指定した名前を使うことも出来る。

```zsh
$ docker stop static-site
static-site
```

これを実サーバにデプロイするなら、単にDockerをインストールし、その上でDockerコマンドを実行すればよい。

### Dockerイメージの作成

新しいDockerイメージを取得したい場合、DockerHubのようなレジストリから取得することも、自分で作成することも出来る。
DockerHub上には多数のイメージがある。また、イメージはCLIから`docker search`で検索することも出来る。

* baseイメージとchildイメージの違い
    * baseイメージは親イメージを持たないもので、ubuntu、busybox、debianなど。
    * childイメージはbaseイメージを元に作成され、機能が追加されたもの。

* officialイメージとuserイメージの違い
    * officialイメージは公式にメンテとサポートされているもの。
        * 通常は一語で、`python`、`ubuntu`、`busybox`、`hello-world`などがそれにあたる。
    * userイメージは一般ユーザにより作成されシェアされているもの。
        * baseイメージに機能を追加している。
        * 通常、`user/image-name`というフォーマットである。

よりよく理解するために、簡単なFlaskアプリケーションのイメージを作成してみる。
ロード時に毎回ランダムな猫のgifをディスプレイに表示するFlaskアプリケーションが作成されているので、ローカルにリポジトリをクローンする。

```zsh
$ git clone https://github.com/prakhar1989/docker-curriculum
$ cd docker-curriculum/flask-app
# この手順はローカルで実施すること。dockerコンテナ内ではない
```

そして、今クローンしたWebアプリケーションのイメージを作成するために、Dockerfileを作成する。

Dockerfileとは、シンプルなテキストファイルで、Dockerクライアントがイメージを作成する時にコールするコマンドが記述されている。
また、Dockerfileに記述するコマンドはほとんどLinuxコマンドと同等であり、これは新しい構文をDockerfile作成のために新たに覚える必要はないということになる。

まずはじめに、Flaskアプリケーションと同フォルダに、`Dockerfile`という名前で空ファイルを作成する。
baseイメージとしてはpython3を使用する。特に、`python:3-onbuild`のpythonイメージを使用する。

```dockerfile
FROM python:3-onbuild
```

とする。次に公開するポート番号を指定する。Flaskアプリケーションは`5000`ポートで動くので、それを指定する。

```dockerfile
EXPOSE 5000
```

最後に、アプリケーションの実行のためのコマンドを指定する。単に`python ./app.py`と書く、`CMD`コマンドを使うと

```dockerfile
CMD["python", "./app/py"]
```

と書ける。`CMD`で、コンテナに対して、起動時に実行すべきコマンドを指示している。

これで、イメージをビルド出来る。`docker build`コマンドで`Dockerfile`からDockerイメージを作成する。
`-t`オプションにタグ名を与え、`Dockerfile`を含むディレクトリの場所を指定する。

```zsh
$ docker build -t kamihara/catnip .
Sending build context to Docker daemon  8.704kB
Step 1/3 : FROM python:3-onbuild
3-onbuild: Pulling from library/python
1c7fe136a31e: Pull complete
ece825d3308b: Pull complete
06854774e2f3: Pull complete
f0db43b9b8da: Pull complete
2d21c767035c: Pull complete
e10b68fb77e6: Pull complete
e2c0fb34dff3: Pull complete
e96bc319bce0: Pull complete
490280dbcfb7: Pull complete
Digest: sha256:76df62c122c910751d8cd3101f8e3da39efd4ee828686b7ff0b5a5b1d967553f
Status: Downloaded newer image for python:3-onbuild
# Executing 3 build triggers
 ---> Running in 431f3c96d745
Collecting Flask==0.10.1 (from -r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/db/9c/149ba60c47d107f85fe52564133348458f093dd5e6b57a5b60ab9ac517bb/Flask-0.10.1.tar.gz (544kB)
Collecting Werkzeug>=0.7 (from Flask==0.10.1->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl (322kB)
Collecting Jinja2>=2.4 (from Flask==0.10.1->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/7f/ff/ae64bacdfc95f27a016a7bed8e8686763ba4d277a78ca76f32659220a731/Jinja2-2.10-py2.py3-none-any.whl (126kB)
Collecting itsdangerous>=0.21 (from Flask==0.10.1->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting MarkupSafe>=0.23 (from Jinja2>=2.4->Flask==0.10.1->-r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/08/04/f2191b50fb7f0712f03f064b71d8b4605190f2178ba02e975a87f7b89a0d/MarkupSafe-1.1.0-cp36-cp36m-manylinux1_x86_64.whl
Installing collected packages: Werkzeug, MarkupSafe, Jinja2, itsdangerous, Flask
  Running setup.py install for Flask: started
    Running setup.py install for Flask: finished with status 'done'
Successfully installed Flask-0.10.1 Jinja2-2.10 MarkupSafe-1.1.0 Werkzeug-0.14.1 itsdangerous-1.1.0
You are using pip version 10.0.1, however version 18.1 is available.
You should consider upgrading via the 'pip install --upgrade pip' command.
Removing intermediate container 431f3c96d745
 ---> c14f035b14b0
Step 2/3 : EXPOSE 5000
 ---> Running in 86d209c4dec8
Removing intermediate container 86d209c4dec8
 ---> 3e48f1f15bc9
Step 3/3 : CMD ["python", "./app.py"]
 ---> Running in 6afca14ae3fc
Removing intermediate container 6afca14ae3fc
 ---> 82068b34e99b
Successfully built 82068b34e99b
Successfully tagged kamihara/catnip:latest
```

もし、`python:3-onbuild`イメージを持っていなければ、はじめにそのイメージをpullしてから、自分のイメージが作成される。

`docker run`でイメージを実行して、実際の動作を確認できる。
以下のコマンドで、コンテナ内の5000ポートを使用し外部に8888ポートを割り当てた。ポート8888を使ったURLでアプリの動作が確認できる。

```zsh
docker run -p 8888:5000 kamihara/catnip
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)
```

### AWSへのデプロイ

AWSへのデプロイをする前に、AWSがアクセス出来るレジストリにイメージを公開する必要がある。
以下のコマンドでDockerHubに公開できる。`username/image_name`の形式が重要。

```zsh
$ docker push kamihara/catnip
denied: requested access to the resource is denied
```

これが終われば、イメージをDockerHub上で見ることが出来る。
今イメージがオンライン上にあり、Dockerがインストール済みの人は誰でも、一コマンドでアプリを実行できる。

```zsh
$ docker run -p 8888:5000 kamihara/catnip
```

#### Beanstalk

Elastic BeanstalkはAWSが提供しているPaaSで、もしHeroku、GAEなどを使っていたことがあれば馴染み深い。
EBは非常に直感的なCLIを備えているが、いくつかの設定が必要だ。簡単にするために、Web UIを使用してアプリケーションを起動する。

ここでのAWSコンソールの細かい操作説明は省略（※元サイトをご覧ください）するが、基本的にはいま作成したアプリケーションはコンテナ化されているので、ElasticBeanstalkに対してはコンテナの情報を伝えればよい。
`Dockerrun.aws.json`を簡単に見てみよう。ここではEBが使用するイメージ名とコンテナが開くポート番号を提供している。

```Dockerrun.aws.json
{
  "AWSEBDockerrunVersion": "1",
  "Image": {
    "Name": "kamihara/catnip",
    "Update": "true"
  },
  "Ports": [
    {
      "ContainerPort": "5000"
    }
  ],
  "Logging": "/var/log/nginx"
}
```

このファイルをアップロードすればアプリケーションが準備できているはずだ。
EBのページを見れば、アプリが起動している事を示す緑のチェックマークがついているはず。

## まとめ

Dockerの基本コマンドから始まり、簡単なDockerfileの作成、AWSのElasticBeanstalkへのアプリケーションデプロイまでをやりました。~~軽い気持ちで和訳メモを作り始めて、ボリュームの多さに心折れかけてました。~~
https://docker-curriculum.com/ のすごいところは、これで終わりではなくマルチコンテナ環境もカバーしているところです。
というわけで、明日はその2としてマルチコンテナ環境への入門編を書きたいと思います。