DjangoアプリをAmazon Linux+Apache+mod_wsgiで公開する

#この記事について
この記事では、Djangoで作成したWebアプリを、実際に本番環境にデプロイ(=Webサーバーにのせて公開)するまでの流れを紹介します。

##使用する環境・バージョン
- OS : Amazon Linux AMI release 2018.03
- Webサーバー : Apache/2.4.41 (Amazon)
- Python 3.6.8
- Django 2.2.7
- mod_wsgi 4.6.8

##前提条件
- Djangoアプリは、ローカル環境で動くところまで完成しているものとします。具体的には、`python manage.py runserver`コマンドを使うことで、localhost:8000にで問題なく動かせるところまで既にできていることとします。
- ssh接続ができるEC2インスタンスをAWSで作成済みであることとします。
- EC2サーバーの中にApacheをインストールし、ブラウザでドキュメントルート直下にアクセスするとテストページが表示される状態であることとします。
- Githubアカウントを所持しており、またEC2インスタンスにgitがインストールされている状態であることとします。

##読者に要求する前提知識
- 基本的なコマンドラインを扱えること。
- gitの基本的な操作ができること。
- Djangoの基本的な知識があること。

#Djangoアプリのデプロイ工程
ここからは、作成済みのWebアプリを実際に本番環境にのせて公開するまでの手順を解説していきます。

##0.サーバー内のディレクトリ・ファイル構造
今回は、以下のようにディレクトリ・ファイルを置いていくことを目指します。
(注：今回無関係なところは一部省略しています)

```
/
├─var
│  └─www
│     └─html  #apacheのドキュメントルート
├─etc
│  └─httpd
│     ├─conf
│　　　│  └─httpd.conf  #apacheの設定ファイル
│     └─conf.d
│        └─django.conf #Djangoを動かすための追加設定ファイル
└─home
   └─ec2-user  #インスタンスにssh接続して最初にいるディレクトリはここ
      └─Django
         ├─env  #Djangoアプリを動かすのに使用する仮想環境
         └─project  #Djangoプロジェクト
            │ manage.py
            ├─static
            ├─project
            │  │  settings.py
            │  │  local_settings.py
            │  └─ wsgi.py
            └─app
               ├─templates
               └─static
```

##1.ローカル環境のDjangoアプリをGithubにアップロード
### gitのローカル/リモートリポジトリの作成
今回は、Githubのリモートリポジトリ経由でサーバー内にDjangoアプリを持ってくることにします。
まずは、GithubのWebサイトでリモートリポジトリを作成してください。
その後は、Djangoのプロジェクト直下で以下のコマンドを使い、作成したリモートリポジトリに紐ずいたローカルリポジトリを作成してください。

```
$ git init
$ git remote add origin [リモートリポジトリのアドレス]
```

###git管理対象外にする設定ファイル(local_settings.py)の作成
ここで、ローカルリポジトリの内容をリモートにプッシュする前にしておかなければいけないことがあります。

Djangoアプリには、ローカル環境とリモート環境で変えておかなければいけない設定があります。例えばproject/settings.py内には、DEBUGという変数があります。

```(local)project/settings.py
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
```
これは、Trueだとhttpエラーの際に詳しいエラー箇所をブラウザ上に表示してくれるものです。しかし、これを本番環境でONにしていると、サーバーの詳しい情報が第三者にダダ漏れになってしまうのでよろしくありません。そのため、本番環境ではFalseにしておくことが推奨されています。

また、同じくproject/settings.py内には、SECRET_KEYという変数があります。

```(local)project/settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'pej^q2ql$$#bzg#crh6k_9(p=%p)&6x(kwh@nos&=!$ej&60fh' #example
```
これは暗号化やハッシュ化で用いられる秘密鍵を指定するものなので、プロジェクト外に公開してはいけません。つまり、この値が書いてある設定ファイルをGithubのリモートリポジトリにプッシュしてはいけません。

したがって、

- ローカル環境と本番環境で値を変える設定
- 公開したくない機密情報(APIキーやDBパスワードなど)を含む設定

の情報を、通常のproject/settings.pyとは違うファイルに記載し、git管理対象外にします。

