知識0からのDocker学習＆活用 ~Blue-Green-Deployment~

#この記事は
Dockerを使って1台のマシンでBlue-Green-deploymentの環境を作ります。
同時に、Docker習得上で気が付いた点についてもまとめています。

少しnginx, rails, puma特有の内容が混ざります。

##なぜBlue-Green-deploymentに行き着いたのか
Dockerが便利だよという話を聞いて色々調べたんですが
本腰入れるならdocker + kubernetesという結論に到達したためです。
中途半端にDockerを極めるくらいならサクッと活用を目指すのが
現状のDockerかなあと思っています。

##構成
色々やり方はあると思いますが、今回はこんな構成にしました。

・nginx (proxy)
・rails + puma (app)　×２
これだけです。

##インストールしたもの
Docker 19.03.2
Docker-compose 1.24.1
インストール方法は省略。OSとバージョン依存なので私も多分また公式見ます。

#Dockerを使ってみての勘所
##最初からリモートで動かそうとするのはダメ
オーケストレーションツールとの連携が印象的で、最初から運用環境での実行を考えていました。
が、Dockerは試行錯誤してうまくいくコマンドだけを整理して書いていく必要があります。

もしコンテナの立ち上げ中にエラーを出してもatatchやexecで途中から進めることは可能ですが、
コンテナを使い回すためには綺麗に書き直されたDockerfileやdocker-compose.ymlが必要です。
ですからまずは何度もやり直しができるローカル環境から始めるべきだと思います。
これに気がつくのに2日かかりました。

##OSイメージはubuntuで
Docker hubの公式レポジトリでOS名の記載がないものはubuntuになっています。
リモートホストがcentOSなので、centOSの方がいいのかなと思いましたけど、
パッケージ管理コマンドをapt系に変更する以外影響はなく、問題なく動きます。
参考になるイメージも大体ubuntuベースなので、ubuntuが良いです。

軽量化にこだわってalpine linuxを使うという記事も多くありますが、
alpineになるとディレクトリをバインドしてホストOS側の機能を使う際に互換性がないです。
そうなるとかなり独自にエラーを吐きながらDockerfileを構築することになります。

これも気がつくのに2日かかりました。

##Dockerfileとdocker-composeの使い分け
使い分けというか、さっくり使うレベルなら
極論ですがなるべくDockerfileは使わなくていいという結論に至りました。
というのはDocker-composeにできてDockerfileにできないことは多いのですが、
Dockerfileにできてdocker-composeにできないことがさほどないからです。

どちらも実際にやっていることは順番にdockerコマンドを呼び出しているだけです。

docker-composeからは
コンテナ間ネットワークの設定
コンテナ間での共通ディレクトリのバインド
といったコマンドが使えますが、Dockerfileにはありません。

Dockerfileにしかできないことは、シェルで実行するコマンドを複数書くことですが、
数行のコマンドであれば && で括ってしまえばdocker-composeでもできます。

なのでDockerfileに関しては全く触れません！
多分今後もあまりDockerfileは使わないでしょう！

##なんなら自分でイメージを編集しない方が良い
暴論ですが、イメージを自分で作っているとDockerのいいところが失われていきます。
ホストOSのカーネルや他のコンテナと独立して環境を作れるのがDockerのいいところです。
例えばnginxのバージョンを上げようと思ったら、nginx最新版イメージに切り替えるだけ。
アプリケーションコンテナには依存ライブラリがないので、あれこれ弄る必要がないです。

イメージをいじくり回している暇があったら別のことをやったほうがいいですね。

#手順
##ディレクトリ構成
下図のようにディレクトリを切ります。

```:HostOS
docker-rails
├── docker-compose.yml
├── nginx
│   └── default.conf
├── testapp01
│   └── (Gemfileのみ必須)
└── testapp02
    └── (Gemfileのみ必須)
```
##docker-compose.yml(rails用リハーサル)
bundle installで大体コケるので、一旦testapp01だけ作って試してみます。

```:command
cd testapp01
rails new testapp
```

