Django アプリケーションをプロダクションで運用する際の覚書

これは [Django Advent Calendar 2018](https://qiita.com/advent-calendar/2018/django) の 12/20 の記事です。

今回はこれまで Django アプリケーションをプロダクションで運用する際にやっておいてよかったこと、もしくはやっていなかったために苦労したことを覚書としてご紹介したいと思います。

- [The Twelve Factor](https://12factor.net/ja/) アプリケーションへの準拠
- ウォームアップ

の2点です。

## The Twelve Factor アプリケーションへの準拠

[The Twelve Factor](https://12factor.net/ja/) アプリケーションは Heroku の中の人が提唱したアプリケーションの構築方法です。
ひとことでまとめると *実行環境を問わないポータブルなアプリケーション* にしましょうというものです。
例えば Django は開発サーバーがインクルードされていますが、プロダクションと同じ環境がローカルでも簡単に (`manage.py runserver` で) 動作可能に構成します。
この構成へ準拠しておくと開発や運用のしやすさだけでなく、開発効率にとっても大きなメリットだと感じています。

私が Django で The Twelve Factor アプリケーションを構成するために実践している手法を2つ紹介したいと思います。

### 環境変数により挙動を変える

これは12項目のうち 'コードベース', '設定', 'バックエンドサービス' に該当します。

- 一つのコードベースを複数の環境 (開発、ステージング、プロダクションなど) へデプロイする
- 各環境の設定の差異については環境変数で制御する
- バックエンドサービスをアタッチされたリソースとして差し替え可能にする

これは [django-environ](https://github.com/joke2k/django-environ) の導入がおすすめです。
Django アプリケーション構成のベストプラクティス構成が詰まった [Cookiecutter Django](https://github.com/pydanny/cookiecutter-django) アプリでも使われています。

例えばデーターベースを環境変数から取得するように、

```python
# settings.py

DATABASES = {
    'default': env.db(default='sqlite://'),
}
```

とします。
プロダクションでは環境変数に

```sh
DATABASE_URL=mysql://user:password@127.0.0.1:3306/dbname
```

を追加し、プロダクションで利用するデーターベースをアタッチします。
ローカルで開発する際はでは `default` の SQLite がアタッチされます。

コードベースを変えずに、環境変数のみで動作が変わるポータブルなアプリケーションとなりました。
これによりアプリケーション開発者はアプリケーションの開発に集中し、インフラ担当者は環境構築 (そして環境変数にセット) に集中することができます。

### すべてのバックエンドをモックする

これは12項目のうち 'バックエンドサービス', '開発/本番一致' に該当します。

- 開発、ステージング、本番環境をできるだけ一致させた状態を保つ

上記でも触れましたが、初期コストは (場合によってはかなり) かかりますが利用しているバックエンドは全てローカルでモックし、差し替え可能にしています。
Django は標準でデーターベース、キャッシュ、メール配信などのバックエンドサービスが差し替え可能です。
しかし、Django がサポートしていないバックエンドを利用することも多いと思います。
私は AWS 上の DynamoDB や Kinesis 系などのマネージドサービスをバックエンドとして選択することが多いですが、これらも以下のサービスを利用し差し替えています。

- DynamoDB: [DynamoDB ローカル](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DynamoDBLocal.html)
- boto/boto3: [moto](http://docs.getmoto.org/)

残念ながら moto のサポートは完璧ではありません。
特に DynamoDB などは 'Operation' のサポートがほぼないため、公式の DynamoDB ローカルの利用がおすすめです。
(ちょっとコントリビュートを頑張ってみましたが心が折れたので、是非どなたか挑戦してみてください)

アプリケーションを特定のバックエンドに依存させ、ポータブル性が損なわれると開発効率に影響します。
例えばアプリケーションをクレジットカード決済サービスに依存させ、共通開発環境にデプロイしないと動作検証ができない場合などは、共通開発環境の取り合いが発生するかもしれません。
その場合も決済サービスをバックエンドサービスとして差し替え可能にしておくと、さくっと起動し、さくっと動作検証できます。

## ウォームアップ

特にランタイムに PyPy を利用している場合は、導入されているサービスも多いと思います。
しかしランタイムとして CPython を利用している場合においてもウォームアップは重要です。
Django アプリケーションは初回リクエスト時にモジュールのインポートや初期化が実行されます。
例えば `apps.py` でシグナルへのコネクト等の前処理を実施しているなどなど。
また、Django に限らず Python のライブラリーの中には `import` 時に初期化されるものも多くあります。
そのためカジュアルにデプロイやプロセスの再起動が実行される環境ではウォームアップは必要不可欠です。

参考) <https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#dealing-with-ultra-lazy-apps-like-django>

これは ISUCON とかでも有効な手段だと思います :)

### ウォームアップ方法

まずウォームアップ用のページ (例では `/warmup/`) を用意します。

```python
# views.py

from django.db import connections
from django.http import HttpResponse
from django.views import View


class WarmUpView(View):
    def get(self, request):
        # Connect to Database
        for database_name in [
            'default',
        ]:
            connections[database_name].ensure_connection()

        # Warm up scripts here

        return HttpResponse('Warmed')
```

```python
# urls.py

from django.conf.urls import url

from .views import (
    WarmUpView,
)


urlpatterns = [
    url(
        r'^warmup/$',
        WarmUpView.as_view(),
    ),
]
```

次に、uWSGI 等のアプリケーションサーバーがロードする `wsgi.py` に以下のように追記し、 `/warmup/` ページをロードします。
以下では、Django の `startproject` が生成する `wsgi.py` にウォームアップページのロードを追加しています。

```python

# wsgi.py

import os
import sys

from django.core.wsgi import get_wsgi_application


os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings")

application = get_wsgi_application()

# Call warmup script
# https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#dealing-with-ultra-lazy-apps-like-django
application({
    'REQUEST_METHOD': 'GET',
    'SERVER_NAME': '127.0.0.1',
    'SERVER_PORT': 80,
    'PATH_INFO': '/warmup/',
    'wsgi.input': sys.stdin,
}, lambda x, y: None)
```

`/warmup/` ページは負荷が高い場合が多いため、外部からのアクセスは遮断しておきましょう。

```
  # nginx でのアクセス遮断例
  location /warmup {
    return 444;  # Close connection
  }
```

### ウォームアップ対象

ウォームアップには以下のようなものを含めるとよいでしょう。

- ロードが重たいライブラリーの `import` 処理
- バックエンドサービスへのコネクション確立、及び前処理
- その他マスターデーターのメモリーへのロードなど

例として、 [PynamoDB](https://pynamodb.readthedocs.io/) (DynamoDB バックエンド) と [Celery](http://celeryproject.org/) (SQS バックエンド) のウォームアップ方法を紹介します。

#### PynamoDB のウォームアップ

例えば、以下のようなテーブルを利用しているとします。

```python
# models.py

from pynamodb.models import Model


class Thread(Model):
    class Meta:
        table_name = 'Thread'

    pass
```

PynamoDB は初回 API コール時に [`DescribeTable`](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DescribeTable.html) API がコールされます。
この API は `ItemCount` なども取得しており、なかなかに時間がかかる場合があります。
以下のように、事前にメタデーターを取得しておくことで、初回ロード、およびプロセス再起動時も通常通りのレイテンシーでレスポンスが返ります。

```python
# views.py

from .models import (
    Thread,
)


class WarmUpView(View):
    def get(self, request):
        # Warmup DynamoDB
        Thread._get_meta_data()
```

#### Celery (SQS バックエンド) のウォームアップ

Celery も大きなライブラリーのため、ウォームアップ時に `import` しておくと良いでしょう。
特にバックエンドに SQS を利用している場合は PynamoDB 同様初回リクエスト時に [`ListQueues`](https://docs.aws.amazon.com/AWSSimpleQueueService/latest/APIReference/API_ListQueues.html) API がコールされます。
他にいい方法があれば教えていただきたいですが、私はウォームアップ用に以下のようなダミーパラメーターを用意しています。

```python

# tasks.py

from myprojct import celery

@celery.task(bind=True)
@retry_when_retryable
def my_task(self, arg1=None, arg2=None, **kwargs):
    if arg1 == 'warmingup':
        return
```

こちらも同様にウォームアップ時に一度コールしておく事により初回ロード、およびプロセス再起動によるレイテンシーへの影響を最小限に抑えることができます。

```python
# views.py

from myprojct import celery


class WarmUpView(View):
    def get(self, request):
        # Warmup Celery / SQS
        task_args = ()
        task_kwargs = {
            'arg1': 'warmingup',
        }
        celery.send_task('my_task', task_args, task_kwargs)
```

話はそれますが、以下の事由からタスクはキーワード引数のみの利用がおすすめです

- デプロイ前後にタスクキューに以前のパラメーターのタスクが残っている可能性がある
- ローリングアップデートなど、新旧タスクの処理が並列に実行される可能性がある
- キーワードなし引数の場合は引数の個数が変わった際にエラーとなる

## まとめ

最初の The Twelve Factor アプリケーションについては、開発のしやすさ、運用のしやすさ双方においてメリットを感じています。
また、以下のような恩恵も実感しています

- (初期コストはかかりますが) 働き方が多様化しても、ローカルでさくっと本番同等の環境が起動できるため、リモートだろうが時差があろうがコントリビュートしやすい
- 問題が起きた時に手元で再現確認しやすい
- 複数リージョンなどでデプロイしている場合もコードベースが汚れにくい (昔は `settings/production/region_a.py` とかやってましたね)

ウォームアップについては処理に時間がかかっているものを発見しないといけません。
これは開発時に発見するのはなかなか難しいので、負荷試験や APM などによるモニタリングが大切です。

もし他にプロダクションで Django を動かすための Tips などがあったら是非教えてください。

最後に現金2万円で査読してれた優しい同僚の皆様に感謝します。

メリークリスマス :)