kakakakakku blog

Weekly Tech Blog : Keep on Learning 👍

Docker Compose で複数コンテナの準備完了を TCP / HTTP で待機できる「dockerize」を試した

Docker Compose を使って複数コンテナを起動するときに,タイミングによっては接続エラーになってしまう場合がある.具体的な例としては「データベースコンテナ」の起動が遅いために「アプリケーションコンテナ」が接続エラーになってしまうことが挙げられる.

depends_on とは

Docker Compose には depends_on という設定項目があり,複数コンテナの「起動順序」を制御できるようになっている.しかし,あくまでこれは「起動順序」となり,必ずしも「準備完了」を保証するものではない.ドキュメントにも「開始 (start)」「準備完了 (ready)」は異なると書いてあり,depends_on を使っても接続エラーは発生する可能性がある.機能不足と考えるのではなく,アプリケーション側でリトライ機構を用意するなど,レジリエンス(回復性)を考慮する必要がある.

docs.docker.com

解決策になるツール

同じドキュメントを読むと depends_on と組み合わせて使う解決策として,アプリケーション側で接続確認をするべきと書いてある.具体的には以下の「4種類」のツールが紹介されている.RelayAndContainers は比較的新しく今年8月頃にドキュメントに追加されていた.

docs.docker.com

その中でも今回は「dockerize」を試す.「dockerize」には大きく「3種類」の機能があり,今回は3番目の「準備完了を待つ機能」を試す.なお,用語としてコンテナ化することを「Dockerized」と言うこともあり,個人的には「dockerize」という名前が認識しにくかったりする.

  • ✅ 機能 1 : 環境変数を埋め込んだ設定ファイルを自動生成するテンプレート機能
  • ✅ 機能 2 : 複数ログファイルを標準出力と標準エラーに流す機能
  • ✅ 機能 3 : メインプロセスを実行する前に TCP / HTTP / HTTPS / Unix Socket を使って「準備完了」を待つ機能

検証環境

今回は「dockerize」を試すために簡単な検証環境を構築した.大きく「3種類」のコンテナがある.AppAPIMySQL となり,構成図を載せておく.Python で実装した App では requests を使って API に HTTP リクエストをする.さらに mysql-connector-python を使って MySQL に接続する.API は Ruby の Sinatra で実装した.

f:id:kakku22:20201111110357p:plain

次に docker-compose.yml は以下のようになる. App から APIMySQLdepends_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 ComposeApp を実行すると,以下のように「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.ymlentrypoint もしくは command をラップして使う.Docker Compose のドキュメントにも書いてある通り,docker-compose.ymlentrypoint を指定すると,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 ComposeApp を実行すると,以下のようになる.追加した「dockerize」によって,APIMySQL「準備完了」を待てるようになった.便利!

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 Composedepends_on では「準備完了」を保証できない点を解決するツールとして「dockerize」を試した.「dockerize」には大きく「3種類」の機能があり,今回は3番目の「準備完了を待つ機能」を試した.App から APIMySQL「準備完了」を待てるようになった.今後同じような課題に遭遇したら検討しようと思う.とは言え,あくまで重要なのは「アプリケーション側でリトライ機構を用意すること」なので,忘れないように!なお,検証環境の設定などは全て GitHub に置いておいた.

github.com