【Docker】docker-composeだけでローリングアップデートを実現する

こんにちは

新年一発目はやはり 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の挙動を勉強できたのでこれ自体はやってよかったです。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA