こんにちは
新年一発目はやはり Docker 関連の記事になります。
タイトルの通り docker-compose のみを利用して、コンテナのローリングアップデート(Dockerイメージの更新)を実現してみたので、ここに書いておきます。
(普通は docker-swarm や k8s を利用すると思います。。。
■構成
※ プラットフォームには docker-desktop を利用しています
構成は下記となり、一旦すべてのリクエストをリバースプロキシ用Nginxコンテナで受けて、web01コンテナへ流し、拡張子が .php の場合は app01コンテナへプロキシするようにしました
また、web01 → app01 へのプロキシでは、Dockerの内部DNS機能である docker-compose.yml のサービス名で名前解決できることを利用して、
app01サービスのコンテナが2台へスケールアウトした場合でも、各コンテナへラウンドロビン形式でリクエストが送信されるようになっています。
リバースプロキシ(nginx) → web01(nginx) → app01
■検証の流れ
1. docker-compose build で各Dockerイメージの作成
2. docker-compose up -d で各コンテナの起動
3. ブラウザから表示確認
4. app01コンテナのソースコードを編集
5. app01のDockerイメージを再ビルド
6. –scale オプションを利用して app01 コンテナをスケールアウト
7. ブラウザから表示確認 (新旧のコンテナが並行起動しているので、それぞれの画面がランダムに表示されることを確認する)
8. 旧 app01 コンテナの停止
9. ブラウザから表示確認
■各種ファイルの説明
ディレクトリ構成
. ├── app │ ├── Dockerfile │ ├── conf │ │ ├── disable_display_error.ini │ │ └── zz-docker.conf │ └── src │ └── index.php ├── docker-compose.yml ├── proxy │ ├── Dockerfile │ └── conf │ └── www.example.com.conf └── web ├── Dockerfile ├── conf │ └── origin.example.com.conf └── src └── index.html
./docker-compose.yml
version: "3.9"
services:
proxy01:
build:
context: ./
dockerfile: proxy/Dockerfile
ports:
- "80:80"
depends_on:
- web01
web01:
build:
context: ./
dockerfile: web/Dockerfile
depends_on:
- app01
app01:
build:
context: ./
dockerfile: app/Dockerfile
restart: always
→ proxy01, web01, app01 のサービスを作成しており、それぞれ専用のDockerファイルからビルドするようにしています
app/Dockerfile
FROM php:8.0.30-fpm
#php.ini用confファイルコピー
COPY ./app/conf/disable_display_error.ini /usr/local/etc/php/conf.d/
#php-fpm用confファイルコピー
COPY ./app/conf/zz-docker.conf /usr/local/etc/php-fpm.d/
RUN groupmod -g 1000 www-data
RUN usermod -u 1000 www-data
#ドキュメントルート作成
RUN apt-get update -y && \
apt-get install -y vim less && \
docker-php-ext-install pdo_mysql && \
mkdir -p /var/www/vhosts/origin.example.com/public_html
COPY ./app/src /var/www/vhosts/origin.example.com/public_html
# ユーザー切り替え
USER www-data
#作業ディレクトリ設定
WORKDIR /var/www/vhosts/origin.example.com/public_html
#ポート開放
EXPOSE 9000
→ ポート9000番で待ち受けます
app/conf/disable_display_error.ini
display_errors = Off log_errors = true
→ あまり意味ないですが、エラーログ出力有効かとブラウザ上にエラーログを表示しないようにしています
app/conf/zz-docker.conf
[global] daemonize = no [www] listen = 9000 pm = dynamic pm.max_children = 50 pm.start_servers = 5 pm.min_spare_servers = 5 pm.max_spare_servers = 35
→ php-fpm チューニング用ファイルです
app/src/index.php
<?php phpinfo();?>
→ phpで処理する用のファイルです
proxy/Dockerfile
FROM nginx:1.25.0 #confファイルのコピー COPY ./proxy/conf/www.example.com.conf /etc/nginx/conf.d EXPOSE 80
→ nginx の公式イメージを利用しており、80番ポートを開放しています
proxy/conf/www.example.com.conf
server {
listen 80;
server_name www.example.com;
index index.php;
access_log /var/log/nginx/www.example.com_access.log main;
error_log /var/log/nginx/www.example.com_error.log;
resolver 127.0.0.11 valid=5s;
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 ~ /(.*) {
set $upstream_web web01;
proxy_pass http://$upstream_web:8080/$1$is_args$args;
}
}
→ 一番最初のリクエストを受けるために、DNSで名前解決する用のドメインを設定します
また、resolver でdockerの内部DNSを明示的に参照するようにしており、ttlの有効期限も5秒に設定しています
加えて、/ 配下へのリクエストはすべて docker-compose.ymlに記載している web01 サービスへ流すようにしており、set を利用して変数化しています
※この値の詳細は後述します
web/Dockerfile
FROM nginx:1.25.0 #confファイルのコピー COPY ./web/conf/origin.example.com.conf /etc/nginx/conf.d #ドキュメントルート作成 RUN mkdir -p /var/www/vhosts/origin.example.com/public_html COPY ./web/src /var/www/vhosts/origin.example.com/public_html EXPOSE 8080
→ こちらも公式のnginxイメージを利用しており、ドキュメントルートの作成や 8080ポートの開放を実施しています
※8080ポートは 80 ポートでも問題ございません
web/conf/origin.example.com.conf
server {
listen 8080;
server_name origin.example.com;
root /var/www/vhosts/origin.example.com/public_html;
index index.html;
access_log /var/log/nginx/origin.example.com_access.log main;
error_log /var/log/nginx/origin.example.com_error.log;
resolver 127.0.0.11 valid=5s;
location / {
try_files $uri $uri/ /index.php?$query_string;
index index.html;
}
location ~ \.php$ {
set $upstream_app app01;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass $upstream_app:9000;
}
}
→ proxy01サービスから流れてくるリクエストを受け取ります
こちらでも proxy01 と同様に resolver と set を利用しています
また、リクエストが / の場合は、通常の index.html ファイルの内容を表示し、.php の拡張子の場合は app01 へリクエストをプロキシしています
app/src/index.html
origin.example.com
→ 通常リクエストで表示する index.html ファイルです
■nginxの resolver と set について
Nginx でproxy_passなどを利用している場合、起動時にドメインの名前解決を行い、そのIPアドレスを再起動時までキャッシュします。
そのため、起動中の間に名前解決したドメインのIPアドレスが変更されると、変更前のIPアドレスがキャッシュとして保持しているので、そのまま旧IPアドレスへ接続しに行きます。
ローリングアップデートにおいて、proxy01 or web01 コンテナからスケールアウトされたコンテナへ同一ドメイン(今回はdocker-composeのサービス名)でリクエストを送信する必要があり、
リクエストの度に名前解決を行うようにしないと、リクエストが旧コンテナへのみ送られ続けるようになります。
なので、今回は当該事象を回避するためnginx用confの resolver にてdockerの内部DNSを明示的に参照し、
TTLを5秒で設定する + set パラメータを利用してサービス名を変数化することによって、おおよそすべてのリクエスト毎に名前解決するようにしています。
なお、set パラメータは必須であり、resolver のみでは動作しないので悪しからず
■検証実施
1. docker-compose build で各Dockerイメージの作成
docker-compose.ymlファイルが存在するディレクトリ上でイメージを作成します
docker-compose build --no-cache
2. docker-compose up -d で各コンテナの起動
docker-compose up -d
3. ブラウザから表示確認
※docker-desktopを利用してるのでローカルで名前解決はしてください
http://www.example.com/
→ origin.example.com が表示されることを確認
http://www.example.com/index.php
→ phpinfo の画面が表示されることを確認
4. app01コンテナのソースコードを編集
それではてきとうにphpのコードを下記へ変更します
<?php //phpinfo();?> <?php echo "testtest";?> <?php echo "testtest";?>
→ php で testtest の文言を出力するようにしています
5. app01のDockerイメージを再ビルド
docker-compose build app01
6. –scale オプションを利用して app01 コンテナをスケールアウト
docker-compose up -d --scale app01=2 --no-recreate
→ app01 サービスコンテナを2台とし、–no-recreate ですでに起動しているコンテナは再作成しないようにしています
docker container ps -a
→ app01サービスのコンテナが2台起動していることを確認
7. ブラウザから表示確認 (新旧のコンテナが並行起動しているので、それぞれの画面がランダムに表示されることを確認する)
http://www.example.com/index.php
→ 何度かアクセスし、phpinfo の画面と testtest の文言が出力されている画面がランダムに表示されていればOKです
8. 旧 app01 コンテナの停止
docker stop ${旧コンテナID}
→ このタイミングで phpinfo の画面は表示されません
なお、docker stop コマンドはメインプロセスが SIGTERM を受信し、デフォルトでは10秒後に SIGKILL を送信するようになっています
9. ブラウザから表示確認
http://www.example.com/index.php
→ testtest の文言が出力されている画面のみが表示されていればOKです
■まとめ
いかがでしたでしょうか。
docker-composeのみで完璧とはいかないまでも、ダウンタイムなしでローリングアップデートを実施できました。
ただ、正直 ここまでするなら素直にdocker-swarmなどを利用して、Docker標準の機能でより安全にローリングアップデートを実施したほうがいいと感じました。
おそらく実務ではほとんど利用しないとは思いますが、改めてコンテナ間の通信や内部DNSの挙動を勉強できたのでこれ自体はやってよかったです。