Docker Compose を使って複数コンテナを起動するときに,タイミングによっては接続エラーになってしまう場合がある.具体的な例としては「データベースコンテナ」の起動が遅いために「アプリケーションコンテナ」が接続エラーになってしまうことが挙げられる.
depends_on
とは
Docker Compose には depends_on
という設定項目があり,複数コンテナの「起動順序」を制御できるようになっている.しかし,あくまでこれは「起動順序」となり,必ずしも「準備完了」を保証するものではない.ドキュメントにも「開始 (start)」と「準備完了 (ready)」は異なると書いてあり,depends_on
を使っても接続エラーは発生する可能性がある.機能不足と考えるのではなく,アプリケーション側でリトライ機構を用意するなど,レジリエンス(回復性)を考慮する必要がある.
解決策になるツール
同じドキュメントを読むと depends_on
と組み合わせて使う解決策として,アプリケーション側で接続確認をするべきと書いてある.具体的には以下の「4種類」のツールが紹介されている.RelayAndContainers は比較的新しく今年8月頃にドキュメントに追加されていた.
- GitHub - vishnubob/wait-for-it: Pure bash script to test and wait on the availability of a TCP host and port
- GitHub - jwilder/dockerize: Utility to simplify running applications in docker containers
- GitHub - eficode/wait-for: ./wait-for is a script to wait for another service to become available.
- GitHub - jasonsychau/RelayAndContainers: This is a utility to order (Docker) containers in orchestration
その中でも今回は「dockerize」を試す.「dockerize」には大きく「3種類」の機能があり,今回は3番目の「準備完了を待つ機能」を試す.なお,用語としてコンテナ化することを「Dockerized」と言うこともあり,個人的には「dockerize」という名前が認識しにくかったりする.
- ✅ 機能 1 : 環境変数を埋め込んだ設定ファイルを自動生成するテンプレート機能
- ✅ 機能 2 : 複数ログファイルを標準出力と標準エラーに流す機能
- ✅ 機能 3 : メインプロセスを実行する前に TCP / HTTP / HTTPS / Unix Socket を使って「準備完了」を待つ機能
検証環境
今回は「dockerize」を試すために簡単な検証環境を構築した.大きく「3種類」のコンテナがある.App と API と MySQL となり,構成図を載せておく.Python で実装した App では requests
を使って API に HTTP リクエストをする.さらに mysql-connector-python
を使って MySQL に接続する.API は Ruby の Sinatra で実装した.
次に docker-compose.yml
は以下のようになる. App から API と MySQL に depends_on
を設定している.
version: '3.8' services: app: build: ./app/. depends_on: - api - mysql command: [ 'python', '/app/app.py' ] api: build: ./api/. mysql: image: mysql:8.0 environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
そして App のサンプル実装は以下とした.API に HTTP リクエストをしたら ✅ API
と表示する.MySQL に接続できたら ✅ MySQL
と表示する.プロダクションで使うなら backoff
など,Python ライブラリでリトライ機構も用意しておくと良さそう.
import mysql.connector import requests response = requests.get('http://api') if response.status_code == requests.codes.ok: print('✅ API') conn = mysql.connector.connect( host='mysql', user='root' ) if conn.is_connected(): print('✅ MySQL')
「dockerize」を使わずに Docker Compose で App を実行すると,以下のように「Connection refused」で接続エラーになる.今回はこの検証環境に「dockerize」を追加して改善していく.
app_1 | Traceback (most recent call last): app_1 | ConnectionRefusedError: [Errno 111] Connection refused (中略) app_1 | Traceback (most recent call last): app_1 | mysql.connector.errors.InterfaceError: 2003: Can't connect to MySQL server on 'mysql:3306' (111 Connection refused)
dockerize をインストール
「dockerize」は GitHub Releases に .tar.gz
で公開されている.Dockerfile
の中で .tar.gz
を展開して使うこともできるし,Apline Linux ならインストール済の Docker イメージも jwilder/dockerize
として公開されている.
今回は以下のように Dockerfile
の中で .tar.gz
を展開して,dockerize
コマンドを使えるようにしている.
FROM python:3.8-alpine RUN apk add --no-cache openssl ENV DOCKERIZE_VERSION v0.6.1 RUN wget https://github.com/jwilder/dockerize/releases/download/${DOCKERIZE_VERSION}/dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz \ && rm dockerize-alpine-linux-amd64-${DOCKERIZE_VERSION}.tar.gz COPY . /app RUN pip install -r /app/requirements.txt CMD ["python", "/app/app.py"]
docker-compose.yml
に dockerize を追加する
「dockerize」は,基本的に docker-compose.yml
で entrypoint
もしくは command
をラップして使う.Docker Compose のドキュメントにも書いてある通り,docker-compose.yml
で entrypoint
を指定すると,Dockerfile 側の CMD
は無視されるため,最終的に以下のように書いた.entrypoint
に指定している dockerize
コマンドに以下のオプションを追加している.
--timeout 20s
: タイムアウト設定(デフォルトは10秒)--wait http://api
: API への HTTP 接続を待機する--wait tcp://mysql:3306
: MySQL への TCP 接続を待機する
version: '3.8' services: app: build: ./app/. depends_on: - api - mysql entrypoint: [ 'dockerize', '--timeout', '20s', '--wait', 'http://api', '--wait', 'tcp://mysql:3306' ] command: [ 'python', '/app/app.py' ] api: build: ./api/. mysql: image: mysql:8.0 environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
dockerize を使って App を実行する
今度は「dockerize」を使って Docker Compose で App を実行すると,以下のようになる.追加した「dockerize」によって,API と MySQL の「準備完了」を待てるようになった.便利!
app_1 | 2020/11/10 01:00:00 Waiting for: http://api app_1 | 2020/11/10 01:00:00 Waiting for: tcp://mysql:3306 (中略) app_1 | 2020/11/10 01:00:00 Received 200 from http://api app_1 | 2020/11/10 01:00:01 Connected to tcp://mysql:3306 (中略) app_1 | ✅ API app_1 | ✅ MySQL app_1 | 2020/11/10 01:00:02 Command finished successfully.
まとめ
Docker Compose の depends_on
では「準備完了」を保証できない点を解決するツールとして「dockerize」を試した.「dockerize」には大きく「3種類」の機能があり,今回は3番目の「準備完了を待つ機能」を試した.App から API と MySQL の「準備完了」を待てるようになった.今後同じような課題に遭遇したら検討しようと思う.とは言え,あくまで重要なのは「アプリケーション側でリトライ機構を用意すること」なので,忘れないように!なお,検証環境の設定などは全て GitHub に置いておいた.