DjangoをWSGI + EC2 + NGINXでデプロイしてみた

# 概要

Djangoで作ったアプリをEC2上にデプロイしましたが、色々とハマったので備忘録として記事にまとめました。
とりあえずEC2を立てて独自ドメインで動くところまで書きます。

# 環境

- Macbook pro (M1 2020)
- Python 3.10.8
- Django 3.2
- EC2 (Ubuntu 22.04)
- Nginx 1.18

# 手順

1. Djangoアプリを作る
2. EC2を立てる
3. ブラウザからDjangoアプリにアクセスできることを確認する
4. Elastic IPでグローバルIPアドレスを固定する
5. Nginxを設定する
6. Route53で独自ドメインを取得
7. SSL証明取得
8. wsgiをデーモン化

# 1. Djangoアプリをつくる

ここでは簡易なDjangoプロジェクトのみ作成します。
Djangoの詳細な開発方法については書籍や他記事を参照してください。

A. 仮想環境を構築する
B. Djangoのプロジェクト・アプリを生成する
C. .envに機密情報を集約する
D. settingsファイルを分割する
E. インストールしたライブラリを`requirements.txt`に出力する
F. Githubにpushする

### A. 仮想環境を構築する

Pythonのデフォルトで使えるvenvで環境を作ります。

```
$ python -m venv django_env
```

仮想環境に入ります。

```
$ source django_env/bin/activate
```


必要なライブラリをインストールします。
ここでは2つだけですが、他に必要なライブラリがあれば適宜インストールしてください。

```
$ pip install django==3.2
$ pip install uwsgi==2.0.21
```

### B. Djangoのプロジェクト・アプリを生成する

Djangoプロジェクトを作り、その下にアプリを作ります。

```
$ django-admin startproject sample_project .
$ django-admin startapp sample_app
```

末尾の `.` を忘れた場合はプロジェクトのディレクトリに移動すればOKです。

```
$ django-admin startproject sample_project
$ cd sample_project
$ django-admin startapp sample_app
```


### C. .envに機密情報を集約する

ライブラリをインストールします。
```
$ pip install django-environ
```

`.env`ファイルを `manage.py`と同じで階層に作成してください。
ここにシークレットキーなどを格納します。
DBのパスワードなどもあればここに書いておきます。
環境によって変える変数もここに書いてもOKです (環境変数でもOK) 。
`manage.py`と同じ階層に作成してください。

```.env
SECRET_KEY=django-insecure-xxxxxxxxxxxxxxxxxxxxxxx
DEBUG=True
ALLOWED_HOSTS=*
```

`settings.py`の該当部分を置き換えます。
```python:sample_project/settings.py
import os
import environ
from pathlib import Path

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

# env系の設定
env = environ.Env()
env.read_env(os.path.join(BASE_DIR, '.env'))
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG') == 'True'
ALLOWED_HOSTS = [env('ALLOWED_HOSTS')]

...
```

Githubにアップロードされないように `.gitignore`に載せておきます。

```.gitignore
.env
```

### D. settingsファイルを分割する

`settings`ディレクトリ作成し、以下のような構成にします。
`settings.py` は `common.py` に名前を変更します。

```
├── settings
│   ├── common.py
│   ├── development.py
│   └── production.py
```

`common.py` に共通の設定を書いておき、 ログ設定など環境で変わる設定を `development.py` や `production.py` などに書きます。
環境ごとに読み込むファイルを変えればそれぞれの設定が反映されます。

```python:development.py
from .common import *
...
```

```python:production.py
from .common import *
...
```

`settings` ディレクトリで階層を深くしたので、 `BASE_DIR` の階層を修正します。
`parent` を1つ増やします。

```python:common.py
BASE_DIR = Path(__file__).resolve().parent.parent.parent
```

`settings.py` を呼び出していたところを修正します。

```python:manage.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings.development')
```

```python:sample_project/wsgi.py
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'sample_project.settings.production')
```

### E. インストールしたライブラリを`requirements.txt`に出力する

EC2上で同じライブラリをインストールできるように出力しておきます。

```
$ pip freeze > requirements.txt
```

### F. Githubにpushする

ここまでの変更内容をGithubにpushしておきます。
`manage.py`と同じ階層で実行します。
push先のリポジトリは事前にGithub上で作成しておきます。

```
$ git init
$ git remote add origin git@github.com:xxxxxxxxxxxxxxxxx.git
$ git add .
$ git commit -m 'create django project'
$ git push origin master
```


# 2. EC2を立てる

A. ubuntu22.04でEC2を立てる
B. EC2にsshで接続する
C. EC2でpyenvでPythonのバージョンを開発環境と合わせる
D. EC2上でGithubからpullする
E. EC2上に仮想環境を構築する

