この記事は
Dockerを使って1台のマシンでBlue-Green-deploymentの環境を作ります。
同時に、Docker習得上で気が付いた点についてもまとめています。
少しnginx, rails, puma特有の内容が混ざります。
なぜBlue-Green-deploymentに行き着いたのか
Dockerが便利だよという話を聞いて色々調べたんですが
本腰入れるならdocker + kubernetesという結論に到達したためです。
中途半端にDockerを極めるくらいならサクッと活用を目指すのが
現状のDockerかなあと思っています。
構成
色々やり方はあると思いますが、今回はこんな構成にしました。
・nginx (proxy)
・rails + puma (app) ×2
これだけです。
インストールしたもの
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最新版イメージに切り替えるだけ。
アプリケーションコンテナには依存ライブラリがないので、あれこれ弄る必要がないです。
イメージをいじくり回している暇があったら別のことをやったほうがいいですね。
手順
ディレクトリ構成
下図のようにディレクトリを切ります。
docker-rails
├── docker-compose.yml
├── nginx
│ └── default.conf
├── testapp01
│ └── (Gemfileのみ必須)
└── testapp02
└── (Gemfileのみ必須)
docker-compose.yml(rails用リハーサル)
bundle installで大体コケるので、一旦testapp01だけ作って試してみます。
cd testapp01
rails new testapp
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でコマンドの実行ディレクトリにします。
cd ../
docker-compose up
docker hubのイメージを利用するので、docker-compose buildは不要です。
最初からupを使います。
なんかエラーが出ると思います。
今回はbundlerのバージョンが違うよというのと、
node.js入れてねというエラーが出ました。
前者は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を作ったら
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側は
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を発行します。
cd testapp01
bundle exec rake secret
secret keyをコピーしておきましょう。すぐに使います。
rails g controller tests
root "tests#show"
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で出力されたキーが入ります。
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コマンドで確認ができます。
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
ヘッダーは理解せずにコピペしたので後で直すかもしれません。
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不要です。
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;
}
docker exec -i -t <コンテナID> /bin/bash
nginx -s reload
この状態でlocalhostの80番へアクセスして、
This is app02.
が表示されていればOKです。
終わりに
これでローカルホストでの構築ができました。
ルートディレクトリのdocker-railsでgit initすれば丸ごとリモート環境へ移植できます。
アプリケーションをバージョンアップする際にはapp02側にリリースを行なって
nginxの接続先を変更すれば内容を変更することができます。
数日動かして問題がないかチェックしてOKであればapp01側にもリリースして
nginxの接続先を01に戻すようにすれば、良い感じのBlue-Green-Deploymentができます。