Dockerでダウンタイムのないデプロイ

webアプリケーションのデプロイを、Dockerを用いてダウンタイムなく行う手順をサーバーのプロビジョニング、アプリの用意、運用方法に分けまとめた。

# はじめに
ECSやGKEとかが、Dockerのクラスタ管理ではデファクトっぽい位置づけになってる気がするが、そういうサービスが使えない場合でかつ、公式ツールを使ってデプロイする場合の方法を調べまとめた。

# 成果物

下記にまとめています。
記事を参照しながら見てください。  
https://github.com/togana/sample-docker-deploy

# 構成

## デプロイのイメージ図

![01.png](https://qiita-image-store.s3.amazonaws.com/0/82745/f92df1d8-34a9-534a-a4f8-c62b45714ec4.png)

## 使用するツール

```sh
Docker version 1.13.1
docker-machine version 0.9.0
docker-compose version 1.11.1
VirtualBox version 5.1.10
```

## 用意する仮想サーバー

サービス用サーバー: 1台
ビルドサーバー: 1台
レジストリサーバー: 1台

# サーバのプロビジョニング

## docker-hostの構築

レジストリサーバー

```sh
$ docker-machine create -d virtualbox container-registry
```

サービス用サーバー

```sh
$ docker-machine create -d virtualbox \
  --engine-insecure-registry $(docker-machine ip container-registry):5000 \
  service
```

ビルドサーバー

```sh
$ docker-machine create -d virtualbox \
  --engine-insecure-registry $(docker-machine ip container-registry):5000 \
  build
```

オプション説明

```text
--engine-insecure-registry : 作成するエンジンが、指定した非安全なレジストリと通信できるようにします。
```

非安全なレジストリとは[こちらを参照](http://docs.docker.jp/engine/reference/commandline/daemon.html#insecure-registries)するといいと思います。
ざっくりいうとTLS通信できればOK。CA証明書を用意してあげればこの設定は必要ないので用意できる方はオプションではなくそちらを使用したほうがいいです。

## レジストリサーバーの構築

レジストリに関する作業ディレクトリは `registry` で行うことにします。

```sh
$ mkdir registry
$ cd registry
```

dockerレジストリはdockerで用意できます。下記の `docker-compose.yml` を用意して実行します。  
ボリュームのマウントをしているので、違う場所に作りたい人は適宜置き換えてください。

```yaml:docker-compose.yml
version: '3'
services: 
  registry:
    image: registry:2.6.0
    ports:
      - 5000:5000
    environment:
      REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /registry
      REGISTRY_STORAGE_DELETE_ENABLED: 'true'
    volumes:
      - /user/local/data/registry:/registry
  web:
    image: hyper/docker-registry-web:v0.1.2
    ports:
      - 8080:8080
    environment:
      REGISTRY_HOST: registry
      REGISTRY_URL: 'http://registry:5000/v2'
      REGISTRY_NAME: 'registry:5000'
      REGISTRY_READONLY: 'false'
```

```sh
# 環境変数をレジストリサーバーに向ける
$ eval $(docker-machine env container-registry)

# registryと管理ツールのコンテナ起動
$ docker-compose up -d
```

## サービス用のサーバーを構築

swarm modeで実行するには[このへん](http://qiita.com/togana/items/d2885e695b666bc1d218)でいろいろ試したので見ると参考になると思います。
今回は動かすだけなので下記のコマンドを実行します。

```sh
# 環境変数をサービスサーバーに向ける
$ eval $(docker-machine env service)

# swarm mode 初期化
$ docker swarm init --advertise-addr $(docker-machine ip service)
```

# アプリの用意

## 提供するサービスのDockerfileを用意する

サービスに関する作業ディレクトリは `nginx-app` で行うことにします。

```sh
$ mkdir nginx-app
$ cd nginx-app
```

今回は、ssiをonにしたnginxコンフィグを用意して `<!--#echo var="hostname" -->` とすることで、hostname=コンテナidなのでコンテナidを表示するだけのwebページを作成します。こうすることでコンテナが切り替わったか確認しやすいコンテナが作成できます。

```text:Dockerfile
FROM nginx:1.11.9-alpine

COPY default.conf /etc/nginx/conf.d/
COPY index.html /usr/share/nginx/html/
```

```text:default.conf
server {
  listen       80;
  server_name  localhost;
  ssi on;

  location / {
    root   /usr/share/nginx/html;
    index  index.html;
  }
}
```

```html:index.html
<!DOCTYPE html> 
<html>
  <body>
    <h1>HostName: <!--#echo var="hostname" --></h1>
  </body>
</html>
```

## 動作確認

下記のようにcurlを実行してhtmlが帰ってきていれば問題なくビルド実行できてる。

```sh
$ eval $(docker-machine env build)
$ docker build -t nginx-app .
$ docker run -dp 80:80 nginx-app
$ curl $(docker-machine ip build)
<!DOCTYPE html>
<html>
  <body>
    <h1>HostName: 18a4af130a5a</h1>
  </body>
</html>
$ docker ps -aq | xargs docker stop | xargs docker rm
18a4af130a5a
```

# 運用方法

## ビルド

### ビルドの実行

先程作ったDockerfileとdefault.confとindex.htmlのあるディレクトリで下記のコマンドを実行します。
今回はタグを(registryのip:ポート番号)/nginx-app:v1.0として作成しています。
nginx-app:v1.0(イメージ名:バージョン)は任意の値を入れることができます。

```sh
# 環境変数をビルドサーバーに向ける
$ eval $(docker-machine env build)

# ビルド実行
$ docker build -t $(docker-machine ip container-registry):5000/nginx-app:v1.0 .

# レジストリサーバーにプッシュ
$ docker push $(docker-machine ip container-registry):5000/nginx-app:v1.0
```

### ビルドコマンドをまとめる

下記の `build.sh` を `sh` ディレクトリに用意する。

```sh
$ mkdir sh
$ cd sh
```

```sh:build.sh
#!/bin/sh
set -eu

tag=${1:-"latest"}

cd $(cd $(dirname $0);pwd)/../nginx-app
docker-machine env build
eval $(docker-machine env build)

docker build -t $(docker-machine ip container-registry):5000/nginx-app:${tag} .
docker push $(docker-machine ip container-registry):5000/nginx-app:${tag}
```

もちろん実行権限を付与する

```sh
$ chmod +x ./build.sh
```

ビルドが下記コマンドでできるようになる。

```sh
# ./build.sh tag
$ ./build.sh v1.0
```

## デプロイ

### デプロイの実行

イメージをレジストリサーバーにプッシュできたので、サービスサーバーで実際に動かします。  
まず下記のような `docker-compose.yml` を用意します。  
`192.168.99.100` は `$(docker-machine ip container-registry)` を実行して表示されてたipに適宜読み替えてください。

```yaml:docker-compose.yml
version: '3'
services:
  web:
    image: 192.168.99.100:5000/nginx-app:v1.0
    ports:
      - '80:80'
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
```

オプション説明

```text
version        : Compose ファイル形式を指定
servises       : 各コンテナをサービスとして定義
web            : 今回実行するコンテナ名(任意の名前に変更可能)
image          : コンテナ実行時の元となるイメージを指定
ports          : ホスト側:コンテナ側 の書式でポートを割り当てる
deploy         : サービスの展開と実行に関連する構成を指定
                 version: 3でのみ指定できる
                 Swarmにデプロイするときのみ有効
                 docker-compose upのときなどは無視される
replicas       : 実行する必要があるコンテナの数を指定
update_config  : サービスの更新方法を設定
parallelism    : 一度に更新するコンテナの数を指定
delay          : 更新するまでの待機時間を指定
restart_policy : コンテナが終了したときに再起動するか設定
condition      : 終了ステータスによって再起動を検討
```

その他オプション

modeオプションをglobalにすることですべてのswarmワーカーに一つづつコンテナを配置することができます
docker stack deployコマンドを使用するときはbuildは使用できない
placementオプションを指定することで配置制約を決めることもできます
もっと知りたければ[公式リファレンス](https://docs.docker.com/compose/compose-file/)を見てください

実際にデプロイします。

```sh
# 環境変数をビルドサーバーに向ける
$ eval $(docker-machine env service)

# デプロイ sample-deployはstack名になるので自由に決めれます
$ docker stack deploy --compose-file docker-compose.yml sample-deploy
```

### デプロイコマンドをまとめる

下記の `deploy.sh` を `sh` ディレクトリに用意する。
毎回docker-compose.ymlを再生成するので `.gitignore` に追加してもいいかもしれない。

```sh:deploy.sh
#!/bin/sh
set -eu

tag=${1:-"latest"}

docker-machine env service
eval $(docker-machine env service)

cat << EOS > docker-compose.yml
version: '3'
services:
  web:
    image: $(docker-machine ip container-registry):5000/nginx-app:${tag}
    ports:
      - '80:80'
    deploy:
      replicas: 2
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
EOS

docker stack deploy --compose-file docker-compose.yml service
```

もちろん実行権限を付与する

```sh
$ chmod +x ./deploy.sh
```

デプロイが下記コマンドでできるようになる。

```sh
# ./deploy.sh tag
$ ./deploy.sh v1.0
```

## 動作確認

下記コマンドを複数回実行するとコンテナIDが表示されるのがわかると思います

```sh
$ curl http://$(docker-machine ip service)
```
## アプリの更新

### v1.1にアップデート

先程作ったindex.htmlに変更を加えます。

```diff:index.html
<!DOCTYPE html> 
<html>
  <body>
    <h1>HostName: <!--#echo var="hostname" --></h1>
+   <h1>version: v1.1</h1>
  </body>
</html>
```

タグを指定してビルドします。

```sh
$ ./build.sh v1.1
```

### v1.1をデプロイ

タグを指定してデプロイを実行します。

```sh
$ ./deploy.sh v1.1
```

デプロイコマンドが終了してもすぐデプロイが終わってるわけではないところに注意が必要です。
実際には裏でローリングアップデートが始まります。

### アップデート時の挙動図解

アップデート前

![02.png](https://qiita-image-store.s3.amazonaws.com/0/82745/11007b50-ab50-01f9-3bfb-d3af4d602a00.png)

デプロイコマンド実行

一つ目のコンテナがstopされ

![03.png](https://qiita-image-store.s3.amazonaws.com/0/82745/0ce8c172-0388-0b02-986d-54a1f5969631.png)

新しいイメージでコンテナ実行

![04.png](https://qiita-image-store.s3.amazonaws.com/0/82745/588dfa65-7bf3-cbf2-91b5-b5e0873ed3cd.png)

10秒待ったのちに2つ目のコンテナがstopされ

![05.png](https://qiita-image-store.s3.amazonaws.com/0/82745/e133f0dd-0f5c-6ab3-d8c2-06ae96ffadfe.png)

新しいイメージでコンテナ実行

![06.png](https://qiita-image-store.s3.amazonaws.com/0/82745/321b4b75-ae65-d01e-a3f0-bb0aeea45eda.png)

こんな感じでローリングアップデートが行われる

# まとめ

ビルド時にタグを付けてレジストリにプッシュ、デプロイ時に使用するイメージのタグを指定することで新しいイメージで動くコンテナをダウンタイムなくデプロイすることができました。  
運用時は、タグの指定をコマンドでできるようにすることで、ビルドのしやすさ、デプロイ時にタグを切り替えることで気軽にロールバック等もできるようになりました。

# 最後に

僕の調べた範囲で書いています。間違ってることなどあれば教えてください!

また、導入目的や目標が定まってない場合、無理にコンテナを導入したりすると炎上不可避なので計画的にコンテナ化しましょう!!

よいコンテナライフを!!!