Dockerを体験してみるハンズオン Part3

前回までのあらすじ

* [Part1 dockerをとりあえず動かしてみた](http://qiita.com/NewGyu/items/24290b79ecb606785edc)
* [Part2 dockerイメージがどんな管理になっているのか](http://qiita.com/NewGyu/items/e200c9fac28d4e2e3cb4)

今回はDockerイメージを実際に作成してリポジトリに登録してみます。

ここで使用しているソースコードはこちらに格納してあります。
https://github.com/NewGyu/docker-demo
[readme.md](https://github.com/NewGyu/docker-demo/blob/master/readme.md)にもある程度のことを書いてあるので参考にしてください。

## ビルドできる環境を作る

https://github.com/NewGyu/docker-demo をビルドするには

* JDK...tomcatにデプロイするwarファイルを作るため  
  JREではなく、javacを含むJDKが必要になります（コンパイルするので）
* git...上記のソースコードを取得するため

が必要です。

```shell-session
$ sudo yum install java-1.8.0-openjdk-devel git
```

勘違いしてほしくないのはこれらはDockerコンテナを動かす上では必要ありません。あくまで私が用意したサンプルをコンパイル・ビルドするために必要だということです。

本来は以下のようにビルド環境と実行環境は別々になるのですが、
![](https://cacoo.com/diagrams/V55Sg3UVjneVSwQv-AAA0D.png)
今回のハンズオンでは一つのインスタンスでまとめてやってしまいます。

## ビルドしてみよう

### ソースコードを取得

普通にgithubからソースコードをcloneで取得してください。

```shell-session
$ git clone https://github.com/NewGyu/docker-demo.git
```

こんなディレクトリ構成です。

```
docker-demo/
├── app                ...下記のinfraのイメージを元にhello.war入りのイメージを作成するもの
│   ├── Dockerfile
│   ├── build.gradle   ...hello.warのビルドスクリプト
│   ├── gradlew
│   ├── gradlew.bat
│   └── src            ...hellow.warのソースコード
├── infra              ...tomcat7+apache2.4の入ったイメージを作成するもの
│   ├── Dockerfile
│   ├── apache.conf
│   │   ├── extra
│   │   └── httpd.conf
│   └── httpd-foreground
└── readme.md
```

### warファイルの生成

warファイルのビルドにはgradleを使用しています。
gradleにはmavenなどの他のビルドツールと違って、gradleをインストールせずにビルドできる[gradle wrapper](http://gradle.monochromeroad.com/docs/userguide/gradle_wrapper.html)という素敵な仕組みがありますが、本筋ではないので紹介だけして割愛します。ここではとりあえず「ソースに同梱されている`gradlew`スクリプトを叩けばいい」ということだけ理解してください。
warファイルのビルド内容については`build.gradle`に記述されていますが、これについても本筋ではないので割愛します。

取得したソースそのままでは代わり映えがしないので``` vi src/main/webapp/index.jsp ```でjspファイルを編集してみましょう。その後に以下のコマンドを実行してください。

```shell-session
$ cd docker-demo/app
$ ./gradlew war
```

すると`./app/build/libs/hello.war`が生成されます。

### イメージのビルド

続いてDockerイメージを作成します。
ビルドするイメージに対する内容は`./app/Dockerfile`に書かれています。こちらについては後述します。

```shell-session:ビルドを実行
$ docker build .

Sending build context to Docker daemon 460.8 kB
Sending build context to Docker daemon 
Step 0 : FROM newgyu/tomcat:1.0.0
1.0.0: Pulling from newgyu/tomcat
e2a4fb18da48: Already exists 
58016a5acc80: Already exists 
  :
a48de4915c8d: Already exists 
Digest: sha256:473b2575f7a97c904ee1c3735891d85313ff254be617f41f21ca83002e84ba46
Step 1 : RUN rm -rf /usr/local/tomcat/webapps/ROOT
 ---> Running in 415983e2d95c
 ---> 851e0dd6ea1e
Removing intermediate container 415983e2d95c
Step 2 : COPY build/libs/hello.war /usr/local/tomcat/webapps/ROOT.war
 ---> 6ab8fbc5b09d
Removing intermediate container ed79995666c9
Successfully built 6ab8fbc5b09d
```

ということでイメージID``6ab8fbc5b09d``が出来ました。

```shell-session
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              VIRTUAL SIZE
<none>              <none>              6ab8fbc5b09d        About a minute ago   640.9 MB
```

### Dockerfileとビルド動作

`docker build`では引数に指定されたディレクトリにある`Dockerfile`を読み込んで動作します。
サンプルアプリケーションの`Dockerfile`は以下の内容になっています。

```:./app/Dockerfile
FROM newgyu/tomcat:1.0.0

RUN rm -rf /usr/local/tomcat/webapps/ROOT
COPY build/libs/hello.war /usr/local/tomcat/webapps/ROOT.war
```

ここから上記`Dockerfile`の内容と`docker build`時に出力された結果ログを突き合わせて解説していきます。
`Dockerfile`の記法の詳細については[公式のリファレンス](https://docs.docker.com/reference/builder/)を参照してください。

#### FROM

まず注目すべきは`FROM`というインストラクションです。
ここにはどのイメージを元にして作成するかが書かれています。（オブジェクト指向でいうところの継承元ですね）自前のDockerイメージを作るときには必ず何かのベースイメージから作ることになるのでFROMは必須になります。

```shell-session:ログ
Step 0 : FROM newgyu/tomcat:1.0.0
1.0.0: Pulling from newgyu/tomcat
e2a4fb18da48: Already exists 
58016a5acc80: Already exists 
  :
a48de4915c8d: Already exists 
Digest: sha256:473b2575f7a97c904ee1c3735891d85313ff254be617f41f21ca83002e84ba46
```

[part1](http://qiita.com/NewGyu/items/24290b79ecb606785edc#%E3%82%82%E3%81%86%E4%B8%80%E5%BA%A6%E5%8B%95%E3%81%8B%E3%81%97%E3%81%A6%E7%A2%BA%E8%AA%8D)で`docker run`した時の動作と同じで、イメージファイルからコンテナを起動します。ローカルリポジトリに該当するイメージがなければ暗黙的に`pull`する動作も同じです。


#### RUN

RUNはイメージを作成する上で最も使われるインストラクションです。
見てわかると思いますが、RUNのあとにLinuxのコマンドを書きます。DockerfileはChefやAnsibleと異なり、DSLではなくLinuxコマンドを普通記述します。Linuxに慣れている人にとってはこの方が理解しやすいでしょう。

複数のRUNを書くこともできるし、一つのRUNで`&&`で区切って複数のコマンドを書くことも出来ます。

```shell-session:&&で複数書く例
RUN apt-get update \
    && apt-get install -y --no-install-recommends
```

```shell-session:RUNインストラクションを複数書く例
RUN apt-get update
RUN apt-get install -y --no-install-recommends
```

今回のサンプルでは`rm`コマンドを実行する実行しています。

```shell-session:ログ
Step 1 : RUN rm -rf /usr/local/tomcat/webapps/ROOT
 ---> Running in 415983e2d95c
 ---> 851e0dd6ea1e
Removing intermediate container 415983e2d95c
```

この２種類の書き方には大差はありませんが、後述するコミットの単位が異ってきます。

#### COPY

ビルドするマシンのローカルにあるファイルをイメージにコピーします。

```:./app/Dockerfile
COPY build/libs/hello.war /usr/local/tomcat/webapps/ROOT.war
```

この例ではgradleでビルドした`hello.war`を`ROOT.war`という名前にしてイメージ内にコピーします。
ローカルファイルのパスは`Dockerfile`からの相対パスで指定します。

```shell-session:ログ
Step 2 : COPY build/libs/hello.war /usr/local/tomcat/webapps/ROOT.war
 ---> 6ab8fbc5b09d
Removing intermediate container ed79995666c9
```

### docker commit

dockerのコンテナは実は変更をするたびにバージョン管理が出来ます。このバージョン管理の仕組みは先のビルド時の、

```shell-session:ログ
Step 1 : RUN rm -rf /usr/local/tomcat/webapps/ROOT
 ---> Running in 415983e2d95c
 ---> 851e0dd6ea1e
      ^^^^^^^^^^^^
```

このログ出力と密接に関係しています。

少し実験してみましょう。

#### コンテナでshellコマンドを実行

```shell-session
$ docker run -it newgyu/tomcat:1.0.0 /bin/bash
root@cd7240eac1a2:/usr/local/apache2# cd /usr/local/tomcat/
root@cd7240eac1a2:/usr/local/tomcat# ls -l webapps/
total 20
drwxr-xr-x  3 root root 4096 Oct 14 13:29 ROOT
drwxr-xr-x 14 root root 4096 Oct 14 13:29 docs
drwxr-xr-x  7 root root 4096 Oct 14 13:29 examples
drwxr-xr-x  5 root root 4096 Oct 14 13:29 host-manager
drwxr-xr-x  5 root root 4096 Oct 14 13:29 manager
```

`newgyu/tomcat:1.0.0`というイメージはtomcatをインストールしたばかりの状態なので、webappsディレクトリにtomcatデフォルトの

* ROOT
* docs
* example
* host-manager
* manager

というWEBアプリケーションがインストールされた状態になっています。
そのうちのROOTを消してみます。

```shell-session
root@cd7240eac1a2:/usr/local/tomcat# rm -rf /usr/local/tomcat/webapps/ROOT/
```

そしてshellを終わりましょう。

```shell-session
root@cd7240eac1a2:/usr/local/tomcat# exit
exit
```

#### コンテナの状態をコミットする

さて、掲題の`docker commit`です。
が、その前に`docker ps`で先ほど作業したコンテナのIDを確認しましょう。すでに終了させたコンテナなので`-a`オプションをつけます。

```
$ docker ps -a
CONTAINER ID        IMAGE                        COMMAND                CREATED             STATUS                       PORTS               NAMES
cd7240eac1a2        newgyu/tomcat:1.0.0          "/bin/bash"            21 minutes ago      Exited (130) 6 minutes ago                       lonely_ritchie      
```
`cd7240eac1a2`をコミットすると、

```
$ docker commit cd7240
c426b57d7bc11b89724b574a04b1ea8b49ba0ab5bdaf4b119bed17dae691db13
newgyu@newgyu-UX31E:~/Desktop/dockersample/app$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
<none>              <none>              c426b57d7bc1        3 seconds ago       640.6 MB
```
`c426b57d7bc1`というイメージが作成されましたね。

`docker commit`コマンドを使うことで、コンテナのある状態をローカルリポジトリの中のイメージとして保存することができるというものになります。つまり`Dockerfile`,`docker build`を使わなくても、
![カチャカチャ ッターン](http://livedoor.blogimg.jp/jigokuno_misawa/imgs/3/7/3725cf20.gif)
とすることでイメージが作れるのです。

ただし、これをやると「どうやって作ったのか不明なVMイメージ」と全く同じことになるので、最近では当然になってきた「Infrastructure As Code」を実践するために`Dockerfile`でイメージの作り方をコード化するようにしましょう。（食品でも生産地とか添加物とかを明示する義務があるように、インフラも見える化すべきですね）

#### さて、ここでdocker buildを振り返る

> dockerのコンテナは実は変更をするたびにバージョン管理が出来ます。このバージョン管理の仕組みは先のビルド時の、
> 
```shell-session:ログ
Step 1 : RUN rm -rf /usr/local/tomcat/webapps/ROOT
 ---> Running in 415983e2d95c
 ---> 851e0dd6ea1e
      ^^^^^^^^^^^^
```
>
> このログ出力と密接に関係しています。

ここに話を戻しましょう。もうお分かりだと思いますが、`docker build`の実行過程で行われることは上記で実験してみたのと同じで`docker commit`でイメージを作成しているだけです。内部的に`Dockerfile`を一行ずつコミットしながらイメージを作成しているわけです。
ここが理解できると、前述の、

> 複数のRUNを書くこともできるし、一つのRUNで`&&`で区切って複数のコマンドを書くことも出来ます。

この意味もわかってくるかと思います。つまりは結果は同じなのだけれど、履歴の残し方が変わってくるわけです。（gitでもどの単位でcommitしていくかを考えますよね？）

### docker history

コミットをしているということは当然履歴を追跡することも可能です。先ほどビルドした

>```
>Successfully built 6ab8fbc5b09d
>```

このイメージの履歴を見てみましょう。

```
$ docker history 6ab8
IMAGE               CREATED             CREATED BY                                      SIZE                         COMMENT
6ab8fbc5b09d        6 hours ago         /bin/sh -c #(nop) COPY file:0a2dfbf69ce2dab06   295.4 kB                     
851e0dd6ea1e        6 hours ago         /bin/sh -c rm -rf /usr/local/tomcat/webapps/R   0 B                          
a48de4915c8d        8 days ago          /bin/sh -c #(nop) CMD ["/bin/sh" "-c" "/usr/l   0 B                          
8d614329fad3        8 days ago          /bin/sh -c #(nop) EXPOSE 80/tcp                 0 B
:                          
```

このイメージを作るために実行されたコマンドをコミット単位で見ることが出来ます。


## DockerHubにイメージを登録してみよう

さて、こうして作成したイメージをリモートリポジトリであるDockerHubに登録してみましょう。

### まずはDockerHubにユーザー登録

https://hub.docker.com/ から登録してください。メールアドレスがあればできるいつものやつです。

### 作ったイメージにタグ名をつける

DockerHubに登録する前に対象のイメージに名前（タグ）をつけてあげる必要があります。これは`docker tag`コマンドを使用します。

```
Usage: docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
```
```shell-session
$ docker tag 50ed3 newgyu/hoge:1.0.0
$ docker images
REPOSITORY                TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
newgyu/hoge               1.0.0               50ed314c079a        11 minutes ago      640.9 MB
```

| 引数       | 意味 |
|------------|----|
|IMAGE       |イメージのIDを指定します|
|REGISTRYHOST|DockerHubにpushする場合は省略します。その他のレジストリにpushする場合はホスト名を指定します|
|USERNAME|DockerHubのユーザー名を指定してください|
|NAME|イメージ名を何か指定してください|
|TAG|通常は``1.0``などのバージョン番号を指定します。省略すると``latest``となります|

### 作ったイメージをpushする

```shell-session
$ docker push newgyu/hoge:1.0.0
The push refers to a repository [newgyu/hoge] (len: 1)
50ed314c079a: Image push failed 

Please login prior to push:     <---- DockerHubにログインしろと言ってきます
Username: newgyu
Password: 
Email: newgyu@mail.com
WARNING: login credentials saved in /home/ec2-user/.docker/config.json
Login Succeeded
The push refers to a repository [newgyu/hoge] (len: 1)
50ed314c079a: Image already exists 
2a17d1069a42: Image successfully pushed 
a48de4915c8d: Image successfully pushed 
```

これでDockerHubにアップロードされたと思いますので、DockerHubのページを確認してみましょう。

https://hub.docker.com/r/ユーザーID/

----

というところで今回は、

* Dockerイメージを作成する
* DockerHubにイメージを登録する

ということをやってみました。