LoginSignup
2
5

More than 3 years have passed since last update.

[Docker]依存関係にあるサービス間のコンテナ起動タイミングを調整する[python]

Last updated at Posted at 2020-08-26

この記事について

この記事は、Django + MySQLの環境構築を通じてDockerの使い方を学ぶに関連する記事群の Part.5 にあたります。

  1. venvを利用してPythonの仮想環境を構築する
  2. Dockerfileの記述を考える
  3. docker-compose.ymlの記述を考える
  4. 設定ファイルを編集し docker-compose up を実行する
  5. 依存関係にあるサービス間のコンテナ起動タイミングを調整する ( 当記事 )

はじめに

この記事では、docker-composeを利用して依存関係にある複数のサービスを動かすにあたって起こることのある、起動順序による接続の失敗に関する問題の解決を目指していきます。

前回までの記事にて、docker-composeを利用したDjango + MySQLの環境構築設定を行いました。
基本的な設定が終わり$ docker-compose upでDjangoサーバを起動しようとすると、
MySQL側の準備が終わっていないのにもかかわらず、DjangoDBへの接続を試みてしまうことで、接続エラーを吐いたまま状況が進行しない状態に陥ってしまうことがあります。

現時点では、初期実行時のみに起こる問題で、MySQL側の準備を待ってから一度手動でキャンセルしてもう一度$ docker-compose upを実行すれば接続には成功するのですが、今後作業の過程で変更を加える際に同じ問題が起こり煩わされることのないように、予め対応策を組み込めないか考えてみます。

エラーの内容

DBとの接続に失敗すると、以下のようなエラーが送りだされ、Django側のコンテナが硬直してしまいます。

# (抜粋)
djst_django | MySQLdb._exceptions.OperationalError: (2002, "Can't connect to MySQL server on 'db' (115)")
djst_django | django.db.utils.OperationalError: (2002, "Can't connect to MySQL server on 'db' (115)")

存在しないDBへの接続を試みた場合に送出されるエラーのようです。
つまり、今回の場合はまだDBの準備が出来ていないことが問題なので、何かしらの手段で出来るまで待つことによって順番を調整出来ないか考えてみます。

ちなみにDockerの公式においてはシェルスクリプトによる解決が推奨されていますが、今回はPythonのコーディングとmysqlclientの接続練習もかねて、専用のPythonのファイルを準備して解決を試みることにします。

ファイルの内容

全体

以下のようなファイルをconfigディレクトリ下に用意しました。

config/wait_for_db.py
import os
import MySQLdb
from time import sleep
from pathlib import Path

os.chdir(Path(__file__).parent)
from local_settings import DB_NAME, DB_USER, DB_PASSWORD


count_to_try = 0
LIMIT_OF_COUNT = 20 # 値は必要に応じて調整


def check_connection(count, limit):
    """
    docker-compose up実行時用、時間調整のための関数。
    """
    try:
        conn = MySQLdb.connect(
            unix_socket = "/var/run/mysqld/mysqld.sock",
            user=DB_USER,
            passwd=DB_PASSWORD,
            host="db",
            port=3306,
            db=DB_NAME,
        )
    except MySQLdb._exceptions.OperationalError as e:
        count += 1
        print("Waiting for MySQL... (", count, "/ 20 )")
        sleep(3)
        if count < limit:
            check_connection(count, limit)
        else:
            print(e)
            print("Failed to connect mySQL.")
    else:
        print("Connected!\n")
        conn.close()
        exit()


if __name__ == "__main__":
    check_connection(count_to_try, LIMIT_OF_COUNT)

記述について

続いて、コードの内容について書いていきます。

まずは必要なライブラリやパッケージをインストールします。

config/wait_for_db.py
import os
import MySQLdb
from time import sleep
from pathlib import Path

os.chdir(Path(__file__).parent)
from local_settings import DB_NAME, DB_USER, DB_PASSWORD

local_settings.pyに関してはos.chdir(Path(__file__).parent)でディレクトリを移動してからimportしています。

