6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

TORICOAdvent Calendar 2020

Day 11

私の開発チームでの Django バックエンド開発環境の紹介 (Python3.8 を docker-compose でデバッグする)

Last updated at Posted at 2020-12-12

macを新調したところ、Python のバージョンがデフォルトで 3.9 になってました。

私の開発チームでは、まだ Python 3.8 で開発しているものがあるため、3.8 での開発環境を構築する必要がありました。ダウングレードのPython環境を作る場合、 pyenv を使うのが一般的だと思いますが、今回は Python 3.8 が含まれる Alpine のコンテナを docker-compose で起動して、pycharm のリモートデバッグで開発を行うようにしました。社内共有がてら、構成を書きます。

ちなみに、docker-compose でのリモートデバッグ開発環境の欠点として、selenium (selene) で、ブラウザウィンドウを表示しながらのオートメーションやテストを行うことが困難なため、そのような用途では mac 上にpyenvで仮想環境を作ったほうが良いでしょう。ヘッドレスであれば問題なさそうです。

なお、本記事内で、docker イメージのバージョン番号は一切書かれていませんが、実際の運用ではバージョン番号を入れています。

要約

この記事では、私の開発チームでの Django のサーバサイドアプリの基本的な構成を紹介するとともに、

  • プライベートリポジトリを含む Pipfile の内容を docker イメージにインストールする方法
  • PyCharm に docker-compose でのスクリーンショットつきデバッグ実行設定手順
  • docker-compose.yml に環境変数のデフォルト値を設定する方法
  • uwsgi.ini に環境変数のデフォルト値を設定する方法
  • nginx を使わず、 uwsgi からスタティックファイルをホストする方法
  • docker login の結果のトークンを kubernetes の secret として保存する pythonスクリプト

などが書いてあります。

デモリポジトリ

docker/build.sh
docker/run.sh

でのコンテナの起動、docker/docker-compose.yml を使ってのデバッグ実行が試せます。

Django__納期を逃さない完璧主義者のためのWebフレームワーク.png

ディレクトリ構成

  • app
    • manage.py
    • myapp
      • settings
        • local.py
        • production.py
  • uwsgi
    • uwsgi.ini
  • docker
    • docker-compose.yml
    • Dockerfile
    • build.sh
    • config.sh
    • run.sh
  • kubernetes
    • apply.sh
    • deployment.yml
    • service.yml

docker

dockerビルド・実行方法

デモリポジトリの clone 後、

docker/build.sh
docker/run.sh

ブラウザで http://127.0.0.1:8000 を開くと、Django のランディングページが表示されます。

ディレクトリ内容

docker/config.sh
image_name=torico/myapp
container_name=myapp

共通で使う、Docker イメージ名などを定義しています。

docker/build.sh
#!/usr/bin/env zsh
cd "$(dirname $0)" || exit
. ./config.sh
cd ..
docker build . --ssh default -t ${image_name} -f docker/Dockerfile

Dockerfile をプロジェクトルートに入れず、docker ディレクトリの中に入れてますが、
ビルド対象はプロジェクトルートのため、一回上位ディレクトリに移動してからビルドしてます。

--ssh オプション、および Dockerfile の1行目の
# syntax=docker/dockerfile:1.0.0-experimental
は、Pipfile から Python のプライベートリポジトリのライブラリをインストールする時に使います。
プライベートリポジトリのライブラリが無い場合は必要ありません。

Dockerfile

# syntax=docker/dockerfile:1.0.0-experimental

FROM alpine:3.12

ENV PYTHONUNBUFFERED 1

RUN apk --no-cache add \
    python3 \
    py3-pip \
    uwsgi \
    uwsgi-http \
    uwsgi-python3 \
    mariadb-connector-c \
    git \
    openssh \
    jpeg

RUN pip3 install pipenv --ignore-installed distlib

COPY Pipfile /tmp/Pipfile