projectファイル内に、新たにlocal_settings.pyというファイルを作成し、git管理したくない設定を記入します。

```(local)project/local_settings.py
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'pej^q2ql$$#bzg#crh6k_9(p=%p)&6x(kwh@nos&=!$ej&60fh' #example

# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
```
その後、project/settings.pyファイルから、local_settings.pyに記載した情報を削除します。
その上で、local_settings.pyの情報を読み込ませるために、上部に以下の一行を追記してください。

```
from .local_settings import *
```
これで設定ファイルは完璧です。そうしたら、作成したlocal_settings.pyをgit管理対象にしましょう。

```(local)project/settings.py
$ vi .gitignore
 #以下を書き込む
 local_settings.py
```
###リモートリポジトリにプッシュ
これでプッシュの準備が整ったので、リモートリポジトリにソースコードをアップしましょう。

```
$ git push origin master
```

##2.EC2インスタンス内でのPython環境構築
###3系Pythonのインストール
Amazon Linuxにデフォルトで入っているPythonは2系なので、3系を手動でインストールしてやる必要があります。今回は、Python3.6とそれに対応するdevelツールを`yum`コマンドでインストールします。

```
$ sudo yum install python36 python36-devel
```
こうすることで、サーバーの中に3系Pythonが入ります。
ここで注意することは、ただ単に`python`とコマンドを叩くとデフォルトの2系が使われてしまうことです。今インストールした3系を使うためのコマンドは`python3`コマンドです。

```
$ python -V
Python 2.7.16
$ python3 -V
Python 3.6.8
```
###仮想環境の作成
今回、Djangoアプリを/home/ec2-user/Django/フォルダ下に配置することにします。そのため、このDjangoフォルダ内にenvという名前の3系Python仮想環境を作成します。

```
$ cd /home/ec2-user/Django/
$ python3 -m venv env
```
これで仮想環境が作成できました。この環境の中に入って、pipを使いDjangoとmod_wsgiをインストールします。

```
$ source env/bin/activate
$ pip install django mod_wsgi
```
Djangoアプリ内でフレームワーク・ライブラリを他に使用している場合(例:Django Rest Fremeworkなど)はそれらも忘れずにインストールしてください。
以下、この仮想環境下で作業することを前提とします。

##3.サーバー内にDjangoアプリを配置
1で準備したGithubのリモートリポジトリから、Djangoアプリを持ってきましょう。
/home/ec2-user/Django/フォルダ下に、リモートリポジトリをクローンしましょう。

```
$ git clone [リモートリポジトリのアドレス]
```
これで、無事にサーバー内にアプリを持ってくることができました。
しかし、先ほどlocal_settings.pyに退避させた設定があるので、サーバー内でのlocal_settings.pyを手動で作成してやります。

```
$ vi /project/project/local_settings.py
#以下を書き込む
SECRET_KEY = 'pej^q2ql$$#bzg#crh6k_9(p=%p)&6x(kwh@nos&=!$ej&60fh' #ローカルと同じ値にする
DEBUG = False #本番環境なのでFalseにします。
```

##4. sqlite3のバージョンアップ
ここで、サーバー内の環境できちんとDjangoが動くかどうか`python manage.py runserver`で確認しましょう。
この際、問題なく起動できる場合は4は飛ばして5に進んでください。
ここで、sqlite3のバージョンエラーが出る場合があります。

