出来上がった物を先に見たいという方はこちらをどうぞ。
きっかけ
最近いろんなアプリケーションをdockernizeすることがふえており、そんな中でてきた課題が「どうやってダウンタイムなしでデプロイしようか」というもの。 もしAWS環境ならCodeDeployを活用すれば良さそうな話だが、自分が今扱っているアプリケーションはわけありでAWSなどは活用できず、dockerだけでどうにかする必要があった。
パッと思いたのは「nginxのコンテナを前段に立ててデプロイの度にupstreamを書き換える」という方法。 blueコンテナとgreenコンテナが記載されたupstream.confをボリュームマウントさせてnginxコンテナを起動しておき、デプロイのときにそれぞれをdown/upさせながらデプロイさせるというもの。 軽くググってみても似たことを試している事例は結構あった。
でも都度ファイルを書き換える事をしていると、リポジトリでのバージョン管理がとてもやりにくくなるのでupstream.confを書き換えなくてもblue/greenデプロイできるようないい方法無いかな〜と模索していた。 そんななか見つけたのがngx_dynamic_upstreamである。
ngx_dynamic_upstreamとは?
GitHub - cubicdaiya/ngx_dynamic_upstream: Dynamic upstream for nginx
nginxモジュールの一つで、upstreamの動的な更新を可能とする。 APIはHTTPで提供されており、curlコマンドなどでupstreamに登録されたサーバの一覧をとったり、down/upさせたり、サーバの追加/削除などが可能になる。
また、nginxを起動するときは初期値としてupstreamの定義が必要だが、nginxを起動した後であれば、API経由でupstreamを更新してもconfigファイル自体の書き換えは行われない。 おそらくオンメモリでupstreamの情報を持っており、それだけを更新しているのだろう。
今回の検証環境
ざっとこんな構成にしてみる。
- ロードバランサ: Ubuntu 20.04のイメージ + nginx + ngx_dynamic_upstream
- アプリケーション(blue/green): docker/getting-started
今回試したいのはロードバランサの部分なので、アプリケーション部分は適当にdocker runするだけでも使えそうなものとしてdocker/getting-startedを採用した。 ちなみにdocker/getting-startedとはdockerのチュートリアルを見れるアプリケーションである。 詳しくは割愛するので、気になる人は各自GitHubをのぞくなりコンテナを起動するなりしてくだされ。
ロードバランサをビルドするためのDockerfile
ngx_dynamic_upstreamはリポジトリをcloneしてソースビルドでインストールする必要がある。
ngx_dynamic_upstream の開発者である cubicdaiyaさんが便利なビルドツールnginx-buildを提供していたので使ってみることにした。
ubuntu:20.04
をベースイメージとしてその中でビルドした。
作成したDockerfileはこちら:https://github.com/rsym/docker-bg-deployment/blob/main/Dockerfile
ビルドの流れはこんな感じ。
- 必要なパッケージをインストール
- nginx-buildをgo getで入手するためにgolangをインストールする
- ngx_dynamic_upstreamをgit cloneするためにgitをインストールする
- その他、nginx-buildには必要パッケージをよしなにインストールしてくれる機能があるが、それだけでは入らなかったものがあったので個別に入れることにした
- nginx-buildを実行
- 指定するオプションは以下
nginx:1.21.6
の公式コンテナで採用されているオプション(docker run --rm -it nginx:1.21.6 nginx -V
で確認できる)- ngx_dymanic_upstreamを追加するためのオプション
--add-module=/path/to/ngx_dynamic_upstream
- 指定するオプションは以下
- make installを実行
- 必要なユーザやディレクトリを作成
2.
で指定したオプションに合わせて必要なもの作成する
- upstreamを定義したnginx.confをイメージへCOPY
- 具体的にはこれ:https://github.com/rsym/docker-bg-deployment/blob/main/nginx/nginx.conf
- upstreamには
app-blue up
とapp-green down
と書いておく
- EXPOSE書いたりCMD書いたり
blue/greenデプロイをしてみる
deploy.sh
というデプロイスクリプトを実行するだけでblue/greenデプロイをしてくれるようにした。
ちなみにコンテナがどれも起動していないときはloadbalancerとblueコンテナを起動してくれるようにしている。
$ docker-compose ps NAME COMMAND SERVICE STATUS PORTS app-blue "/docker-entrypoint.…" app-blue running 80/tcp loadbalancer "nginx -g 'daemon of…" loadbalancer running 0.0.0.0:80->80/tcp, :::80->80/tcp
http://localhost
にアクセスするとapp-blueに流れていることがわかる。
$ docker-compose logs -f ... app-blue | 172.18.0.4 - - [16/Mar/2022:11:14:52 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-" app-blue | 172.18.0.4 - - [16/Mar/2022:11:14:53 +0000] "GET /assets/images/favicon.png HTTP/1.0" 200 521 "http://localhost/tutorial/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"
この状態でdeploy.sh
を実行するとapp-greenに切り替えてくれる。
「app-green起動」→「upstream書き換え(app-green up & app-green down)」→「app-blue停止」という流れでデプロイしている。
$ sh deploy.sh [+] Running 1/1 ⠿ app-green Pulled 2.4s [+] Running 1/1 ⠿ Container app-green Started 0.6s server 172.18.0.2:80 weight=1 max_fails=1 fail_timeout=10; server 172.18.0.3:80 weight=1 max_fails=1 fail_timeout=10; server 172.18.0.2:80 weight=1 max_fails=1 fail_timeout=10 down; server 172.18.0.3:80 weight=1 max_fails=1 fail_timeout=10; [+] Running 1/1 ⠿ Container app-blue Stopped 0.2s Going to remove app-blue [+] Running 1/0 ⠿ Container app-blue Removed 0.0s deployment is completed! ===== current upstream ===== server 172.18.0.2:80 down; server 172.18.0.3:80; ===== current service ===== NAME COMMAND SERVICE STATUS PORTS app-green "/docker-entrypoint.…" app-green running 80/tcp loadbalancer "nginx -g 'daemon of…" loadbalancer running 0.0.0.0:80->80/tcp, :::80->80/tcp
切り替え中のdocker-composeのログはこんな感じ。
app-green | 2022/03/16 11:17:18 [notice] 1#1: using the "epoll" event method app-green | 2022/03/16 11:17:18 [notice] 1#1: nginx/1.21.6 app-green | 2022/03/16 11:17:18 [notice] 1#1: built by gcc 10.3.1 20211027 (Alpine 10.3.1_git20211027) app-green | 2022/03/16 11:17:18 [notice] 1#1: OS: Linux 4.18.0-348.el8.0.2.x86_64 app-green | 2022/03/16 11:17:18 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 app-green | 2022/03/16 11:17:18 [notice] 1#1: start worker processes app-green | 2022/03/16 11:17:18 [notice] 1#1: start worker process 32 app-green | 2022/03/16 11:17:18 [notice] 1#1: start worker process 33 app-green | 2022/03/16 11:17:18 [notice] 1#1: start worker process 34 app-green | 2022/03/16 11:17:18 [notice] 1#1: start worker process 35 app-blue | 2022/03/16 11:17:19 [notice] 1#1: signal 3 (SIGQUIT) received, shutting down app-blue | 2022/03/16 11:17:19 [notice] 32#32: gracefully shutting down app-blue | 2022/03/16 11:17:19 [notice] 32#32: exiting app-blue | 2022/03/16 11:17:19 [notice] 33#33: gracefully shutting down app-blue | 2022/03/16 11:17:19 [notice] 33#33: exiting app-blue | 2022/03/16 11:17:19 [notice] 34#34: gracefully shutting down app-blue | 2022/03/16 11:17:19 [notice] 32#32: exit app-blue | 2022/03/16 11:17:19 [notice] 34#34: exiting app-blue | 2022/03/16 11:17:19 [notice] 33#33: exit app-blue | 2022/03/16 11:17:19 [notice] 35#35: gracefully shutting down app-blue | 2022/03/16 11:17:19 [notice] 35#35: exiting app-blue | 2022/03/16 11:17:19 [notice] 34#34: exit app-blue | 2022/03/16 11:17:19 [notice] 35#35: exit app-blue | 2022/03/16 11:17:19 [notice] 1#1: signal 17 (SIGCHLD) received from 34 app-blue | 2022/03/16 11:17:19 [notice] 1#1: worker process 33 exited with code 0 app-blue | 2022/03/16 11:17:19 [notice] 1#1: worker process 34 exited with code 0 app-blue | 2022/03/16 11:17:19 [notice] 1#1: signal 29 (SIGIO) received app-blue | 2022/03/16 11:17:19 [notice] 1#1: signal 17 (SIGCHLD) received from 32 app-blue | 2022/03/16 11:17:19 [notice] 1#1: worker process 32 exited with code 0 app-blue | 2022/03/16 11:17:19 [notice] 1#1: worker process 35 exited with code 0 app-blue | 2022/03/16 11:17:19 [notice] 1#1: exit
再びhttp://localhost
にアクセスするとapp-greenへ流れていることがわかる。
app-green | 172.18.0.4 - - [16/Mar/2022:11:21:01 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-" app-green | 172.18.0.4 - - [16/Mar/2022:11:21:08 +0000] "GET /tutorial/ HTTP/1.0" 304 0 "http://localhost/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.74 Safari/537.36" "-"
ちなみにコンテナ内のnginx.conf
に記述したupstreamはコンテナ起動時のまま。
でもちゃんとapp-greenにリクエストが流れてくれる。
$ docker exec -it loadbalancer cat /etc/nginx/nginx.conf ... upstream backends { zone zone_for_backends 128k; server app-blue; server app-green down; } } $ curl "http://127.0.0.1/dynamic?upstream=zone_for_backends" server 172.18.0.2:80 down; ←app-blue server 172.18.0.3:80; ←app-green
以後deploy.sh
を実行するたびにapp-blue→app-green→...となってくれる。
工夫の余地はあるかもしれないが、ngx_dynamic_upstreamを用いてblue/greenデプロイを実現することはできそう。