```docker-compose.yml
version: '3'
services:
  app01:
    image: "ruby:2.6"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"
```
HostOSのtestapp01をコンテナの/appにバインドし、
working_dirでコマンドの実行ディレクトリにします。

```:command
cd ../
docker-compose up
```
docker hubのイメージを利用するので、docker-compose buildは不要です。
最初からupを使います。

なんかエラーが出ると思います。
今回はbundlerのバージョンが違うよというのと、
node.js入れてねというエラーが出ました。

前者はdocker-compose.ymlのコマンドを追加して解決します。

```docker-compose.yml
version: '3'
services:
  app01:
    image: "ruby:2.6"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"
```

後者はnode.js依存のgemをGemfileから削除して回避しました。
後で別途nodeのコンテナを立てて、webpacker使おうと思っているので。
turbolink使う方とかwebpackerの方はnode.jsをインストールするコマンドを追加しましょう。
もしかすると行が長くなってくるので、この辺で修正を多く入れる場合は
ちゃんとDockerfileを作った方がいいカモです。

その場合は、例えば./testapp01/DockerfileにDockerfileを作ったら

```docker-compose.yml
version: '3'
services:
  app01:
    build: "./testapp01/Dockerfile"
    ports:
      - "3000:3000"
    volumes:
      - ./testapp01:/app
    working_dir: /app
    command: >
      bash -c "bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"
```
という感じになるはずです。
Dockerfile側は

```:./testapp01/Dockerfile
FROM ruby:2.6
RUN (some command necessary) &&\
    (another command necessary)
```
依存パッケージのインストールに必要なコマンドを入れてください。

なお、Dockerfileでbundle installを書いてもエラーを起こして停止します。
docker-composeのbuildでDockerfileのコマンドが呼び出され、完了後に次に進むので
Dockerfileのコマンド実行中はまだホストOSのディレクトリがバインドされておらず、
Gemfileが見つからないためです。

docker-compose upでコケなくなったら次に進みます。

##railsアプリの準備
※rails固有の話なので関係のない人は飛ばしてください。

プロダクション環境で動くようにsecret_keyを発行します。

```:command
cd testapp01
bundle exec rake secret
```
secret keyをコピーしておきましょう。すぐに使います。 

```:command
rails g controller tests
```

```config/routes.rb
root "tests#show"
```

```app/controller/tests_controller.rb
def show
  render plain: "This is app01."
end
```

app02も同じことを行います。app02は"This is app02."にしておいてくださいね。
当たり前ですがここを"This is app01."にすると後で切り替えの確認で困ります。

##docker-compose.yml(本番)
###ソース
SECRET_KEY_BASE: "your_key_base"の部分はrake secretで出力されたキーが入ります。

```docker-compose.yml
version: '3'
services:
  webserver:
    image: nginx
    ports:
      - "80:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
  app01:
    image: "ruby:2.6"
    expose:
      - "3000"
    volumes:
      - ./testapp01:/app
    environment:
      VIRTUAL_HOST: localhost
      SECRET_KEY_BASE: "your_key_base"
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"
  app02:
    image: "ruby:2.6"
    expose:
      - "3000"
    volumes:
      - ./testapp02:/app
    environment:
      VIRTUAL_HOST: localhost
      SECRET_KEY_BASE: "your_key_base"
    working_dir: /app
    command: >
      bash -c "gem install bundler:2.0.1 && bundle install && bundle exec puma -t 5:5 -e production -C /app/config/puma.rb -p 3000"

networks:
  default:
    driver: bridge
```

###コロンに注意
yaml文法のコロンとdocker-compose文法のコロンに要注意です。
yaml文法はコロンの後半角スペースが必要です。

```
    image: nginx
```
一方でdocker-compose文法のコロンは半角スペースを入れるとエラーになります。

```
    volumes:
      - ./testapp01:/app
```

###expose vs ports
exposeとportsはどちらもポート解放に関する設定ですが、portsの方が色々できます。
こちらを参考にしました。
https://tkzo.jp/blog/difference-between-ports-and-expose-in-docker-compose/#ports_8211-3