次に変数を定義します。

config/wait_for_db.py
count_to_try = 0
LIMIT_OF_COUNT = 20 # 値は必要に応じて調整

count_to_tryは後述する関数が呼び出された回数をカウントするためのもので、LIMIT_OF_COUNTはその回数制限です。20回繰り返したのちにファイルを終了するように関数内で条件分岐をします。この値は必要に応じて調整して使います。

メインとなる関数check_connectionの内容については、
1. try文にてMySQLへの接続を試し、
2. 失敗した場合はexcept文にて接続試行回数をカウントして同一関数へ再帰させ、
3. 成功した場合はelse文にて接続を終了しこのファイルを閉じる
といった流れになります。

config/wait_for_db.py
try:
    conn = MySQLdb.connect(
        unix_socket = "/var/run/mysqld/mysqld.sock",
        user=DB_USER,
        passwd=DB_PASSWORD,
        host="db",
        port=3306,
        db=DB_NAME,
    )

try文のunix_socketについては接続に利用するソケットですが、これは$ docker-compose up実行時のログに表示があるので、それを書き写せばOKです。あとは前回Djangoから設定したものと同様です。
.

config/wait_for_db.py
except MySQLdb._exceptions.OperationalError as e:
    count += 1
    print("Waiting for MySQL... (", count, "/ 20 )")
    sleep(3)
    if count < limit:
        check_connection(count, limit)
    else:
        print(e)
        print("Failed to connect mySQL.")

except文については、試行回数をカウント、出力してから3秒待機し、20回に到達したらエラー内容を出力してファイルを終了します。上限を設けたのは、実際に接続エラーがある場合の無限ループを防ぐためです。
.

config/wait_for_db.py
else:
    print("Connected!\n")
    conn.close()
    exit()

try文において試行した接続が成功した時 ( => MySQLの準備が出来た時 ) はelse文で接続成功を宣言し、接続を閉じたあと、ファイルを終了します。

else文を通過することが出来た場合は、次に控えるrunserverコマンドが滞りなく実行されるはずです。
.

config/wait_for_db.py
if __name__ == "__main__":
    check_connection(count_to_try, LIMIT_OF_COUNT)

末尾の部分は、if __name__ == "__main__":、つまり「このファイルが直接実行された時は」、関数check_connectionを読み込むという記述です。引数は冒頭で定義した変数です。

最後に、サーバー実行前にこのファイルが実行されるよう、docker-compose.ymlcommand内に記述を追加します。

docker-compose.yml
command: >
    bash -c "
        pip install -r requirements.txt &&
        python config/wait_for_db.py &&
        python manage.py runserver 0.0.0.0:8000
    "

以上で、runserverコマンドの実行をMySQLのセットアップまで待機させるプログラムの準備が出来ました。

実際に$ docker-compose upを実行してみると・・・

# (抜粋)
djst_django | Waiting for MySQL... ( 1 / 20 )
# (中略...)
djst_mysql | Version: '5.7.31'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server (GPL)
djst_django | Connected!
# (中略...)
djst_django | Starting development server at http://0.0.0.0:8000/
djst_django | Quit the server with CONTROL-C.

待機ののち、接続確認後に無事Djangoサーバーを起動できました。
( 前回の記事から繰り返しになりますが、設定の都合上、実際の動作確認は表示されているhttp://0.0.0.0:8000/ではなく、http://127.0.0.1:8000/http://localhost:8000/から行うことになります。 )

終わりに

以上で、依存関係にあるサービス間のコンテナ起動タイミングを調整するという当初の目的は達成できました。
何かご指摘などありましたら、コメントいただけますと幸いです。

今回についてはトレーニングもかねてpythonで記述しましたが、シェルスクリプトの方も自在に使いこなせるよう挑戦していきたいです。

(こちら↓から最初のページに戻れます。)
Django + MySQLの環境構築を通じてDockerの使い方を学ぶ

ご覧いただきありがとうございました。

2
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
5