Django Girls TutorialのアプリをDocker環境(Django × MySQL × Redis)で動くようにしてみた。
その際にポイントだと思ったこと・ハマったことを備忘録として残しておく。
githubのリポジトリは以下
https://github.com/kamatimaru/djangogirls-tutorial-docker
環境
開発用PC
- OS : MacOS Catalina
- バージョン : 10.15.4
Docker
- バージョン : 19.03.8
アプリケーションサーバー(Dockerコンテナ)
- OS : Amazon Linux2
- バージョン : amzn2-container-raw-2.0.20200406.0-x86_64
- 使用したイメージ : amazonlinux:2
- Python
- バージョン : 3.7.6
- Django
- バージョン : 2.2.4
DBサーバー(Dockerコンテナ)
- DB : MySQL
- バージョン : 5.7.30
- 使用したイメージ : kamatimaru/mysql57-ja
※ 公式のmysql:5.7のイメージに自分なりに日本語設定を追加したもの。
githubのリポジトリは以下。
https://github.com/kamatimaru/docker-mysql57-ja
キャッシュサーバー (Dockerコンテナ)
- キャッシュサーバー : Redis
- バージョン : 6.0.0
- 使用したイメージ : redis:6
目的
- Docker/Docker Composeの勉強
- DjangoでSqlite以外のDBを使う際の設定方法の勉強
- Djangoでキャッシュサーバーを使う際の設定方法の勉強
- アプリケーションサーバー × DBサーバー × キャッシュサーバーの構成はよくあるので、一度Dockerでの開発環境を構築する実績を作っておくことで、他のプロジェクトに展開できるようにする。
ポイントだと思ったこと・ハマったこと
イメージ作成時
mysqlclientのインストールに失敗する
Macの時とは別のエラーでpipでのmysqlclientのインストールに失敗した。
結論としては、以下のモジュール全てをyumでインストールしておく必要があった。
yum install -y python3-devel mysql mysql-devel gcc
コンテナ起動時にコマンドを実行したい。
毎回コンテナ起動後にコンテナにログインして
- マイグレーション
- サーバー起動
- superuserの作成
を行うのは効率が悪いので、コンテナ立ち上げ時にコマンドを実行する方法がないか調べたところ、「ENTRYPOINT」という命令で実現できた。
ENTRYPOINT /bin/bash ${DEPLOY_DIR}/docker-entrypoint.sh
※ docker-entrypoint.shの中身については後の項に記載
ENTRYPOINTで環境変数が展開されない
ENTRYPOINT記述する際に、最初は
ENTRYPOINT ["/bin/bash", "${DEPLOY_DIR}/docker-entrypoint.sh"]
と記載していたが、環境変数が展開されなくて少しハマった。
stackoverflowに同じような質問があったので、その回答を参考に
ENTRYPOINT /bin/bash ${DEPLOY_DIR}/docker-entrypoint.sh
と書いたら解決。
stackoverflow.com
コンテナ起動時
Django Girls Tutorialアプリのコンテナが起動後すぐに停止してしまう
「dockser-compose up -d」でそれぞれのコンテナが立ち上がるようにはなったが、Django Girls Tutorialアプリのコンテナだけ、起動後すぐに停止してしまうという事象が発生した。
調べていると以下の記事を発見
qiita.com
記事の通り、docker-compose.ymlに
my-djangogirls-tutorial: ... tty: true
を付けるとコンテナが停止しなくなった。
ホストOS側からブラウザで「http://localhost:8000」にアクセスすると「ERR_EMPTY_RESPONSE」が返ってくる
Django Girls Tutorialアプリのコンテナにログインして、
python3 manage.py migrate python3 manage.py runserver
を実行した後、ホストOS側からブラウザで「http://localhost:8000」にアクセスすると、以下のように「ERR_EMPTY_RESPONSE」が返ってきた。
こちらも、調べていると以下の記事を発見
qiita.com
IPアドレス「0.0.0.0」でアプリケーションサーバーを起動すると解決するらしい。
Djangoではrunserver時にオプションで以下のようにIPアドレスとポートを指定できるようなので、以下のコマンドを実行してから再度ホストOS側のブラウザで閲覧したところ、無事DjangoのTOPページを表示できた。
python3 manage.py runserver 0.0.0.0:8000
※ネットワークの知識に疎いので、上記のQiitaの解説記事を完全には理解できなかったが、ここで止まっていると先に進めないので、いったん宿題として積むことにする。
コンテナ起動時にDBを作成する。
毎回コンテナ起動後にMySQLのコンテナにログインして、「CREATE DATABASE」を実行するのは効率が悪いので、コンテナ起動時にDBを作成したいと思った。
最初はちょっと面倒だなと思いつつも、docker-entrypoint.sh内で「CREATE DATABASE」を実行しようと思っていたが、公式のMySQLのイメージはコンテナ起動時に「MYSQL_DATABASE」という環境変数を渡せば、起動時にデータベースを作成してくれるということを知った。
hub.docker.com
実際にdocker-compose.ymlに以下のように記載すると、「djangogirls」というデータベースがコンテナ起動時に作成されるようになった。
db_server: ... environment: ... MYSQL_DATABASE: djangogirls
DBサーバー・キャッシュサーバーのIPアドレスの取得方法
Docker Composeでは各コンテナのプライベートIPアドレスは動的に割り当てられるため、固定ではない。
他方でもちろんDjangoのsettings.pyにはMySQLとRedisのIPアドレスを設定する必要がある。
解決方法としては、docker-compose.ymlに連携したいコンテナを設定しておくと、環境変数から連携先のコンテナのIPアドレスやポート番号を取得することができる。
docker-compose.ymlには以下のように記述する。
my-djangogirls-tutorial: .... links: - "db_server:mysql" - "cache_server:redis" .... db_server: .... ports: - "3306:3306" .... cache_server: .... ports: - "6379:6379" ....
すると、以下のように「my-djangogirls-tutorial」のコンテナの環境変数に情報が設定される。
$ printenv ... MYSQL_PORT_3306_TCP_ADDR=172.17.0.3 ... REDIS_PORT_6379_TCP_ADDR=172.17.0.2 ... $
コンテナ起動時にマイグレーションの実行・サーバーの起動を行う
docker-entrypoint.shに以下を記載すればOK
python3 ${DEPLOY_DIR}/manage.py migrate python3 ${DEPLOY_DIR}/manage.py runserver 0.0.0.0:8000
※ ${DEPLOY_DIR}はDockerfileでENVで宣言しているので、docker-entrypoint.sh内でも使うことができる。
コンテナ起動時にsuperuserの作成を行う
createsuperuserは対話モードで実行されるため、自動化するためにはexpectコマンドが必要になる。
以下をDockerfileに追記して、イメージ作成時にexpectをインストールする。
RUN yum install -y expect
expectコマンドの使い方については、以下の記事を参考にさせて頂いた。
また、コンテナのロケールを日本語に設定しないと、expectが日本語を判定できないので、以下もDockerfileに追記する。
RUN yum install -y glibc-langpack-ja ENV LANG ja_JP.UTF-8
以下を参考にさせて頂いた。
qiita.com
結論としては、以上をDockerfileに記述した上でdocker-entrypoint.shに以下のように記述すると、admin/passwordでsuperuserをコンテナ起動時に自動で作成することができる。
expect -c " spawn python3 ${DEPLOY_DIR}/manage.py createsuperuser expect \"ユーザー名 (leave blank to use 'root'):\" send \"admin\n\" expect \"メールアドレス:\" send \"admin@example.com\n\" expect \"Password:\" send \"password\n\" expect \"Password (again):\" send \"password\n\" expect \"Bypass password validation and create user anyway? \\\\\[y/N\\\\\]:\" send \"y\n\" expect \"Superuser created successfully.\" exit 0 "
※ 尚、以下のブラケット([])の部分は、expectで使用されているtclという構文の特殊文字なのでエスケープが必要だが、なぜ5個必要なのかは分からなかった。1〜4個で試しみていずれもうまくいかなかったので、5個で試してみたらうまくいった。
(ちなみに、expectは部分一致でも判定してくれるので、「Bypass password validation and create user anyway? 」まででもよい。)
expect \"Bypass password validation and create user anyway? \\\\\[y/N\\\\\]:\" send \"y\n\"
MySQLの起動を待ってからマイグレーションを実行する
コンテナ起動時にマイグレーションを実行するに当たって、MySQLが起動する前にマイグレーションコマンドを実行して、エラーでコンテナ起動が失敗することがありうるのか?というのが気になった。
ドキュメントを確認したところ、
- docker-composeはデフォルトではコンテナが起動する順番を保証しない。
- 「depends_on」というオプションを使えばコンテナが起動する順番は制御できるが、アプリケーション起動までのWAITは行わない
とのことであった。
従って、以下のようにdocker-entrypoint.shの中にWAIT処理を記述した。
conn_established=0 for _ in `seq 1 60` do mysql -uroot -ppassword -h ${MYSQL_PORT_3306_TCP_ADDR} -e "SELECT 1 FROM dual;" > /dev/null 2>&1 if [ $? -eq 0 ]; then conn_established=1 break fi sleep 1 done if [ $conn_established -eq 1 ]; then # MySQLへの接続が確立できた場合に実行する処理 else # MySQLに接続できなかった場合に実行する処理 fi
最終的なdocker-entrypoint.sh
docker-entrypoint.shは最終的に以下のようになった。
https://github.com/kamatimaru/djangogirls-tutorial-docker/blob/master/docker-entrypoint.sh
Djangoの設定
環境変数からの値の読み込み
今回の用途ではPythonの標準ライブラリのos.environで十分だが、試験的にakiyokoさんの『現場で使える Django の教科書《基礎編》』で紹介されている「django-environ」を使ってみる。
・インストール方法
requirements.txtに以下を記述
django-environ
・使い方(例)
import environ env = environ.Env() mysql_ip = env("REDIS_PORT_6379_TCP_ADDR")
MySQLへの接続の設定方法
同じくakiyokoさんの本のp117〜p118を参考にさせて頂いた。
requirements.txtに以下を記述
mysqlclient
・以下のように設定する。
DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "djangogirls", "USER": "root", "PASSWORD": "password", "HOST": env("MYSQL_PORT_3306_TCP_ADDR"), "PORT": "3306" } }
以上で接続できた
Redisへの接続の設定方法
調べてみたところ、DjangoでRedisを使う場合はdjango-redisというサードパーティーのライブラリを使用するのがスタンダードらしい。
github.com
Djangoの公式ドキュメントにはMemcachedの設定方法しか記載されていない。
docs.djangoproject.com
従って、Redisの場合は設定方法に関してもdjango-redisのドキュメントを参照する。
結論としてはドキュメント通りにsettings.pyに以下を追記すればOK
CACHES = { "default": { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://" + env("REDIS_PORT_6379_TCP_ADDR") + ":6379", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", } } } SESSION_ENGINE = "django.contrib.sessions.backends.cache"
動作確認
MySQL
管理画面からコンテンツを登録した後、DBをSELECTしてデータが保存されていることを確認する。
mysql> select * from blog_post; +----+-----------+--------------------------------+----------------------------+----------------------------+-----------+ | id | title | text | created_date | published_date | author_id | +----+-----------+--------------------------------+----------------------------+----------------------------+-----------+ | 1 | テスト | テスト用の記事です。 | 2020-05-03 02:18:34.000000 | 2020-05-03 02:18:48.000000 | 1 | +----+-----------+--------------------------------+----------------------------+----------------------------+-----------+ 1 row in set (0.00 sec)
Redis
管理画面にログインした後、Redisを検索してセッションが保存されていることを確認する。
root@690eda5c0010:/data# redis-cli 127.0.0.1:6379> keys * 1) ":1:django.contrib.sessions.cachegz3e1parfbzg4h5g2k8zdcapmpjqs5bt" 127.0.0.1:6379> get :1:django.contrib.sessions.cachegz3e1parfbzg4h5g2k8zdcapmpjqs5bt "\x80\x04\x95\x97\x00\x00\x00\x00\x00\x00\x00}\x94(\x8c\r_auth_user_id\x94\x8c\x011\x94\x8c\x12_auth_user_backend\x94\x8c)django.contrib.auth.backends.ModelBackend\x94\x8c\x0f_auth_user_hash\x94\x8c(ce3757e18e11fe9e307c39921281fcf0a9039b95\x94u." 127.0.0.1:6379>
以上