今回の場合、appはproxyからしかアクセスしません。ですからexposeを使っています。
一応、ローカルで見る分には直接アクセスもできた方が便利かもしれません。
その場合はローカルホストからのみホストの13000番から直接アクセスできるようにして

```
    ports:
      - "127.0.0.1:13000:3000
```
としても良いかもしれません。
一方でproxyはホストOSの80番からproxyの80番へ接続させるためportsを使います。

```
    ports:
      - "80:80"
```

###network
まず、dockerは初期状態で3つのネットワークを立ち上げています。
これは次のようにdockerコマンドで確認ができます。

```:command
docker network ls

NETWORK ID          NAME                   DRIVER              SCOPE
e0e37d582a18        bridge                 bridge              local
1ac914f6e912        host                   host                local
91508af4a972        none                   null                local
```

ネットワークにはID, 名前, ドライバがあります。
hostはホストOSを含むネットワーク、
bridgeはホストOSとは切り離されたコンテナ間を包括するネットワークです。

noneは単一コンテナのみのネットワークだそうです、（ネットワークとは一体？）
あくまで実装としてのネットワークってことでしょうが、ちょっと置いておきましょう。

今回はproxyだけがホストOSを通じてリクエストを受け取ることができれば良いので
bridgeを使っています。

なお、既存のネットワークを利用する場合には

```
networks:
  default:
    external:
      name: bridge
```
のようにしてもOKです。

##nginx/default.conf
ヘッダーは理解せずにコピペしたので後で直すかもしれません。

```nginx/default.conf
server {
    listen          80;

    server_name		localhost
    proxy_set_header    Host    $host;
    proxy_set_header    X-Real-IP    $remote_addr;
    proxy_set_header    X-Forwarded-Host      $host;
    proxy_set_header    X-Forwarded-Server    $host;
    proxy_set_header    X-Forwarded-For    $proxy_add_x_forwarded_for;

    location / {
        proxy_pass    http://app01:3000;
#        proxy_pass    http://app02:3000;
    }

    access_log  /var/log/nginx/access.log;
    error_log   /var/log/nginx/error.log;   
}
```
docker-composeで指定したapp01, app02が
ネットワーク内でホスト名として使えるようです。この関係性が便利ですね！

ちなみにですが、nginx/default.confが読み込まれるproxyの環境からみて
localhostはproxy自身になるようで、ホストOSを指すものではありません。
ですので

```
    location / {
        proxy_pass    http://localhost:3000;
    }
```
とか

```
    location / {
        proxy_pass    http://127.0.0.1:3000;
    }
```
とか

```
    location / {
        proxy_pass    http://0.0.0.0:3000;
    }
```
とか書いて、ホストOSの3000番ポートがappに繋がっていたとしても
appに繋がらず、かといってproxyも3000番は閉じているのでnginxがエラーになります。


##コンテナ立ち上げ
ここもimageのみなのでbuild不要です。

```:command
docker-compose up
```
途中でDockerfileを書いた人だけ先にbuildしてupしてください。

#動作確認
##初期状態
ブラウザでlocalhostの80番へアクセスして、
This is app01.
が表示されていればOKです。

##接続先切り替え
nginx/default.confのコメントアウトを切り替えます。
その後でproxyのシェルを1つ起動して、nginxをリロードします。

```
    location / {
#        proxy_pass    http://app01:3000;
        proxy_pass    http://app02:3000;
    }
```

```command
docker exec -i -t <コンテナID> /bin/bash

nginx -s reload
```


この状態でlocalhostの80番へアクセスして、
This is app02.
が表示されていればOKです。

#終わりに
これでローカルホストでの構築ができました。
ルートディレクトリのdocker-railsでgit initすれば丸ごとリモート環境へ移植できます。

アプリケーションをバージョンアップする際にはapp0２側にリリースを行なって
nginxの接続先を変更すれば内容を変更することができます。
数日動かして問題がないかチェックしてOKであればapp01側にもリリースして
nginxの接続先を01に戻すようにすれば、良い感じのBlue-Green-Deploymentができます。