### A. ubuntu22.04でEC2を立てる

AWSコンソールでEC2ページにアクセスします。
右上の「インスタンスを起動」からインスタンスを作成します。
![ec2画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/11937e04-b671-9027-0ecb-b528b35b034f.png)

サーバーをに名前をつけます。
何でもよければ雑に `アプリ名-server` あたりにします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/b86cc960-b14b-179b-cec2-ab73c74777c4.png)

OSを選択します。
今回はUbuntu22.04でを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/fa95f26b-5c28-9965-bc00-9a925fa7548d.png)

インスタンスタイプを選択します。
`t2.micro`は1CPU・メモリ1GBなので、デプロイするアプリの規模が大きい場合は高性能なインスタンスを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/88b3e0b3-8384-4d61-deb5-e2087179663e.png)

ssh接続などでEC2にアクセスするときに必要です。必ず生成しましょう。
また、再発行ができないので大切に保管しましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/2b8af354-c57f-e3ea-6314-2359fb64ce6d.png)

EC2へのアクセス制御を行います。
セキュリティグループを作成する場合、sshトラフィックの許可はプルダウンから「自分のIP」を選択しておきます。
![スクリーンショット 2023-03-22 1.59.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/25bd23e6-93ed-8ad1-b1f5-058a3389455f.png)


右下の「インスタンスを起動」ボタンを押下するとEC2インスタンスが立ち上がります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/6dcd14c4-bb8e-4dab-cf29-bdb615a4aee4.png)


### B. EC2にsshで接続する

EC2のコンソールから先程作成したインスタンスを選択してください。
インスタンス概要ページの「接続」を選択すると、SSHクライアントの接続情報が表示されます。
`.pem` は先程生成したキーペアです。保存した先のPathも含めてください。

```
$ ssh -i "/to/your/pem_path/xxxxx.pem" ubuntu@ec2-IPアドレス.ap-northeast-1.compute.amazonaws.com
```

### C. EC2でpyenvでPythonのバージョンを開発環境と合わせる

local環境のPythonバージョンを確認します。

```
$ python -V
Python 3.10.8
```

EC2にssh接続し、必要なライブラリをインストールします。

```
$ sudo apt update; sudo apt install build-essential libssl-dev zlib1g-dev \
libbz2-dev libreadline-dev libsqlite3-dev curl \
libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
```

pyenvをpullします。

```
$ git clone https://github.com/pyenv/pyenv.git ~/.pyenv
```

pathを通します。

```
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'command -v pyenv >/dev/null || export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(pyenv init -)"' >> ~/.bashrc
```

変更内容を反映します。

```
$ source .bashrc
```

pyenvがインストールできていることを確認します。

```
$ pyenv --version
```

localのPythonと同じバージョンがインストール可能かチェックします。

```
$ pyenv install -list
```

localのPythonと同じバージョンをインストールします。

```
pyenv install 3.10.8
```

指定したバージョンがインストールされていることを確認します。

```
$ pyenv versions
```

Pythonのバージョンを変更します。

```
$ pyenv global 3.10.8
```

指定したバージョンに変更されていることを確認します。

```
$ python -V
Python 3.10.8
```

### D. EC2上でGithubからpullする

sshのディレクトリに移動します。

```
$ cd ~/.ssh
```

鍵を作ります。

```
$ ssh-keygen -t rsa
```

`id_rsa.pub`の中身をコピーし、↓で `New SSH key` として登録します。

https://github.com/settings/keys

接続確認をします。 `You've successfully authenticated` と出力されればOKです。

```
$ ssh -T git@github.com
```

Djangoアプリをpullします。

```
$ cd ~
$ git clone git@github.com:xxxxxxxxxxxxx.git
```


### E. EC2上に仮想環境を構築する

localで作ったときと同様に構築します。

```
$ python -m venv django_env
$ source django_env/bin/activate
```

ライブラリをインストールします。

```
$ pip install -r requirements.txt
```

# 3. ブラウザからDjangoアプリにアクセスできることを確認する

A. とりあえずmanage.pyで動かす
B. wsgiで動かす

### A. とりあえずmanage.pyで動かす

EC2インスタンスに割り当てているセキュリティグループで8000番ポートを解放します。
インバウンドルールで以下のように設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/52b19616-6880-d657-3d6b-7e9ec164a34a.png)

`manage.py`のあるディレクトリ移動して次を実行します。

```
$ python manage.py runserver 0.0.0.0:8000
```

ブラウザからEC2インスタンスのグローバルIPアドレスで接続できるか確認します。

```
http://グローバルIP:8000
```


### B. wsgiで動かす

`manage.py`を停止し、同じ階層で実行します。

```
$ uwsgi --http :8000 --module sample_project.wsgi
```

