【Django】DjangoによるWeb開発プロジェクトPart7  - クラウドコンテナホスティングサービス jaistingをリリースするまで -

## はじめに

お久しぶりです。大分ゴタゴタしていましたが、Django改めクラウドコンテナホスティングツールの開発から得られた知見を執筆したいと思います。
今回は、いよいよ本番環境へデプロイする所までやってみたいと思います。

## Jaistingについて

改めての説明となりますが、クラウドコンテナホスティングシステム「Jaisting」についての説明です。
JaisingはFreeBSDの準仮想化システムである「jail」を利用したクラウドコンテナホスティングのシステムです。
昨今、流行りのKubernetesにどうしても馴染めなかった事、VMによるクラスタリングとコンテナによるクラスタリングの違い・優位性が分からなかった事、どうせやるなら自分の慣れている環境でコンテナを取り扱うフレームワークのものを作りたかった等様々な理由があり開発に至りました。

開発経緯の詳しい説明についてはこちら→[https://note.mu/himrock922/n/n8e35b1d70e19](https://note.mu/himrock922/n/n8e35b1d70e19)

最近、ゴタゴタしていましたが、そろそろパブリックにアクセスできる環境を用意しても良いかなという事で今回、ついにリリースする事にしました。

今回は、中身の説明よりもDjangoによる本番環境へリリースするためのノウハウを執筆したいと思います。

なお、この記事だけでは不足している箇所もあったりするのですが、何分ホストOSがFreeBSDという事もあり、中々全部をやるにあたっての難易度(というより準備するものが多すぎる感)が高い事もあり、省略している部分もありますが、その部分は適宜追加していければと考えています。

それではいってみましょう。

## WSGIによるDjango起動

Pythonの標準ライブラリである`WSGI(Web Server Gateway Interface)`というものがあります。Djangoでは、`startproject`のコマンドを実行した時にシンプルな`wsgi.py`と言う設定ファイルを用意してくれます。

中身は以下のようなものです。

```python:wsgi.py
"""
WSGI config for jaisting project.

It exposes the WSGI callable as a module-level variable named ``application``.

For more information on this file, see
https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/
"""

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'jaisting.settings')

application = get_wsgi_application()
```

中身の設定なのですが、後々`gunicorn`と言ったWebサーバツール経由でサーバを起動させるので、その時に一緒に見ます。

## Gunicornのインストール

`Gunicorn`(なんて読むこれ? ジーユニコーン?)は、UNIX向けのPython WSGI HTTPサーバです。Rubyにも`unicorn`と言ったワーカープロセス形式で実行できるプロジェクトがありますが、Gunicornはそれを移植したモデルです。
今回はこのツールをインストールしてデプロイを行います。

```bash
% pip install gunicorn
```

GunicornはWSGI HTTPSサーバであるため、WSGIアプリケーションの設定を含んだモジュールの指定が必要となります。前述のwsgi.pyを利用することでgunicorn経由での実行が可能となります。

```bash
% gunicorn jaisting.wsgi -b ip_address:port_number -D
```

`-D` オプションでデーモンモードとして起動させます。このままgunicornで直接アクセスすることも可能ですが、これも結局、WSGIはあくまでも、WebサーバとDjangoの橋渡しを行うためのインタフェースという立ち位置でもあり、本番環境運用するのであれば、別個Webサーバを用意して、Webサーバ経由でDjangoアプリケーションにアクセスできるようにします。

## Nginxのインストール

```bash
% sudo pkg install nginx
```

FreeBSDホスト側では、Webアプリケーションは一つしか起動させない予定ですが、一先ず設定ファイルが煩雑化しないように、VirtualHostでドメイン固有毎に設定を区切ります。

```nginx:nginx.conf
http {
 (中略)
 include /etc/nginx/conf.d/*.conf;
}
```

```nginx:/usr/local/etc/nginx/conf.d/jaisting.conf
server {
    listen 80 default_server;
    listen [::]:80;
    server_name jaisting.dev;

    # Redirect all HTTP requests to HTTPS with a 301 Moved Permanently response.
        #return 301 https://$host$request_uri;
    location /static {
            alias /home/himrock922/server/jaisting/public;
    }       
    location / {
                    proxy_redirect     off;
                    proxy_set_header   X-FORWARDED_PROTO http;
                    proxy_set_header   Host              $http_host;
                    proxy_set_header   X-Real-IP         $remote_addr;
                    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
                    proxy_pass http://0.0.0.0:8080;
    }
}
```

一旦、gunicornを8080番ポートで動くWebサーバとして起動させ、Nginxでリバースプロキシを設定しています。

```nginx
    location /static {
            alias /home/himrock922/server/jaisting/public;
    }       
```

`/static`ディレクトリはDjangoで画像、CSS、JavaScript等のレンダリングに必要なリソースを提供する場所です。今回のプロジェクトでは、webpackで一括管理しているため、publicディレクトリに置いています。しかし、本番環境の場合、`/static`ディレクトリに一箇所に集めて、コンパイルしたものを提供するため、本番環境では必然的に/staticディレクトリを参照するようになります。

今回は、Django側で作成したリソースは一切存在しないため、`/static`をpublicのaliasアクセス先として設定しています。他にも画像をアップロードする場所として`image`ディレクトリがあるのですが、今回のアプリケーションでは画像アップロードの機構は使わないため、一旦省略しています。

## webpackのproduction環境の設定

webpackにおけるdevelopmentモードでのコンパイルは、その名の通り開発者用に可読性の高い一つのJSファイルとしてコンパイルしてくれますが、これをそのまま本番環境で運用すると、サイズが大きくなってしまい、ブラウザの読み込む速度が遅くなります。
そこで、本番環境用にJSファイルのサイズを限りなく小さくし、読み込み速度を極力早くするようにします。

一番手取り早い方法は`webpack-merge`を利用することかと思われます。

```bash
yarn add webpack-merge
```

コンパイルする対象のファイルは共通化させ、Dev環境と本番環境でコンパイルするサイズを変化できるようにします。具体的には、

```js:webopak.config.js
// webpack.config.js
var path = require('path')
var BundleTracker = require('webpack-bundle-tracker')
var VueLoaderPlugin = require('vue-loader/lib/plugin')

module.exports = {
    entry: './frontend/packs/application.js',
    output: {
        path: path.resolve(__dirname, './public/bundles/'),
        filename: "[name]-[hash].js",
    },
    plugins: [
        new BundleTracker({filename: './webpack-stats.json'}),
        new VueLoaderPlugin(),
    ],
    module: {
        rules: [
            {
                test: /\.js$/,
                include: [ // use `include` vs `exclude` to white-list vs black-list
                    path.resolve(__dirname, "node_modules"), // white-list your app source files
                    require.resolve("bootstrap-vue"), // white-list bootstrap-vue
                ],
                loader: "babel-loader"
            },
            {
                test: /\.css$/,
                use: ['style-loader', 'css-loader']
            },
            {
                test: /\.vue$/,
                use: ['vue-loader']
            }
        ]
    },
    resolve: {
        alias: {
            'vue$': 'vue/dist/vue.esm.js',
            'vuex$': 'vuex/dist/vuex.js'
        }
    }
}
```

上の`webpack.config.js`でコンパイルするファイルやloaderプラグインを決め、

```js:webpack.dev.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

module.exports = merge(common, {
  mode: 'development',
  devtool: 'inline-source-map',
  devServer: {
    contentBase: './dist',
  },
});
```

```js:webpack.prod.js
const merge = require('webpack-merge');
const common = require('./webpack.config.js');

module.exports = merge(common, {
  mode: 'production',
});
```

上二つのファイルでDev環境、本番環境用にコンパイル設定を分けます。なお、webpack4から、modeの指定が可能となり、productionモードを指定することにより、必要なプラグインが有効化され、ファイルサイズを縮小することができます。

また、`packages.json`にscriptタグを記述することで、npmやyarnといったパッケージマネージャ経由でコンパイルを実行することができます。

```js:packages.json
  "scripts": {
    "start": "webpack --config webpack.dev.js",
    "build": "webpack --config webpack.prod.js"
  }
```

```bash
% yarn build
```

![スクリーンショット 2019-11-10 21.49.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/144991/4b22b1fe-3f26-2d0d-0ced-545220b5136b.png)

## 本番環境用の環境変数を用意する

本番環境用に設定したい環境変数に関してはリポジトリにはコミットせず、サーバ側に置きます。
Railsではdotenvというものがありましたが、Djangoには`django-environ`というツールがあるのでそれを使います。

```bash
pip install django-environ
```

インストール後は

```bash:.env
SECRET_KEY=xxxxxx
DATABASE_ENGINE=xxxxx
DATABASE_NAME=xxxxxx
DATABASE_USER=xxxxx
PASSWORD=xxxxx
HOST=xxxx
```

.envファイルを作成し、必要な環境変数を記述します。
そして、envの変数を必要な箇所に紐付けます。

```python:settings.py
import environ
env = environ.Env(
    # set casting, default value
    DEBUG=(bool, False)
)
environ.Env.read_env()

(中略)

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = env('SECRET_KEY')

# Database
# https://docs.djangoproject.com/en/2.1/ref/settings/#databases

DATABASES = {
    'default': {
        'ENGINE': env('DATABASE_ENGINE'),
        'NAME': env('DATABASE_NAME'),
        'USER': env('DATABASE_USER'),
        'PASSWORD': env('PASSWORD'),
        'HOST': env('HOST')
    }
}

```

## Djangoを本番環境に運用する際のチェックリスト

Djangoには`manage.py check --deploy`によって、本番運用する際に必要な設定が整っているかチェックすることができるコマンドがあります。主にはセキュリティインシデントに引っ掛からないような設定になるかどうかを最低限確認することができます。

```bash
% manage.py check --deploy
WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. If your entire site is served only over SSL, you may want to consider setting a value and enabling HTTP Strict Transport Security. Be sure to read the documentation first; enabling HSTS carelessly can cause serious, irreversible problems.
?: (security.W006) Your SECURE_CONTENT_TYPE_NOSNIFF setting is not set to True, so your pages will not be served with an 'x-content-type-options: nosniff' header. You should consider enabling this header to prevent the browser from identifying content types incorrectly.
?: (security.W007) Your SECURE_BROWSER_XSS_FILTER setting is not set to True, so your pages will not be served with an 'x-xss-protection: 1; mode=block' header. You should consider enabling this header to activate the browser's XSS filtering and help prevent XSS attacks.
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. Unless your site should be available over both SSL and non-SSL connections, you may want to either set this setting True or configure a load balancer or reverse-proxy server to redirect all connections to HTTPS.
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. Using a secure-only session cookie makes it more difficult for network traffic sniffers to hijack user sessions.
?: (security.W016) You have 'django.middleware.csrf.CsrfViewMiddleware' in your MIDDLEWARE, but you have not set CSRF_COOKIE_SECURE to True. Using a secure-only CSRF cookie makes it more difficult for network traffic sniffers to steal the CSRF token.
?: (security.W019) You have 'django.middleware.clickjacking.XFrameOptionsMiddleware' in your MIDDLEWARE, but X_FRAME_OPTIONS is not set to 'DENY'. The default is 'SAMEORIGIN', but unless there is a good reason for your site to serve other parts of itself in a frame, you should change it to 'DENY'.
```

このように、色々出てきますね。ただし、今回はNginxのリバースプロキシ経由でアクセスさせたいので、Nginx側で設定した方が楽な設定もあったりするので、一概にこれ全部をDjango側で設定しなければいけないというものでもありません。あくまでこれは目安程度で考えていただければいいかと。

## gunicornをUNIX domain socketで起動させる

gunicornをUNIX domain socketで起動させます。
ここで、もう手動でコマンド実行するのも煩わしいので設定ファイルを作成します。

以下はサンプルとしたgunicorn.conf.pyの一部を変えたものです。

```python:gunicorn.conf.py
bind='unix:/var/run/****.sock'
workers = 3
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 2
daemon = True
pidfile = '/****/jaisting.pid'
umask = 0
(中略)
```

Nginxの方も適宜変えます。

```nginx:/usr/local/etc/nginx/conf.d/jaising.conf
server {
    listen 80 default_server;
    listen [::]:80;
    server_name jaisting.dev;

    location /static {
            alias /var/www/jaisting/public;
    }       

    location / {
            try_files $uri @proxy;
    }
    location @proxy {
                    proxy_redirect     off;
                    proxy_set_header   X-FORWARDED_PROTO http;
                    proxy_set_header   Host              $http_host;
                    proxy_set_header   X-Real-IP         $remote_addr;
                    proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
                    proxy_pass http://app;
    }

}

upstream app {
        server unix:/var/run/gunicorn.sock;
}
```

## SSL証明書の導入

Let's Encryptによる証明書を発行し、Nginxの設定を書き換えます。
今回の記事の本質ではないので省略。

## Farbic3によるデプロイ

デプロイのために別個FreeBSDマシンを用意しなくてはならないので省略

## 本番環境

[https://jaisting.dev/](https://jaisting.dev/)

## まとめ

皆様のおかげでようやくここまでこれました。これからも執筆と開発を続けていきます。
なお、肝心の接続アカウントですが、コンテナホスティングという事もあり、スーパーユーザで起動させているところもあるため、アカウント登録は一先ずワンクッション置きたいと思います。
ご興味ある方はぜひ、ご連絡をいただけると幸いです!

## シリーズ

[Part6.5](https://qiita.com/himrock922/items/5b8f39a61de9b640d840) ← [Part 7](https://qiita.com/himrock922/items/28dc4d0178fc0bba81a9) → [Part 8](https://qiita.com/himrock922/items/05d21415f4eba1c82fc5)

## 参考文献

* [https://docs.djangoproject.com/ja/2.2/howto/deployment/](https://docs.djangoproject.com/ja/2.2/howto/deployment/)
* [https://github.com/benoitc/gunicorn](https://github.com/benoitc/gunicorn)
* [https://docs.djangoproject.com/ja/2.2/howto/deployment/wsgi/modwsgi/](https://docs.djangoproject.com/ja/2.2/howto/deployment/wsgi/modwsgi/)
* [https://docs.djangoproject.com/ja/2.2/intro/tutorial06/](https://docs.djangoproject.com/ja/2.2/intro/tutorial06/)
* [https://webpack.js.org/guides/production/](https://webpack.js.org/guides/production/)
* [https://webpack.js.org/configuration/mode/](https://webpack.js.org/configuration/mode/)
* [https://github.com/joke2k/django-environ](https://github.com/joke2k/django-environ)
* [https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/](https://docs.djangoproject.com/en/2.2/howto/deployment/checklist/)
* [https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py](https://github.com/benoitc/gunicorn/blob/master/examples/example_config.py)