概要
Djangoの環境をコンテナで作って動かしていたが、クラウドネイティブを目指してImmutableでマイクロサービスな構成を作ろうとすると一般的なDjangoの作り方ではいまいちに感じる部分が多かった。
いろいろ弄ってみて良い感じにできたので逆引き的な書き方で記事にしておく。
コード一式は以下のリポジトリ参照
https://github.com/sensq/container_django
構成
最終的な構成は以下の図を参照
一見難しそうだが、コンテナを使うと割と簡単に構築できる。
データ、キャッシュ、アプリログはすべて別々のDBに保存するようにし、アプリケーションには一切のデータを持たせないようにした。
StaticファイルはDjangoが生成するものをNginxで参照させる必要があるため、両方から見える場所に置くようにする。あらかじめ生成物をすべて用意しておけばNginxから見えるだけでもよいが、その場合は管理するファイルが増える。今回は単純にホストの適当なディレクトリにマウントさせているだけ。
API-ServerはDjangoに機能追加していくとごてごてして辛くなった経験から、依存性の低い機能を別出しできる場所として作った。一つのコンテナにAPIを追加していってもいいし、1機能ごとにコンテナを作ってもいいし、とりあえずあると便利だと思う。今回はアプリのログをInfluxDBに保存する部分だけ実装した。
コンテナの監視はやらない。
上の画像のmermaid(Qiitaだと対応していなかった)
使用するミドルウェアと用途
-
Docker(コンテナ管理)
- 下記すべてを一つのサーバで良い感じに動かしてくれる凄いクジラ
- Kubernetesの前座
-
Nginx(Webサーバ)
- Webアクセスする入り口
- URLでバックエンドにルーティングさせる
-
uWSGI+Django(アプリケーションサーバ)
- アプリケーションが動くところ
- Nginxがルーティングさせてくるところ
-
Django
はPythonのWebアプリケーションフレームワーク -
uWSGI
はPythonのWebアプリを動かすためのアプリケーションサーバ-
Gunicorn
,Nginx Unit
などでも代替可
-
-
MySQL(RDBMS)
- アプリのデータを保存するところ
- どのRDBMSも使い方はほとんど変わらないので好きなものを使えばいい
-
memcached(KVS = Key-Value-Store)
- アプリのキャッシュ(ログイン情報とか)を保存するところ
- アプリのコンテナをステートレスにするために必要
-
Redis
でもたぶん代替可
-
InfluxDB(TSDB = Time-Series-DataBase)
- アプリのログを保存するところ
-
Chronograf
- InfluxDBを可視化するツール
ディレクトリ構成
.
├── .env # docker-composeの環境変数を定義するファイル
├── .gitignore
├── README.md
├── docker-compose.yml
└── build/
├── api/ # API Serverコンテナ関連のファイル
│ ├── Dockerfile
│ ├── apiserver.py
│ └── requirements.txt
├── db/ # MySQLコンテナ関連のファイル
│ ├── Dockerfile
│ └── my.cnf # MySQLの設定ファイル
├── sample_app/ # AP(Django)コンテナ関連のファイル
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── check_admin.py # 管理者ユーザの存在をチェックするスクリプト
│ ├── docker-entrypoint.sh # コンテナ起動時に毎回実行されるスクリプト
│ └── project/ # Djangoプロジェクトディレクトリ
│ ├── manage.py
│ ├── prj/ # Djangoプロジェクト全体の設定ファイル
│ │ ├── settings.py
│ │ ├── urls.py
│ │ └── wsgi.py
│ └── app/ # Djangoアプリケーションディレクトリ
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── models.py
│ ├── tests.py
│ ├── urls.py
│ ├── views.py
│ └── templates/
└── web/ # Nginxコンテナ関連のファイル
├── Dockerfile
├── default.conf.template # Nginxの設定ファイル
└── uwsgi_params
docker-compose.yml
環境変数で各種設定を行えるようにしておき、設定変更やコード変更した場合はコンテナを作り直す。とはいえ、コード変更の確認で毎回ビルドするのは面倒なので開発中はvolumes
でコードのディレクトリを永続化させておく方が楽でよい。
実際に使う場合は、db
とinfluxdb
のコンテナにはvolumes
で永続化させる設定を記載すること
version: '3'
services:
web:
build: ./build/web
depends_on:
- sample_app
volumes:
- ./data/static:/codes/static:ro
ports:
- "80:80"
- "443:443"
command: >
/bin/sh -c "envsubst '
$$NGINX_LOCATION_SUBDIR
$$WSGI_CONTAINER_NAME
$$WSGI_PORT
' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
&& nginx -g 'daemon off;'"
environment:
NGINX_LOCATION_SUBDIR: ${NGINX_LOCATION_SUBDIR} # Webアクセスするサブディレクトリ名
WSGI_CONTAINER_NAME: ${WSGI_CONTAINER_NAME} # WSGIを動かすコンテナの名前
WSGI_PORT: ${WSGI_PORT} # WSGIを動かすポート
sample_app:
build:
context: ./build/sample_app
args:
DJANGO_PROJECT_NAME: ${DJANGO_PROJECT_NAME}
depends_on:
- db
volumes:
- ./data/static:/static
# - ./hoge-dir-path:/${DJANGO_PROJECT_NAME}
environment:
WSGI_PORT: ${WSGI_PORT} # WSGIを動かすポート
WSGI_PROCESSES: ${WSGI_PROCESSES} # WSGIを動かすプロセス数
WSGI_THREADS: ${WSGI_THREADS} # WSGIを動かすスレッド数
NGINX_LOCATION_SUBDIR: ${NGINX_LOCATION_SUBDIR} # Webアクセスするサブディレクトリ名
DJANGO_DEBUG: ${DJANGO_DEBUG} # DjangoのDEBUGモードの有効化
DJANGO_ALLOWED_HOSTS: ${DJANGO_ALLOWED_HOSTS} # Djangoに接続を許可するホスト名またはIP
DJANGO_PROJECT_NAME: ${DJANGO_PROJECT_NAME} # Djangoプロジェクトの名前(フォルダ名と合わせる)
DJANGO_APPLICATION_NAME: ${DJANGO_APPLICATION_NAME} # Djangoアプリケーションの名前(フォルダ名と合わせる)
DJANGO_ADMIN_EMAIL: ${DJANGO_ADMIN_EMAIL} # Django管理者ユーザのEMAIL
DJANGO_ADMIN_PASSWORD: ${DJANGO_ADMIN_PASSWORD} # Django管理者ユーザのパスワード
DATABASE_CONTAINER_NAME: ${DATABASE_CONTAINER_NAME} # Djangoで使用するDBコンテナの名前
DATABASE_PORT: ${DATABASE_PORT} # Djangoで使用するDBコンテナの公開ポート
MYSQL_DATABASE: ${MYSQL_DATABASE} # Djangoで使用するDBの名前
MYSQL_USER: ${MYSQL_USER} # Djangoで使用するDBのログインユーザ
MYSQL_PASSWORD: ${MYSQL_PASSWORD} # Djangoで使用するDBのログインパスワード
APISERVER_HOST: ${APISERVER_HOST} # API Serverが稼働しているホストやコンテナの名前
db:
build: ./build/db
# volumes:
# - ./hoge-dir-path:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_ROOT_HOST: ${MYSQL_ROOT_HOST}
MYSQL_DATABASE: ${MYSQL_DATABASE} # Djangoで使用するDBの名前
MYSQL_USER: ${MYSQL_USER} # Djangoで使用するDBのログインユーザ
MYSQL_PASSWORD: ${MYSQL_PASSWORD} # Djangoで使用するDBのログインパスワード
cache:
image: memcached:alpine
apiserver:
build: ./build/api
environment:
INFLUXDB_HOST: ${INFLUXDB_HOST} # InfluxDBが稼働しているホストやコンテナの名前
INFLUXDB_PORT: ${INFLUXDB_PORT} # InfluxDBが稼働しているポート番号
INFLUXDB_DB: ${INFLUXDB_DB} # InfluxDBに初期作成するDB名
APISERVER_PORT: ${APISERVER_PORT} # API Serverを稼働させるポート番号
APISERVER_DEBUG: ${APISERVER_DEBUG} # API ServerのDEBUGモードの有効化
influxdb:
image: influxdb:alpine
# volumes:
# - ./hoge-dir-path:/var/lib/influxdb
environment:
INFLUXDB_DB: ${INFLUXDB_DB} # InfluxDBに初期作成するDB名
INFLUXDB_ADMIN_USER: ${INFLUXDB_ADMIN_USER} # InfluxDBの管理者ユーザ名
INFLUXDB_ADMIN_PASSWORD: ${INFLUXDB_ADMIN_PASSWORD} # InfluxDBの管理者ユーザのパスワード
chronograf:
image: chronograf:alpine
ports:
- "${CHRONOGRAF_PORT}:8888"
environment:
INFLUXDB_URL: ${INFLUXDB_URL} # InfluxDBのURL
INFLUXDB_USERNAME: ${INFLUXDB_USERNAME} # InfluxDBに接続するユーザ名
INFLUXDB_PASSWORD: ${INFLUXDB_PASSWORD} # InfluxDBに接続するユーザのパスワード
.envの記載例
NGINX_LOCATION_SUBDIR=sample
WSGI_CONTAINER_NAME=sample_app
WSGI_PORT=5000
WSGI_PROCESSES=5
WSGI_THREADS=3
DJANGO_DEBUG=True
DJANGO_ALLOWED_HOSTS=192.168.1.100
DJANGO_PROJECT_NAME=prj
DJANGO_APPLICATION_NAME=sample
DJANGO_ADMIN_EMAIL=admin@localhost.com
DJANGO_ADMIN_PASSWORD=admin
DATABASE_CONTAINER_NAME=db
DATABASE_PORT=3306
MYSQL_ROOT_PASSWORD=P@ssw0rd
MYSQL_ROOT_HOST=%
MYSQL_DATABASE=django
MYSQL_USER=django
MYSQL_PASSWORD=dj@ng0
KEYCLOAK_USER=admin
KEYCLOAK_PASSWORD=admin
APISERVER_HOST=apiserver
APISERVER_PORT=4000
APISERVER_DEBUG=True
INFLUXDB_HOST=influxdb
INFLUXDB_PORT=8086
INFLUXDB_URL=http://influxdb:8086
INFLUXDB_DB=sample
INFLUXDB_ADMIN_USER=admin
INFLUXDB_ADMIN_PASSWORD=admin
INFLUXDB_USERNAME=admin
INFLUXDB_PASSWORD=admin
CHRONOGRAF_PORT=8888
ざっくりとした各コンテナの構築説明
-
Nginxコンテナ
- 基本的にはコンフィグファイル(HTTPS化する場合は証明書と鍵も)を送るだけ
- DjangoでWSGIを使うために
uwsgi_params
というファイルも送る - ただし、コンフィグファイルで環境変数を使うために下記の細工がいる
-
Djangoコンテナ
- コンテナで良い感じに使うためにいろいろと細工がいる(下記参照)
-
API-Serverコンテナ
- Post用のメソッドを1つ作成して
Flask
でWebサーバとして起動しておくだけ - 具体的には下記参照
- Post用のメソッドを1つ作成して
-
MySQLコンテナ
- 環境変数のみで設定が完結するため、ビルド不要だった
- どこかのバージョンから以下の設定を行わないとDjangoから使えなくなったため、my.cnfを送るだけのDockerfileが必要
default_authentication_plugin=mysql_native_password
-
memcachedコンテナ
- 環境変数のみで設定が完結するため、ビルド不要
-
InfluxDBコンテナ
- 環境変数のみで設定が完結するため、ビルド不要
-
Chronografコンテナ
- 環境変数のみで設定が完結するため、ビルド不要
API-Server補足
InfluxDBのClientを使うためにpip
でinfluxdb
のインストールが必要。flask-cors
も入れておいた方がいい。
あとは以下のようなコードでAPIを作成できる。
from flask import Flask, jsonify, abort, make_response, render_template, request
from flask_cors import CORS
import os
api = Flask(__name__)
CORS(api) # CORS有効化
@api.route('/add_influxdb', methods=['POST'])
def post():
influxdb_host = os.environ.get("INFLUXDB_HOST")
influxdb_port = os.environ.get("INFLUXDB_PORT")
influxdb_database = os.environ.get("INFLUXDB_DB")
from influxdb import InfluxDBClient
client = InfluxDBClient(host=influxdb_host,
port=influxdb_port,
database=influxdb_database)
# Postで渡されたパラメータを受け取る
# 以下はタグ1つ、フィールド2つのパラメータを受け取るの場合の例
measurement = request.form["measurement"]
type = request.form["type"]
value1 = request.form["value1"]
value2 = request.form["value2"]
# InfluxDBに書き込むデータを作成する
# RDBMSのように事前にテーブル定義をしておかなくても、柔軟にデータを書き込める
data = [{
"measurement": measurement,
"tags": {"type": type},
"fields": {
"value1": value1,
"value2": value2
}
}]
# InfluxDBに書き込む
client.write_points(data)
return make_response(data[0])
# Webサーバを起動する
if __name__ == '__main__':
port = os.environ.get("APISERVER_PORT")
from distutils.util import strtobool
debug = strtobool(os.environ.get("APISERVER_DEBUG"))
api.run(host='0.0.0.0', port=port, debug=debug)
curlでAPI実行する場合のコマンド例
curl -POST http://apiserver:4000/add_influxdb -d "measurement=ipmgr&type=Hoge&value1=val1&value2=val2"
PythonでAPI実行する場合のコード例
import os
import urllib.request
import urllib.parse
host = os.environ.get("APISERVER_HOST")
port = os.environ.get("APISERVER_PORT")
api_url = "http://" + host + ":" + port + "/add_influxdb"
data = {
"measurement": measurement,
"type": type,
"value1": value1,
"value2": value2
}
encoded_data = urllib.parse.urlencode(data).encode('utf-8')
req = urllib.request.Request(api_url, encoded_data)
res = urllib.request.urlopen(req)
逆引きDjango問題
プロジェクト名とアプリ名を環境変数化したい
変更の必要なファイルは以下に配置されている4つのpythonファイル。
"<デフォルトがプロジェクト名だけど実はなんでもいい>/"
├── manage.py
├── "<プロジェクト名>/"
│ ├── settings.py
│ ├── urls.py # 上部のコメント部分だけなので適当な文字列に書き換える
│ └── wsgi.py
└── "<アプリケーション名>/"
└── # ここに配置されるファイルには該当する部分無し
どれもpythonスクリプトのため、os.environ.get("KEY_NAME")
で環境変数から値を取得できる。また、取得した値を使って文字列結合することでプロジェクト名やアプリ名の含まれるパスなどを作成できる。
-
manage.py
とwsgi.py
理由わからないがDJANGO_SETTINGS_MODULE
という環境変数に値をsetしているので、該当部分を以下のように書き換える。
# DJANGO_PROJECT_NAMEはdocker-compose.ymlで定義した環境変数
prj_name = os.environ.get("DJANGO_PROJECT_NAME")
os.environ.setdefault('DJANGO_SETTINGS_MODULE', (prj_name + '.settings'))
-
settings.py
主に以下の部分を書き換える。他に必要な部分があれば同様に書き換え可能。
# DJANGO_PROJECT_NAMEとDJANGO_APPLICATION_NAMEはdocker-compose.ymlで定義した環境変数
PRJ_NAME = os.environ.get("DJANGO_PROJECT_NAME")
APP_NAME = os.environ.get("DJANGO_APPLICATION_NAME")
INSTALLED_APPS = [
# 省略
APP_NAME,
]
ROOT_URLCONF = (PRJ_NAME + '.urls')
WSGI_APPLICATION = (PRJ_NAME + '.wsgi.application')
-
urls.py
アプリケーション内のurls.pyを参照させるように書き換える。
urlpatterns = [
path('admin/', admin.site.urls),
path('', include(os.environ.get("DJANGO_APPLICATION_NAME") + '.urls'))
]
settings.pyをいろいろ環境変数化したい
os.environ.get("KEY_NAME")
で環境変数から値を取得できるため、環境変数化したい部分をdocker-compose.yml
で定義することで好きなように環境変数化可能
他のファイルで環境変数の値を使いたい場合も同様
import文でアプリケーション名を書く必要がある
importlib
を使うと動的にimportできる。以下のように書くことで環境変数の値からimportするモジュールを決定させられる。
import os
app_name = os.environ.get("DJANGO_APPLICATION_NAME")
module_path = app_name + ".models"
from importlib import import_module
module = import_module(module_path)
Hoge = getattr(module, "Hoge")
# これで以下を記載した場合と同様にimportされる
# from app_name.models import Hoge
DBにMySQLを使いたい
settings.py
に以下のように記載する。
一般的には各パラメータには固定値を記載するが、環境変数を参照させるようにすることでコンテナを汎用的に扱いやすくする。
※事前にpip installでpymysql
を入れておくこと
import pymysql
pymysql.install_as_MySQLdb()
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.environ.get("MYSQL_DATABASE"),
'USER': os.environ.get("MYSQL_USER"),
'PASSWORD': os.environ.get("MYSQL_PASSWORD"),
'HOST': os.environ.get("DATABASE_CONTAINER_NAME"),
'PORT': os.environ.get("DATABASE_PORT", "3306"),
'OPTIONS': {
'charset': 'utf8mb4',
},
}
}
キャッシュにmemcachedを使いたい
settings.py
に以下のように記載する。
LOCATIONのサーバ名とポート番号は固定でよいと思っているが、環境変数化したかったらMySQLと同様にos.environ.get
を使って環境変数を参照させるようにすればよい。
※事前にpip installでpython-memcached
を入れておくこと
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': 'cache:11211',
}
}
DBの初期セットアップが終わる前にDjangoが起動してしまう
DBに接続できない状態でDjangoが起動してしまうとMigrationが上手くいかず正常に起動しない。
docker-compose
でdepend_on
を使えば起動順を設定できるが、あくまでコンテナの起動順のため、初期セットアップが終わるまでは待ってくれない。
そのため、docker-entrypoint.sh
の最初でDBコンテナにアクセスできるようになるまで待つwhileループを入れておく。
DATABASE_HOSTNAME=${DATABASE_CONTAINER_NAME}
DATABASE_PORT=${DATABASE_PORT:-'3306'}
echo "Waiting for database"
while ! nc -zv $DATABASE_HOSTNAME $DATABASE_PORT
do
sleep 0.1
done
DB作成するたびに管理者ユーザを作らないといけない
docker-entrypoint.sh
でadminユーザの存在をチェックし、存在していなかったら作成する。
一般的にはpython manage.py createsuperuser
で作成するが、対話形式になってしまうのでDjango shellから作成する。
-
docker-entrypoint.sh
該当部分のみ抜粋
PRJ_NAME=${DJANGO_PROJECT_NAME}
# Create Admin User
EXIST_ADMIN=`python manage.py shell < /check_admin.py`
if [ ${EXIST_ADMIN} = 'True' ]; then
:
else
echo "Does not exist admin user."
/${PRJ_NAME}/manage.py shell -c "from django.contrib.auth.models import User; User.objects.create_superuser('admin', os.environ.get('DJANGO_ADMIN_EMAIL','admin@localhost.com'), os.environ.get('DJANGO_ADMIN_PASSWORD','admin'))"
echo "Created admin user!! ('admin', ${DJANGO_ADMIN_EMAIL:-'admin@localhost.com'}, ${DJANGO_ADMIN_PASSWORD:-'admin'})"
fi
-
check_admin.py
存在チェックのスクリプトも埋め込めるが、流石に長いので別ファイルにしておく。
from django.contrib.auth.models import User
try:
User.objects.get(username="admin")
print('True')
except User.DoesNotExist:
print('False')