同じくブラウザからグローバルIPアドレスで接続できるか確認します。

```
http://グローバルIP:8000
```

# 4. Elastic IPでグローバルIPアドレスを固定する

A. Elastic IPを割り当てる (= 取得する) 
B. Elastic IPをEC2に関連付ける
C. EC2の `.env` にElastic IPで固定したグローバルIPアドレスを追加

### A. Elastic IPを割り当てる

EC2のコンソールで左のメニューから `Elastic IP` を選択します。
![スクリーンショット 2023-03-21 23.18.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/cc2e88f9-97ca-50e5-1590-561be577b0b1.png)


右上オレンジの `Elastic IP アドレスを割り当てる` を押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/95d44c7b-d600-c137-e952-a5a9bd76f46c.png)



設定は特に何も触れず右下の `割り当て` を押下するとElastic IPが割り当てられます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/1f62f01e-7109-b3c1-6ec2-f005e8c6182f.png)

### B. Elastic IPをEC2に関連付ける

先程割り当てたElastic IPを選択します。
![スクリーンショット 2023-03-21 23.20.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/e2dffc36-b450-5ccd-0385-c85def352598.png)

右上オレンジの `Elastic IP アドレスの関連付け` を押下します。
![スクリーンショット 2023-03-21 23.23.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/862c0a8d-59bf-0fac-0282-7c5a2d6fcf46.png)


`インスタンス`の検索ウインドウから先程つくったEC2を選択します。
`プライベート IP アドレス` の検索ウインドウで選択したインスタンスのIPアドレスが選択できるようになるので、それを選択します。
右下の `関連付ける` を押下するとEC2インスタンスにElastic IPがグローバルIPとして関連付けられます。
![ElasticIP関連付け.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/ce69cd1e-450a-7619-00f5-e8b3f9847424.png)

関連付けが完了するとEC2のホスト名が変わるので、ssh接続する際は改めてEC2コンソールから接続情報を確認してください。

### C. EC2の `.env` にElastic IPで固定したグローバルIPアドレスを追加

```.env
ALLOWED_HOSTS=固定したグローバルIPアドレス
```


# 5. Nginxを設定する

A. nginx.confを編集する
B. uwsgiファイルをコピーする
C. 静的ファイルをNginxに配置する
D. nginxを動かす

### A. nginx.confを編集する

設定を書くファイルを作成します。

```
$ sudo touch /etc/nginx/sites-available/sample_project
```

中身は以下のようにします。

```sh:sample_project
upstream django {
    server unix:///home/ubuntu/sample_project/sample_project.sock;
}

server {
    listen 80;
    server_name グローバルIPアドレス;
    root /usr/share/nginx/html;

    location /static {
        alias /usr/share/nginx/html/static;
    }

    location /media {
        alias /usr/share/nginx/html/media;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarder-Proto $scheme;
        uwsgi_pass django;
        include /home/ubuntu/sample_project/uwsgi_params;
    }
}
```

シンボリックリンクを張って `nginx.conf` に読み込ませます。

```
$ sudo ln -s /etc/nginx/sites-available/sample_project /etc/nginx/sites-enabled/sample_project
```

### B. uwsgiファイルをコピーする

`uwsgi_params` ファイルをNginxからプロジェクトの`manage.py`と同じ階層のディレクトリにコピーします。

```
$ sudo cp /etc/nginx/uwsgi_params /home/ubuntu/sample_project/
```

### C. 静的ファイルをNginxに配置する

Djangoプロジェクトの静的ファイル (cssとjavascriptなど) をNginxに配置します。

配置するディレクトリを作成します。
```
$ sudo mkdir /usr/share/nginx/html/static
$ sudo mkdir /usr/share/nginx/html/media
```

ディレクトリの所有者を書き換えます。

```
$ sudo chown ubuntu:ubuntu /usr/share/nginx/html/static
$ sudo chown ubuntu:ubuntu /usr/share/nginx/html/media
```

settingsの設定を書き換えます。

```python:development.py
from .common import *

STATIC_ROOT = BASE_DIR / "static"
...
```

```python:production.py
from .common import *

STATIC_ROOT = '/usr/share/nginx/html/static'
MEDIA_ROOT = '/usr/share/nginx/html/media'
...
```

`manage.py`の階層に行き、以下コマンドを実行します。

```
$ python manage.py collectstatic
```

静的ファイルが配置されたか確認します。

```
$ ls -lrt /usr/share/nginx/html/static
```

### D. Nginxを動かす

Nginxを有効化・起動します。
```
$ sudo systemctl enable nginx
$ sudo systemctl start nginx
```

