考虑一个在容器中有效设置Django运行环境的配置方案
简述
我原本是在容器中创建和运行Django的环境,但是当我试图朝着云原生和不可变的微服务架构的方向迈进时,发现一般的Django构建方式感觉不够好。
经过各种尝试,我成功地实现了一个很好的解决方案,所以我打算以逆向思维的方式将其写成一篇文章。
有关完整的代码,请参考以下存储库:
https://github.com/sensq/container_django
构成
最终构成请参考下图。
看起来可能有点困难,但使用容器可以相对容易地进行构建。
数据、缓存和应用程序日志均保存在不同的数据库中,并且不允许应用程序持有任何数据。
由于Django生成的静态文件需要在Nginx中引用,所以需要将它们放置在两者都可见的位置上。如果预先准备好所有生成文件,只需从Nginx中看到即可,但这样会增加管理文件的数量。本次只是简单地将其挂载在合适的主机目录中。
由于在将功能添加到Django时变得混乱且困难,API-Server创建了一个可以将功能独立出来的地方作为依赖性较低的功能。可以将API添加到一个容器中,也可以为每个功能创建一个容器,总之,我认为它很方便。这次只实现了将应用程序日志保存到InfluxDB的部分。
不进行容器的监控。

上面的图片中的美人鱼(在Qiita上没有对应的支持)
graph LR
User((User))
User -->|"アクセス"| Nginx
User -->|"アクセス"| Chronograf
subgraph フロント
Nginx
end
Nginx -->|"Staticファイル参照"| static
uWSGI -->|"Staticファイル参照"| static
Nginx -->|"リバースプロキシ"| uWSGI
uWSGI -->|"データ"| MySQL
uWSGI -->|"キャッシュ"| memchached
uWSGI -->|"アプリログ"| Flask
Chronograf -->|"アクセス"| InfluxDB
Flask -->|"アプリログ"| InfluxDB
subgraph AP
uWSGI["uWSGI+Django"]
end
subgraph API Server
Flask
end
subgraph CacheDB
memchached
end
subgraph DB
MySQL
end
subgraph 時系列DB
InfluxDB
end
subgraph ログ可視化
Chronograf
end
static
中文翻译:所使用的中间件及其应用领域
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配置文件
在环境变量中进行各种设置,并在更改设置或代码后重新创建容器。然而,由于每次进行代码更改的确认都很麻烦,所以在开发过程中最好通过挂载代码目录来持久化代码,这样更方便且更好。
在实际使用时,需要将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というファイルも送る
ただし、コンフィグファイルで環境変数を使うために下記の細工がいる
envsubstを使ってDockerで設定ファイルに環境変数を埋め込めこむ汎用的なパターン
Djangoコンテナ
コンテナで良い感じに使うためにいろいろと細工がいる(下記参照)
API-Serverコンテナ
Post用のメソッドを1つ作成してFlaskでWebサーバとして起動しておくだけ
具体的には下記参照
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问题的反向引用
希望将项目名称和应用程序名称转化为环境变量。
需要更改的文件有以下四个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的服务器名称和端口号固定,但如果想要将其变为环境变量,可以使用os.environ.get来参考环境变量,就像MySQL一样。请确保提前通过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就启动了。
如果在无法连接到数据库的情况下启动Django,迁移将无法成功并且无法正常启动。
通过使用docker-compose的depend_on选项,可以设置启动顺序,但这仅仅是为了容器的启动顺序,它不会等待直到初始设置完成。
因此,在docker-entrypoint.sh的开始处添加一个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
每次创建数据库时都必须创建管理员用户。
使用`docker-entrypoint.sh`脚本来检查管理员用户的存在,如果不存在,则创建该用户。
通常情况下,我们可以使用`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')