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 を使ってのデバッグ実行が試せます。
ディレクトリ構成
- app
- manage.py
- myapp
- settings
- local.py
- production.py
- settings
- 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 のランディングページが表示されます。
ディレクトリ内容
image_name=torico/myapp
container_name=myapp
共通で使う、Docker イメージ名などを定義しています。
#!/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
Docker Compose
→ New
→ Docker for mac
Configuration file(s)
に、docker/docker-compose.yml
を指定
Service
に myapp
を指定
Python interpreter path:
は、python
から python3
に変更しておく。
Alpine linux では、python コマンドでは python3 が起動できないためです。
設定したら [OK]
をクリック。
すると、先程作った Docker イメージ内の Python3.8環境が認識されます。
Path mappings も設定します。
プロジェクト内の app
ディレクトリを、/var/src/app
にマッピングします。
PyCharm での Project Structure の設定
⌘+,
→ Project Structure
Django ディレクトリを Sources に追加
PyCharm での Django Support の設定
⌘+,
→ Django
Enable Django Support
にチェック
Django project root
は、django
ディレクトリ
Settings
には、myapp/settings/local.py
を設定
実行コンフィギュレーションの作成
右上の、Edit Configurations
より
+
→ Django Server
→ Host
に 0.0.0.0
を指定して、環境を作ります。
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]
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()