```
$ python manage.py runserver
(省略)
django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).
```
このときは、3.8.3以降のsqlite3をインストールしてやる必要があります。
`yum`でやるとうまくいかないという情報があったので、`wget`で地道にやっていくことにします。
(ソース：[【django】SQLiteバージョンエラー](https://teratail.com/questions/194159))

```
#新バージョンのsqlite3ソースを取得
$ wget https://www.sqlite.org/2019/sqlite-autoconf-3300100.tar.gz
$ tar xzf ./sqlite-autoconf-3300100.tar.gz 
$ rm -f ./sqlite-autoconf-3300100.tar.gz

# ビルドしてインストール
$ cd ./sqlite-autoconf-3300100/
$ ./configure --prefix=/usr/local
$ make
$ sudo make install
```
参考：[Django2.2で開発サーバー起動時にSQLite3のエラーが出た場合の対応](https://qiita.com/rururu_kenken/items/8202b30b50e3bfa75821)

インストールが成功しているかどうか確認します。

```
#インストール先の確認
$ sudo find /usr/ -name sqlite3
/usr/lib64/python2.6/sqlite3
/usr/lib64/python3.6/sqlite3
/usr/lib64/python2.7/sqlite3
/usr/bin/sqlite3
/usr/local/bin/sqlite3

#バージョン確認
$ /usr/bin/sqlite3 --version
3.30.0 2019-10-10 20:19:45 18db032d058f1436ce3dea84081f4ee5a0f2259ad97301d43c426bc7f3df1b0b
```
無事に3.8.3以降のsqlite3を入手できました。旧sqliteの名前を変更→新sqliteのシンボリックリンクを作成して、`sqlite3`コマンドが新しいバージョンのものを使うようにします。

```
$ sudo mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
$ sudo ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
```

また、インストールした新バージョンのsqlite3を共有ライブラリに加えるため、パスを通します。

```
$ vi /etc/ld.so.conf.d/sqlite3-x86_64.conf
#以下を書き込む
/usr/local/lib
```
共有ライブラリへのパスの通し方として、環境変数LD_LIBRARY_PATHを更新する方法もありますが、LD_LIBRARY_PATHの設定を下手な場所で定義しても反映されません。なので大人しくld.so.conf.dでパスを定義しました。
参考：[LD_LIBRARY_PATH を設定しても反映されないことがある](https://kokufu.blogspot.com/2016/01/ldlibrarypath.html)

([上で参考にあげた記事](https://qiita.com/rururu_kenken/items/8202b30b50e3bfa75821)ではLD_LIBRARY_PATHの設定を~/.bashrcに書き込んでいますが、これではうまくいきませんでした)

##5.Apacheとmod_wsgiの連携設定
サーバー内で無事にDjangoが起動できることを確認したら、いよいよApacheにのせて公開する準備に入っていきましょう。

まずは、先ほど入れたmod_wsgiがどこに入っているのか、パスを`find`コマンドで確認しましょう。

```
$ find -name 'mod_*.so'
./env/lib/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so
```
このmod_wsgi-py36.cpython-36m-x86_64-linux-gnu.soがmod_wsgiの本体です。今調べたパスは後で使うのでメモしておいてください。

次に、Apacheに設定を追加します。以下のコマンドで、mod_wsgiについて記載する設定ファイルを新たに作ってください。

```
$ sudo vi /etc/httpd/conf.d/django.conf
```
こうすると、viが起動してファイル内の編集ができるようになります。ここで、以下の設定を記入してください。

```/etc/httpd/conf.d/django.conf
LoadModule wsgi_module /home/ec2-user/Django/env/lib64/python3.6/site-packages/mod_wsgi/server/mod_wsgi-py36.cpython-36m-x86_64-linux-gnu.so

WSGIScriptAlias / /home/ec2-user/Django/project/project/wsgi.py
WSGIPythonPath /home/ec2-user/Django/env/lib/python3.6/site-packages
WSGIPythonHome /home/ec2-user/Django/env

<Directory /home/ec2-user/Django/project/project>
<Files wsgi.py>
Require all granted
</Files>
</Directory>
```
設定の意味は以下の通りです。

- LoadModule : mod_wsgiの本体ファイルの位置。
- WSGIScriptAlias : 1つめのURLがアクセスされたら、2つめのwsgiスクリプトに移譲するという設定。ここでは、ApacheのルートにきたリクエストをDjangoプロジェクト内のwsgi.pyに飛ばしています。
- WSGIPythonPath : 実行中にPythonに通させるパス。
- WSGIPythonHome : Apacheに使わせるPythonのホームディレクトリー。
- Directory〜以下 : 指定のディレクトリにApacheのアクセス許可を与えます。

参考：[公式ドキュメント　How to use Django with Apache and mod_wsgi](https://docs.djangoproject.com/en/2.2/howto/deployment/wsgi/modwsgi/)

記入できたら、Apacheを再起動します。

```
$ sudo service httpd restart
```
ブラウザから自分のサイトにアクセスして、無事にDjangoで作ったプロジェクトが表示されていれば成功です！

##6.静的ファイル(css, js, img)の公開
しかし、今ブラウザで表示されている自分のプロジェクトは、css, jsが効いていない/画像が表示されていない状態だと思います。
これは、開発環境と本番環境で静的ファイルの取り扱い方が違うことに起因しています。

###静的ファイル公開の仕組み
一般に、Djangoでアプリを開発する際には静的ファイルというのは各アプリケーション内のstaticフォルダに格納することが多いはずです。

```
project
 │ manage.py
 ├─project
 │  │  settings.py
 │  │  local_settings.py
 │  └─ wsgi.py
 ├─app1
 │  ├─templates
 │  └─static #app1で使用する静的ファイル
 └─app2
    ├─templates
    └─static #app2で使用する静的ファイル
```
しかし本番環境(=設定でDEBUGがFalse)においては、静的ファイルは一箇所(一般にプロジェクト直下)に全て集めて、それをWebサーバー(Apache)が使用するという仕組みになっています。そのため、開発時のようにアプリごとにstaticが分散している状態では認識してくれないというわけです。
参考：[公式ドキュメント　静的ファイルのデプロイ](https://docs.djangoproject.com/ja/2.2/howto/static-files/deployment/)
　　　[Djangoにおける静的ファイル(static file)の取り扱い](https://qiita.com/saira/items/a1c565c4a2eace268a07)

###settings.pyの追記
Djangoには、わざわざ手動で本番環境用の静的ファイルディレクトリを作らなくても、コマンド一発でそれができる仕組みが備わっています。ここからは、それを利用してプロジェクト直下に静的ファイルを集めるための設定を加えていきましょう。

project/settings.pyに以下の設定があることを確認してください。

```project/settings.py
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/2.2/howto/static-files/
STATIC_URL = '/static/'
```
デフォルトでこの設定はあるはずです。それぞれの意味は以下の通りです。

- BASE_DIR : プロジェクトのドキュメントルート(=一番上のprojectディレクトリ)のパス。
- STATIC_URL : 静的ファイルの配信用URL。

これが確認できたら、settings.pyに以下の設定を追加してください。

```project/settings.py
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
```
STATIC_ROOTは、本番環境用の静的ファイルをどこに集めるかの設定です。ここでは「プロジェクト直下にstaticというフォルダを作る」という設定になっています。

###静的ファイルの収集
ここまでできたら準備は完璧です。コマンドを使って各アプリごとに散らばった静的ファイルを、先ほどのSTATIC_ROOTの位置にまとめましょう。
プロジェクト直下(manage.pyが実行できる位置)に移動して、以下のコマンドを実行してください。

```
$ python manage.py collectstatic
```
このコマンドを実行することで、project直下にstaticファイルが自動で生成され、その中にアプリで使用した全ての静的ファイルが格納されている状態になります。

```
project
 │ manage.py
 ├─static #新たにできるディレクトリ
 ├─project
 │  │  settings.py
 │  │  local_settings.py
 │  └─ wsgi.py
 ├─app1
 │  ├─templates
 │  └─static
 └─app2
    ├─templates
    └─static
```

###Apache側での設定を追記
次に、一箇所に集まった静的ファイルのディレクトリをApacheに認識させてやりましょう。
/etc/httpd/conf.d/django.confファイルに以下の設定を追記してください。

```/etc/httpd/conf.d/django.conf
Alias /static/ /home/ec2-user/Django/project/static/

<Directory /home/ec2-user/Django/project/static>
Require all granted
</Directory>
```
それぞれの意味は以下の通りです。

- Alias : 1つめのURLがアクセスされたら、2つめのディレクトリに移譲するという設定。ここでは、(ドメイン名)/static(=settings.pyで設定したSTATIC_ROOT)にアクセスされたら、先ほど`collectstatic`コマンドで作成した本番環境用の静的ファイルに飛ばすようになっています。
- Directory以下 : 本番環境用の静的ファイルにApacheのアクセス許可を与えます。

###Apache再起動
これで準備は万端ですので、Apacheを再起動します。

```
$ sudo service httpd restart
```
ブラウザから自分のドメインにアクセス、きちんと静的ファイルがある状態の自分のサイトが表示されていれば大成功です！