RUN apk add --no-cache --virtual=.build-deps \
    gcc \
    make \
    python3-dev \
    musl-dev \
    libffi-dev \
    mariadb-dev \
    postgresql-dev \
    g++ \
    libgcc \
    libstdc++ \
    libxml2-dev \
    libxslt-dev \
    jpeg-dev \
    && PIPENV_PIPFILE=/tmp/Pipfile pipenv install --dev --system --skip-lock --deploy \
    && apk del .build-deps \
    && rm -rf /var/cache/apk/* \
    && rm -rf /tmp/*

RUN mkdir -m 777 -p /var/log/myapp

COPY uwsgi /var/src/uwsgi
COPY app /var/src/app
RUN chown -R uwsgi:uwsgi /var/src

USER uwsgi
RUN cd /var/src/app && python3 ./manage.py collectstatic --noinput

EXPOSE 8000
CMD ["uwsgi", "--ini", "/var/src/uwsgi/uwsgi.ini"]

ライブラリを多く盛り込んでいます。今回のように、ただ Django を起動するだけではこれらのライブラリは
必要ありませんが、 PIL、ソーシャルログイン、HTMLパーサー、mysqlclient、firebase、
もろもろ Pipenv に追加していくと、ビルドのためのライブラリが必要になります。

今回のように、必要ライブラリをすべて入れてビルドする以外にも、
ビルドが必要なライブラリは Pipenv ではインストールせず、
ビルド済みライブラリをインストールする方法であったり、
ビルドを行うにしてもマルチステージの Dockerfile を作って行う方法もありますが、
Alpine では apk --virtual= でのインストールが、
Dockerfile を簡潔に保てて良いと感じており、よく使います。

Dockerイメージ単体で(ボリュームのマウントなしで)起動するようにするため、
Djangoのソースコードをコピーして含めています。
Admin などで static ディレクトリを使うため、Dockerビルド段階で collectstatic を実行しておきます。

なお、Alpine Linux にはロケールデータが無いため、国際化 i10n, i18n は難しいです。
国際化が必要な場合は、debian を使うのが楽だと思います。

参考までに、マルチステージビルドを行った場合の (別プロジェクトで使っている) Dockerfile も載せます。

FROM alpine:3.12 AS builder
# マルチステージビルドを試してみたが、他のプロジェクトでやってるような
# apk --virtual をつかう手法と比べてイメージサイズを少なくできるわけではない。
# --virtual のほうが dockerfile が短くできるので、そっちがいいかな。

RUN apk --no-cache add \
  python3 \
  py3-pip

RUN pip3 install pipenv --ignore-installed distlib

COPY Pipfile /tmp/Pipfile

RUN apk add --no-cache --virtual=.build-deps \
  gcc \
  make \
  python3-dev \
  musl-dev \
  libffi-dev \
  mariadb-dev \
  postgresql-dev \
  g++ \
  libgcc \
  libstdc++ \
  libxml2-dev \
  libxslt-dev \
  jpeg-dev \
  && PIPENV_PIPFILE=/tmp/Pipfile pipenv install --system --skip-lock --deploy \
  && apk del .build-deps \
  && rm -rf /var/cache/apk/* \
  && rm -rf /tmp/*


FROM alpine:3.12

COPY --from=builder /usr/lib/python3.8/site-packages /usr/lib/python3.8/

RUN apk --no-cache add \
  git \
  python3 \
  py3-pip \
  musl \
  uwsgi \
  uwsgi-http \
  uwsgi-python3 \
  mariadb-connector-c \
  libxml2 \
  libxslt \
  py3-pillow

# py3-pillow は、画像のライブラリをインストールするため

RUN mkdir -m 777 -p /var/log/awesome-app

COPY conf /var/src/conf
COPY awesome_app /var/src/awesome_app
RUN chown -R uwsgi:uwsgi /var/src

USER uwsgi
RUN cd /var/src/awesome_app && python3 ./manage.py collectstatic --noinput

EXPOSE 8080
CMD ["uwsgi", "--ini", "/var/src/conf/uwsgi.ini"]

PyCharm からのデバッグ方法

新しい mac は、Python 3.9 がデフォルトでインストールされており、
そのままでは Python 3.8 の Python 仮想環境 (.venv)
を作ることができません。

brew で pyenv をインストールして、pyenv 内でダウングレードバージョンの
Python を管理するのも良いのですが、Pycharm は docker でのデバッグ実行も可能です。

docker でのデバッグ実行をする際は、docker イメージのバージョンをファイルで管理したほうが簡潔なため、
docker-compose 経由での起動がおすすめです。

(docker-compose を使わず、 docker run 相当の実行コンフィギュレーションを書いて、デバッグ実行させることもできますが、その場合各実行コンフィギュレーションの中に Dockerイメージのバージョン番号を埋め込むことになるため、イメージのバージョンアップを行った都度設定を書き換えないといけないため手間です。イメージのバージョン番号を docker-compose.yml の中にいれておけば、管理が容易になりますので、docker-compose を勧めています。)

PyCharm での Python Interpreter の指定

⌘+, → Python Interpreter → Add

スクリーンショット_2020-12-11_20_35_31.png

Docker ComposeNewDocker for mac

スクリーンショット_2020-12-11_20_40_19.png

Configuration file(s) に、docker/docker-compose.yml を指定

Servicemyapp を指定

Python interpreter path: は、python から python3 に変更しておく。

Alpine linux では、python コマンドでは python3 が起動できないためです。

Cursor_と_Add_Python_Interpreter.png

設定したら [OK] をクリック。

すると、先程作った Docker イメージ内の Python3.8環境が認識されます。

Path mappings も設定します。

Edit_Project_Path_Mappings_と_Preferences-2.png

スクリーンショット 2020-12-11 21.22.12.png

プロジェクト内の app ディレクトリを、/var/src/app にマッピングします。

PyCharm での Project Structure の設定

⌘+, → Project Structure

Django ディレクトリを Sources に追加

Preferences-5.png

PyCharm での Django Support の設定

⌘+, → Django

Enable Django Support にチェック

Django project root は、django ディレクトリ

Settings には、myapp/settings/local.py を設定

Preferences-4.png

実行コンフィギュレーションの作成

右上の、Edit Configurations より
+Django ServerHost0.0.0.0 を指定して、環境を作ります。

スクリーンショット 2020-12-11 20.50.31.png

Run_Debug_Configurations_と_torico-myapp_–_docker-compose_yml.png

Python Interpreter は、先程設定した Docker compose になっているはずです。

虫ボタンからデバッグ実行すると、docker-compose で起動した Python へリモートデバッグが行えます。

docker-compose.yml の解説

version: '3'

services:
  myapp:
    image: torico/myapp
    container_name: myapp
    ports:
      - "8000:8000"
    environment:
      DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-myapp.settings.local}
    restart: always
    volumes:
      - ../app:/var/src/app
DJANGO_SETTINGS_MODULE: ${DJANGO_SETTINGS_MODULE:-myapp.settings.local}

docker compose version 3 より、環境変数のデフォルト値が使えるようになっています。

この場合は、環境変数 DJANGO_SETTINGS_MODULE が設定されていなければ、
myapp.settings.local を使うという設定になります。

そのため、

DJANGO_SETTINGS_MODULE=myapp.settings.development docker-compose up -d

といった、環境を切り替えてのDjango の起動も容易です。

    volumes:
      - ../app:/var/src/app

Dockerファイルで、app ディレクトリをまるごとコピーしてイメージを作っていますが、
docker-compose から起動した際はホストのディレクトリを docker イメージのディレクトリを上書きする形でマウントしています。
開発のためです。

uwsgi

冒頭で、

docker/build.sh
docker/run.sh

のコマンドで Django サーバを起動したり、後述の kuberneters で起動する際は、
webアプリケーションサーバとして uwsgi が起動します。

uwsgi の設定ファイルは以下の形ですが

uwsgi.ini
[uwsgi]
base = /var/src/app
chdir = %(base)

plugins = http,python3
http = 0.0.0.0:8000
vacuum = true
die-on-term = true

module = myapp.wsgi:application
master = true

if-not-env = DJANGO_SETTINGS_MODULE
env = DJANGO_SETTINGS_MODULE=myapp.settings.local
endif =

env = LC_ALL=en_US.UTF-8
env = LANG=en_US.UTF-8
touch-reload = %(base)/myapp/wsgi.py
uid = uwsgi
static-map = /static=/var/src/staticfiles
logto = /var/log/myapp/%n.log
thunder-lock = true
buffer-size = 32768
processes = %k
threads = 16

ポイントとしては

if-not-env = DJANGO_SETTINGS_MODULE
env = DJANGO_SETTINGS_MODULE=myapp.settings.local
endif =

ここで、もし環境変数 DJANGO_SETTINGS_MODULE がセットされていなければ、
デフォルト値 myapp.settings.local を使うようにしています。

kubernetes で起動する際、settings を外から指定できるようにするためです。

static-map = /static=/var/src/staticfiles

こちらの設定では、リクエストパスが /static から始まる場合、リクエストを Django に渡さずに uwsgi のみで静的ファイルのレスポンスを返すようにしています。 staticfiles ディレクトリの中身は、docker イメージのビルド時に ./manage.py collectstatic で作っています。

processes = %k

こちらは、processes の数値としてCPUコア数をそのまま使っています。

外部通信 (DB, 決済API、ElasticSearch, Redis, メール, ログ等) が多かったりして
待機時間が多いアプリの場合は、並列で多くのリクエストを扱えるよう、threads は多めにしています。

検証環境は少なめ、本番環境は多めにするために

threads = %(%k * 10)

といった設定にすることもあります。(コア数の10倍)

kubernetes

作ったイメージは、EKS にプッシュして kubernetes にデプロイします。

mac で、kubernetes/apply.sh を実行して、本番環境を構築します。

#!/usr/bin/env zsh

export KUBECONFIG=${HOME}/.kube/my-kubeconfig

kubectl apply -f deployment.yml
kubectl apply -f service.yml
kubectl apply -f ingress.yml

予め、 kubernetes クラスタの kubeconfig を、mac の ${HOME}/.kube/my-kubeconfig
としてコピーしておき、そのパスを環境変数 KUBECONFIG に設定することで、
本番クラスタを操作できます。

便宜上、今回のマニフェスト (deployment.yml) は

containers:
  - name: myapp
    image: torico/myapp
    imagePullPolicy: Never

としており、ローカルの docker イメージを使う設定となっていますが、実際はECR からプルするため

containers:
  - name: manga-master
    image: 000000000.dkr.ecr.ap-northeast-1.amazonaws.com/torico/myapp
    imagePullPolicy: Always

となります。

これは EKS 内での起動を想定しているため、認証の設定がありません。
EKS ではなく独自にクラスタを立てている場合は、下記のように認証設定を行います。

containers:
  - name: manga-master
    image: 000000000.dkr.ecr.ap-northeast-1.amazonaws.com/torico/myapp
    imagePullPolicy: Always
imagePullSecrets:
  - name: ecr-credeintial

このようにして、ecr-credential を作る時は、python スクリプトでこのように作っています。

(aws ecr get-login の結果を、kubectl create secret する)

#!/usr/bin/env python3

import subprocess

namespace = 'torico'
secret_name = 'ecr-credeintial'
aws_region = 'ap-northeast-1'
docker_server = 'https://00000000.dkr.ecr.ap-northeast-1.amazonaws.com'


def main():
    output = subprocess.check_output([
        '/snap/bin/aws', 'ecr', 'get-login',
        '--no-include-email', '--region', aws_region,
    ]).decode()
    words = output.split()
    username = words[words.index('-u') + 1]
    password = words[words.index('-p') + 1]

    command = [
        '/snap/bin/kubectl', '-n', namespace, 'delete',  'secret', secret_name]
    subprocess.run(command)

    command = [
        '/snap/bin/kubectl', '-n', namespace, 'create', 'secret',
        'docker-registry', secret_name,
        f'--docker-username={username}',
        f'--docker-password={password}',
        f'--docker-server={docker_server}'
    ]
    subprocess.run(command)


if __name__ == '__main__':
    main()
6
7
0

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
6
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?