EC2インスタンスのセキュリティグループからインバウンドでhttpリクエストを有効にします。
8000番ポートを有効にしたインバウンドルールは削除しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/cb9532d3-ab77-4239-7bae-f24148e0a08e.png)


`manage.py`のあるディレクトリで以下を実行し、ブラウザからグローバルIPでアクセスできることを確認します。
```
$ uwsgi --socket /home/ubuntu/sample_project/sample_project.sock --module sample_project.wsgi --chmod-socket=666
```


# 6. Route53で独自ドメインを取得

A. ドメイン名を決めて購入
B. 固定したグローバルIPを紐付ける
C. ドメイン名をNginxとDjangoに反映する

### A. ドメイン名を決めて購入

AWSコンソールでRoute53にアクセスします。
`ドメインの登録`に取得したいドメイン名を入力し、チェックを押下します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/c8c6db8f-ecc6-7f37-8795-7a02b6b792af.png)

次ページで購入するドメイン名を確定し、以降のページで購入手続きを完了してください。
個人利用の場合、whoisで調べても個人情報の連絡先等はすべてAWSの連絡先になっていました。
筆者は購入完了後20分程度でドメインが有効化されました。

### B. 固定したグローバルIPを紐付ける

Route53のダッシュボードからホストゾーンを選択します。

![スクリーンショット 2023-03-22 0.11.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/109c6ef1-9a25-8d5f-3b0e-f45b143c757e.png)

有効になったドメイン名を選択し、`レコードを作成`を押下します。
![スクリーンショット 2023-03-22 0.13.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/ada93a29-a019-cd3e-1b60-d025dfabde3f.png)

以下を入力し、`レコードを作成`を押下します。
- レコード名: サブドメインを使用する場合は入力
- 値: EC2インスタンスに割り当てた固定グローバルIPアドレス
![スクリーンショット 2023-03-22 1.55.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/c7303c17-13d1-c1fa-6764-eb94356d98d3.png)


これでドメイン名が固定したグローバルIPに紐付けられました。

### C. ドメイン名をNginxとDjangoに反映する

Nginxにドメイン名を追加

```sh:/etc/nginx/sites-available/sample_project
server_name グローバルIPアドレス 取得したドメイン名;
```

```python:common.py
...
ALLOWED_HOSTS = ['取得したドメイン名', env('ALLOWED_HOSTS')]
...
```

これでドメイン名でアクセスできるようになります。

```
http://取得したドメイン名
```

# 7. SSL証明取得

A. Let’s Encryptをインストール
B. Nginxで443へのアクセスを受け付ける

### A. Let’s Encryptをインストール

nginxを停止し、Let’s Encryptをインストールします。

```
$ sudo systemctl stop nginx
$ sudo apt-get install letsencrypt
```

SSL化に必要な証明書を取得します。

```
$ sudo certbot certonly --standalone -d 取得したドメイン名
```

証明書の実行権限を書き換えます。
```
$ sudo chmod 700 /etc/letsencrypt/live/
```

### B. Nginxで443へのアクセスを受け付ける

```sh:sample_project
upstream django {
    server unix:///home/ubuntu/sample_project/sample_project.sock;
}

server {
    listen 80;
    server_name ドメイン名;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name ドメイン名;
    root /usr/share/nginx/html;

    ssl_certificate /etc/letsencrypt/live/ドメイン名/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ドメイン名/privkey.pem;

    location /static {
        alias /usr/share/nginx/html/static;
    }

    location /media {
        alias /usr/share/nginx/html/media;
    }

    location / {
        proxy_set_header Host $http_host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarder-Proto $scheme;
        uwsgi_pass django;
        include /home/ubuntu/sample_project/uwsgi_params;
    }
}
```

EC2インスタンスのセキュリティグループでインバウンドルールにhttpsリクエストを追加します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341413/731ae2bb-4d4c-627f-8e10-c45e0c06cede.png)

Nginxを再起動します。

```
$ sudo systemctl start nginx
```

これでhttpsでアクセスできるようになります。

```
https://ドメイン名
```

# 8. wsgiをデーモン化

A. wsgi.iniファイルを作成
B. バックグラウンドで動かす

### A. wsgi.iniファイルを作成

いままでDjangoを実行していた↓のコマンドをファイルに転記します。
```
$ uwsgi --socket /home/ubuntu/sample_project/sample_project.sock --module sample_project.wsgi --chmod-socket=666
```

```uwsgi.ini
[uwsgi]
socket = /home/ubuntu/sample_project/sample_project.sock
module = sample_project.wsgi
chmod-socket = 666
```

動作確認をします。

```
$ uwsgi --ini uwsgi.ini
```

### B. バックグラウンドで動かす

バックグラウンドで動けばssh接続を切ってもアプリが動き続けます。

```
$ uwsgi --ini uwsgi.ini &
```