考虑一个在容器中有效设置Django运行环境的配置方案

简述

我原本是在容器中创建和运行Django的环境,但是当我试图朝着云原生和不可变的微服务架构的方向迈进时,发现一般的Django构建方式感觉不够好。
经过各种尝试,我成功地实现了一个很好的解决方案,所以我打算以逆向思维的方式将其写成一篇文章。
有关完整的代码,请参考以下存储库:
https://github.com/sensq/container_django

构成

最终构成请参考下图。
看起来可能有点困难,但使用容器可以相对容易地进行构建。
数据、缓存和应用程序日志均保存在不同的数据库中,并且不允许应用程序持有任何数据。

由于Django生成的静态文件需要在Nginx中引用,所以需要将它们放置在两者都可见的位置上。如果预先准备好所有生成文件,只需从Nginx中看到即可,但这样会增加管理文件的数量。本次只是简单地将其挂载在合适的主机目录中。

由于在将功能添加到Django时变得混乱且困难,API-Server创建了一个可以将功能独立出来的地方作为依赖性较低的功能。可以将API添加到一个容器中,也可以为每个功能创建一个容器,总之,我认为它很方便。这次只实现了将应用程序日志保存到InfluxDB的部分。

不进行容器的监控。

arch.png

上面的图片中的美人鱼(在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')
广告
将在 10 秒后关闭
bannerAds