何も考えずにEC2にDjango×SQLite3×nginx×gunicornな環境を構築する

# はじめに
AWSのEC2上でDjangoプロジェクトを展開しようとして丸一日悩んでしまったので、手順をまとめました。あくまでも自己責任でお願いします。

# やったこと
EC2インスタンスを立てて何も考えずにDjangoを動かして、ネット上からアクセスできるようにする。
今回はSQLite3,nginx,gunicornを使います。

# 注意
- ec2-userをそのまま使っていたり、セキュリティ的に怪しい所があるため注意すること。**今回はAWSでdjangoプロジェクトをとりあえず動かせるようにすることを目的とする。**
- VPC、サブネット、インターネットゲートウェイの設定はすでに完了していて、EC2自体はネットから参照できる状態になっている所からスタートする
- ssh(22)、HTTP(80)とDjango用(8000)のポートが開放されている状態を前提とする
- DBはSQLite3を使用する。また、**SQLite3のバージョンを最新にするため既存のソフトウェアでSQLite3を使用している際は注意すること。**
- Djangoの管理者画面がスタイルシートなどを含めて読み込めたところで本記事のゴールとする

# 参考にした文献

- [【20分でデプロイ】AWS EC2にDjango+PostgreSQL+Nginx環境を構築してササッと公開](https://qiita.com/tachibanayu24/items/b8d73cdfd4cbd42c5b1d)
- [AWSにDjangoアプリケーションをデプロイ(Nginx, gunicorn, postgresql)](https://qiita.com/pokotsun/items/1272479e36c5146c6609)
- [AWS Amazon Linux2 で Django 2.2以降の環境を構築する](https://qiita.com/Ajyarimochi/items/674b703622155e46dc1d)
- [【AWS】EC2でWebサーバの構築 – Nginx構築 -【Nginx】](https://swallow-incubate.com/archives/blog/20191211/)
- [Nginxで躓いたところ及びメモ](https://qiita.com/ikeay/items/d72eb482b7e1db00d348)


# 必要なものを揃える

本記事ではvenvなどは使わず、そのままpython3を使っていくため注意すること。

```shell:install
# 後述処理でパスワードを求められるため設定する。また、rootを使えるようにする
$ sudo passwd ec2-user
$ sudo passwd root
$ sudo yum update
$ sudo yum install python3
# Amazon Linux 2で$sudo yum install nginx と入力するとコマンドを催促されるので、それを入力する。
$ sudo amazon-linux-extras install nginx1
$ sudo pip3 install django
$ sudo pip3 install gunicorn
```

Amazon Linux 2にもデフォルトでSQLite3がインストールされていたが、Djangoで求めるバージョン(3.8.3以降)にマッチしないため、SQLiteのアプデを実施する。

```shell:SQLite3のinstall
# 後述のSQLite3インストール時のmakeファイルのビルドに使用する。
$ sudo yum install gcc
# 最新版のSQLite3をインストールする。執筆当時は3.33.0が最新だった。(https://www.sqlite.org/download.html)
# 展開した後にmakeファイルをビルドしてmake installまで実施する
$ wget https://www.sqlite.org/2020/sqlite-autoconf-3330000.tar.gz
$ tar zxvf sqlite-autoconf-3330000.tar.gz
$ cd sqlite-autoconf-3330000
# makeファイルのビルド
$ ./configure --prefix=/usr/local
$ sudo make
$ sudo make install
```

次に、過去に入っていたSQLite3から現行のSQLite3を使用するように設定を通す。

```shell:SQLite3のinstallつづき
# すでにインストール済みのsqlite3を確認する
$ sudo find / -name sqlite3
# もともとあったやつは多分これ。バージョンが古いことを一応確認しておく。
/usr/bin/sqlite3　--version
# 今回インストールしたのは多分これ。バージョンが古いことを一応確認しておく。
/usr/local/bin/sqlite3　--version
# 古い方のsqlite3のフォルダ名を変え、また新しい方のsqlite3へパスを設定しておく
sudo mv /usr/bin/sqlite3 /usr/bin/sqlite3_old
sudo ln -s /usr/local/bin/sqlite3 /usr/bin/sqlite3
# ライブラリへのパスを通しておく
su
sudo touch /etc/ld.so.conf.d/sqlite3.conf
sudo echo /usr/local/lib >> sqlite3.conf
# rootからログアウト
exit
# 過去のsqlite3へのリンクとディレクトリを削除
$ sudo rm /lib64/libsqlite3.so.0
$ sudo rm -rf /lib64/libsqlite3.so.[過去のバージョン]
# ライブラリのパスを更新する
sudo ldconfig
# 新しいライブラリへのパス(/usr/local/lib)が通っているか確認する
$ ldconfig -p | grep sqlite
# 一応バージョンも確認する
sqlite3 --version
```
最後のバージョン確認で現行のSQlite3のものが出力されれば、次に進む。

# Djangoの設定
とりあえずプロジェクトを作る。プロジェクト名は公式ドキュメントでおなじみのmysiteにする。

```shell:django-admin
# /home/ec2-userなどの任意のユーザのhomeディレクトリへ移動
$ cd /~
$ django-admin startproject mysite
```
今回はadmin画面が見えればOKなので、最低限の設定だけをする。

```shell:django-admin
$ cd mysite/mysite
$ vi settings.py
```

setting.pyにおいて設定するのは

- 末尾にSTATIC_ROOTを追加(あわせてimport osも追記)
- ALLOWED_HOSTSに自分のパブリックドメインやパブリックIPアドレスを設定

ぐらい。一応全文を載せておく

```python:setting.py
import os
from pathlib import Path

# Build paths inside the project like this: BASE_DIR / 'subdir'.                                                                                        
BASE_DIR = Path(__file__).resolve().parent.parent

# SECURITY WARNING: keep the secret key used in production secret!                                                                                      
SECRET_KEY = 'hogepugefugahogepugefuga...'

# SECURITY WARNING: don't run with debug turned on in production!                                                                                       
DEBUG = True

# 自分のドメインやパブリックIPアドレスとか
ALLOWED_HOSTS = ['*.*.*.*',]


# Application definition                                                                                                                                

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ROOT_URLCONF = 'mysite.urls'

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        },
    },
]

WSGI_APPLICATION = 'mysite.wsgi.application'

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

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': BASE_DIR / 'db.sqlite3',
    }
}


# Password validation                                                                                                                                   
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators                                                                          

AUTH_PASSWORD_VALIDATORS = [
    {
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    },
    {
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    },
]


# Internationalization                                                                                                                                  
# https://docs.djangoproject.com/en/3.1/topics/i18n/                                                                                                    

LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, JavaScript, Images)                                                                                                                
# https://docs.djangoproject.com/en/3.1/howto/static-files/                                                                                             

STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR,'static')
```

終わったら、djangoプロジェクトの静的ファイルをまとめたり、
管理者ページに入るまでの設定をしておく。

```shell:django
$ cd ~/mysite
# 自身の情報をここで入れておく。パスは忘れないように
$ python3 manage.py makemigrations
$ python3 manage.py migrate
$ python3 manage.py createsuperuser
# STATICファイルをとりまとめておく
$ python3 manage.py collectstatic
```

もし、python3 manage.py collectstaticでcheck_sqlite_versionと書かれたエラーを吐いた場合はSQLite3の設定を見直すこと。過去のSQLite3を見に行っている可能性が高い。

Djangoそのものの設定がうまくできているか確認するために、gunicornから動かしてみる。

```shell:django
# 必ずmanage.pyなどが存在しているフォルダへ移動すること
$ cd ~/mysite
$ sudo gunicorn mysite.wsgi --bind=0.0.0.0:8000
```
ブラウザでパブリックIPあるいはドメインへアクセスした際に、例のロケットが表示されればOK。確認できたらgunicornをCtrl+Xなどで終了する。

# gunicornの設定

以下の設定ファイルを作成（編集）する。

- /etc/systemd/system/gunicorn.service
- /etc/systemd/system/gunicorn.socket

なお、gunicorn.serviceのExecStartのパスはwhichコマンドで確認する。

```shell:gunicorn.serviceのExecStartに設定するパスのチェック
which gunicorn
```

ユーザー名（グループ名）、WorkingDirectory、ExecStartのwsgiファイル名とListenStreamは各自の環境に揃えておくこと。(特に工夫がなければ、ユーザー名はnginxと一致させておく。)

```conf:gunicorn.service
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
Type=notify
# the specific user that our service will run as
# ここのユーザ名とグループ名はデーモンを実行するユーザを入れる。
# 特に何も考えていなければec2-userにする。
User=ec2-user
Group=ec2-user
# another option for an even more restricted service is
# DynamicUser=yes
# see http://0pointer.net/blog/dynamic-users-with-systemd.html
RuntimeDirectory=gunicorn
# Djangoプロジェクトのパスを入れる
WorkingDirectory=/home/ec2-user/mysite
# gunicornの実行ファイルのあるパスと、プロジェクト名.wsgi
ExecStart=/usr/local/bin/gunicorn mysite.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=mixed
TimeoutStopSec=5
PrivateTmp=true

[Install]
WantedBy=multi-user.target
```

```conf:gunicorn.socket
[Unit]
Description=gunicorn socket

[Socket]
# sockファイルを作る場所を入れる。基本はdjangoプロジェクトのパスを入れておく。
ListenStream=/home/ec2-user/mysite/mysite.sock
# Our service won't need permissions for the socket, since it
# inherits the file descriptor by socket activation
# only the nginx daemon will need access to the socket
# gunicorn.serviceと同じユーザを入れる
SocketUser=ec2-user
# Optionally restrict the socket permissions even more.
# SocketMode=600

[Install]
WantedBy=sockets.target
```

**ここで注意として、sockファイルとsocketファイルを混同させないようにすること。**あくまでも、sockファイルはnginxとgunicornが共有するファイルである。

終わったら、gunicornのデーモンの起動と自動起動の登録をする。

```shell:gunicorn起動と自動起動設定
$ sudo systemctl start gunicorn.service
$ sudo systemctl enable gunicorn
# ちゃんとプロセスが動いていることを確認する。
$ sudo systemctl status gunicorn
```

プロセスが正常に起動していれば、次に進む。

# nginxの設定
まず、nginxが正常に動いていて、ネットからつながることを確認する。つながらない場合は、EC2ダッシュボードのインスタンス→セキュリティからポート80が開放されているか、あるいはEC2のダッシュボード上で示されているパブリックIPアドレスやドメインが正しいか確認する。

```shell:nginx
systemctl start nginx
# コマンド実行後にEC2のパブリックIPアドレスにブラウザからアクセスして、
# nginxが動いているページが見えることを確認する。
# 確認できたら、以下のコマンドで一旦終了する。
systemctl stop nginx
```

確認できたら、nginxの設定をする。設定するファイルは次の通り。(mysite.confは新規作成する。また、拡張子が.confなら好きな名前でOK)

-  /etc/nginx/nginx.conf
-  /etc/nginx/conf.d/mysite.conf 

```conf:nginx.conf
# 全文は多すぎるので省略。userをnginxからec2-userなどのサービスを動かすユーザーへ変える。
# 思い入れがなければ、gunicorn起動時のユーザと一致させておく
...
user ec2-user;
...
# httpブロック内などに、この一行があるか確認する。書いてあればOK
include /etc/nginx/conf.d/*.conf;
...
```

```conf:mysite.conf
server {
    listen  80;
    # ドメインあるいはIPアドレスを設定する
    server_name     *.*.*.*;

    # djangoの静的ファイルの置き場所を指定する。
    location /static {
        alias /home/ec2-user/mysite/static;
    }

    # djangoのadmin用の静的ファイルの置き場所を指定する。
    location /static/admin {
        alias /home/ec2-user/mysite/static/admin;
    }

    # gunicorn起動時に作成されるsockファイルの場所を指定する。
    location / {
        proxy_pass http://unix:/home/ec2-user/mysite/mysite.sock;
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }

}

```

作成したら、デーモンをstart（あるいはrestart）させる。statusでプロセスが問題なく起動していれば次に進む。

```shell:nginx
sudo systemctl start nginx
# すでに起動していた場合はsudo systemctl restart nginx
# プロセスが適切に動いていることを確認する
sudo systemctl status nginx
```

# ブラウザから管理者画面を確認してみる
以下コマンドでnginxとgunicornのプロセスがバックグラウンドで動いていることを確認する。動いていなければエラーから各設定を修正してstartしておく。

```shell:プロセスのチェック
$ sudo systemctl status nginx
$ sudo systemctl status gunicorn
```
そのあと、自身のDjangoプロジェクトフォルダ(/home/ec2-user/mysite)内にmysite.sockが作成されていることを確認し、ブラウザで「自身のパブリックIPアドレスorドメイン/admin」で管理者画面を開いて見る。CSSを含めてAdminのログインページが表示されれば完了。

もしCSSが死んでいる場合はこのあたりを疑ってみる。

- [Djangoの設定でSTATIC_ROOT](https://qiita.com/drafts/059dea2f63b85c7e8449/edit#django%E3%81%AE%E8%A8%AD%E5%AE%9A)を忘れている。
- mysite.confのLocationやAliasが間違っている。

また、もし502エラー(Bad Gateway)が出てくるようであれば、このあたりを間違えているかも。

- nginx.confやgunicorn.serviceのユーザーが同じではなかった（意図した権限を持ったユーザじゃなかった）
- 同ファイルにおいてsockファイルへのパスが異なっている。

今日はここまで。