EC2でwebアプリを作成したメモ

## Guide

Zennでも同じ記事を投稿しています。以下のリンクから参照できます。
- https://zenn.dev/mount_tyo/articles/01fd1ddb77e250

## 概要

AWS EC2+Nginx+gunicorn+Django+PostgreSQLでシンプルなwebアプリを作成したログ．
以下の内容は，すでにAWS EC2へSSH接続していることを前提としている．

## 環境構築

今回の記事で使用するパッケージなどのインストール方法をまとめて記載する．

```bash
$ sudo yum install git
$ sudo yum install openssl-devel
$ sudo yum install python-psycopg2

$ sudo amazon-linux-extras install nginx1
$ sudo amazon-linux-extras install postgresql14
```

Pythonのversionを管理するために，`pyenv`をインストールする．

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

$ echo '# pyenv' >> ~/.bashrc
$ echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bashrc
$ echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bashrc
$ echo 'eval "$(pyenv init --path)"' >> ~/.bashrc

$ source .bashrc

$ pyenv -v
pyenv 2.3.6-15-g13d85686
```

プロジェクトディレクトリを作成する．
Pythonバージョンを`3.9.13`とし，`venv`で仮想環境を作成する．

```bash
$ mkdir ~/django_app
$ cd ~/django_app/

$ pyenv install 3.9.13

$ python -V
Python 3.9.13

# 仮想環境`venv`を作成する
$ python -m venv venv

# 仮想環境に入る
source activate venv/bin/activate

# pipを最新バージョンに更新する
pip install --upgrade pip
```

pipで必要なpythonモジュールをインストールする．

```bash
# Djano関連
$ pip install django
$ pip install django-environ
$ pip install dj-database-url
$ pip install djangorestframework
$ pip install mojimoji
# PostgreSQL関連
$ pip install -U setuptools
$ pip install psycopg2
# Gunicorn
$ pip install gunicorn
```

## Webサーバ構築

WebサーバとしてNginxを利用する．

```bash
# バージョン確認
$ sudo nginx -v
nginx version: nginx/1.22.0
```

### Nginxの設定

Nginxの初期設定ファイルのバックアップを取る

```bash
$ sudo cp -a /etc/nginx/nginx.conf /etc/nginx/nginx.conf.backup
```

Nginxの通信データを`http://127.0.0.1:8000`へ送るように設定する．
これにより，サーバの`80`番ポートに来たアクセスをDjango側へ転送できる．

```bash
$ sudo vim /etc/nginx/nginx.conf
```

以下のように記述．

```conf
~
http{
  ~
  server{
    ~
    location / {
      # `http://xx.xx.xx.xx/`へ来たアクセスをDjango側へ転送する.
      proxy_set_header Host $http_host;                                   # host name
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        # source address
      proxy_set_header X_Forwarded-Proto $scheme;                         # URL scheme
      # localhostの8000番ポートへ転送する.
      proxy_pass http://127.0.0.1:8000;
    }
    # ここまで
    ~
  }
  ~
}
~
```

Nginxを起動する.

```bash
$ sudo systemctl start nginx
```

インスタンス起動時にNginxも自動で起動させる.

```bash
$ sudo systemctl enable nginx
Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.
```

サービスステータス表示する.

```bash
$ systemctl status nginx
● nginx.service - The nginx HTTP and reverse proxy server
   Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; vendor preset: disabled)
   Active: active (running) since {DayOfWeek} {Year}-{Month}-{Day} {Hour}:{Minute}:{Seconds} UTC; 28s ago
 Main PID: 26497 (nginx)
   CGroup: /system.slice/nginx.service
           ├─26497 nginx: master process /usr/sbin/nginx
           ├─26498 nginx: worker process
           └─26499 nginx: worker process
```

## PostgreSQLの設定

Djangoアプリケーションで利用するためのDBを構築する．

### 初期化

まず，データベースサーバを初期化する.

```bash
$ sudo /sbin/service postgresql initdb
Hint: the preferred way to do this is now "postgresql-setup initdb"
Initializing database ... OK
```

### サービス起動設定

今回の記事では，サーバ内でDBサーバを構築する方法を取った．
データベースサーバを起動する．

```bash
$ sudo /sbin/service postgresql start

$ sudo systemctl status postgresql
● postgresql.service - PostgreSQL database server
   Loaded: loaded (/usr/lib/systemd/system/postgresql.service; disabled; vendor preset: disabled)
   Active: active (running) since ** **-**-** **:**:** UTC; **min **s ago
  Process: 9585 ExecStart=/usr/bin/pg_ctl start -D ${PGDATA} -s -o -p ${PGPORT} -w -t 300 (code=exited, status=0/SUCCESS)
  Process: 9580 ExecStartPre=/usr/bin/postgresql-check-db-dir ${PGDATA} (code=exited, status=0/SUCCESS)
 Main PID: 9589 (postgres)
   CGroup: /system.slice/postgresql.service
           ├─9589 /usr/bin/postgres -D /var/lib/pgsql/data -p 5432
           ├─9590 postgres: logger process   
           ├─9592 postgres: checkpointer process   
           ├─9593 postgres: writer process   
           ├─9594 postgres: wal writer process   
           ├─9595 postgres: autovacuum launcher process   
           └─9596 postgres: stats collector process   
```

自動起動設定をする．

```bash
$ sudo systemctl enable postgresql
Created symlink from /etc/systemd/system/multi-user.target.wants/postgresql.service to /usr/lib/systemd/system/postgresql.service.
```

### DB作成

Djangoアプリが利用するDBを作成する．

```bash
$ sudo -u postgres psql
psql (9.2.24)
Type "help" for help.

postgres=# 
```

以下のように入力し，DBを作成した．

```sql
-- 新規にDBを作成する．
postgres=# CREATE DATABASE djangodb;
CREATE DATABASE
-- ユーザ名とパスワードを設定する．
postgres=# CREATE USER django WITH PASSWORD '****';
CREATE ROLE
-- 文字コードを`UTF-8`にする．
postgres=# ALTER ROLE django SET client_encoding TO 'utf8';
ALTER ROLE
-- トランザクション分離レベルを設定する．
postgres=# ALTER ROLE django SET default_transaction_isolation TO 'read committed';
ALTER ROLE
-- タイムゾーンを東京/日本とする．
postgres=# ALTER ROLE django SET timezone TO 'UTC+9';
ALTER ROLE
-- `django`ユーザにDBの全ての権限を付与する．
postgres=# GRANT ALL PRIVILEGES ON DATABASE djangodb TO django;
GRANT
-- psqlを修了する．
postgres-# \q
```

postgresqlの設定ファイルのバックアップを取り，認証方法を変更する．

```bash
$ sudo cp /var/lib/pgsql/data/pg_hba.conf /var/lib/pgsql/data/pg_hba.conf.backup

$ sudo vim /var/lib/pgsql/data/pg_hba.conf
```

変更箇所は以下の通り．認証方法を`ident`から`md5`へ変更する．

```conf
# TYPE  DATABASE        USER            ADDRESS                 METHOD

# "local" is for Unix domain socket connections only
local   all             all                                     peer
# IPv4 local connections:
host    all             all             127.0.0.1/32            md5  # <-- here!
# IPv6 local connections:
host    all             all             ::1/128                 md5  # <-- here!
# Allow replication connections from localhost, by a user with the
# replication privilege.
local   replication     postgres                                peer
host    replication     postgres        127.0.0.1/32            ident
host    replication     postgres        ::1/128                 ident
```

設定ファイルの変更を適用するため，serviceを再起動する．

```bash
$ sudo systemctl restart postgresql
```

## 簡易的なWebアプリ作成

### Djangoプロジェクトの作成

Djanoでプロジェクトを作成する．

```bash
$ cd ~/django_app/

$ django-admin startproject website .
```

`~/django_app/website/settings.py`を修正する．

```python
# DBへのパス
DATABASE_URL=postgres://django:{password}@localhost:/djangodb
# アクセスを許可するホスト
ALLOWED_HOSTS={Your IP},localhost,127.0.0.1
# URLの末尾に`/`がない時，`/`を付けたURLへリダイレクトしない
APPEND_SLASH=False
# タイムゾーンを日本に設定
TIME_ZONE = 'Asia/Tokyo'
# 日本語
LANGUAGE_CODE = 'ja'
# 静的ファイルが集約されるパス
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
```

### gunicornを起動

- gunicornを起動する．
  - `127.0.0.1:8000`をバインドし，Nginxからのデータを受け取る．
  - Djangoプロジェクトのwsgi設定ファイル`~/django_app/website/wsgi.py`を指定する．
  - バックグラウンドで実行する．

```bash
# Djangoプロジェクトのrootに移動する
$ cd ~/django_app/

# 仮想環境に入る(既に入っていたら必要なし)
$ source venv/bin/activate

$ gunicorn --bind 127.0.0.1:8000 website.wsgi -D
```

### 簡単なテスト

現在，EC2のIPアドレスへHTTPリクエストを送信すると，ポート`80`でリクエストを受信したNginxがローカルホストのポート`8000`へデータを転送し，gunicornが転送されてきたデータを`django_app`のwsgiに紐づけて，Djangoアプリがリクエストを処理する，という仕組みになっている．

試しに，別のホストから`curl`でEC2サーバにHTTP GET リクエストを送信してみる．

```bash
$ curl http://{Your IP}/
```

レスポンスとして，Djangoのロケットのページを見ることができる．

## 簡単なAPIの実装

今回は，DBへのデータの登録と呼び出し，削除機能を実装する．

まず，API実装のための`rest_framework`をプロジェクトへ追加する．`~/django_app/website/settings.py`を編集する．

```python
INSTALLED_APPS = [
  ...
  'rest_framework', #new
]
```

DB操作を実現するためのアプリを作成する．
- アプリ名：`crud_app`

```bash
$ python manage.py startapp crud_app
```

新規にアプリを作成したため，`~/django_app/website/settings.py`へ追記する．

```python
INSTALLED_APPS = [
   ...
    'rest_framework',
    'crud_app', #new
]
```

今回は，以下に示す簡易的なテーブルを作成する．

```txt
テストテーブル
-----------------
名前      (char)
数量      (int)
作成日時   (date)
```


`~/django_app/crud_app/models.py`を以下のように記載し，DBモデルを作成する．

```python
from django.db import models

class DjangoModel(models.Model):
    name    = models.CharField("Name", max_length=240)  # 名前
    amount  = models.IntegerField("Amount", default=1)  # 数量
    created = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.name
```

プロジェクトのrootで，マイグレーションを適用し，dbカラムを作成する．

```bash
$ python manage.py makemigrations

$ python manage.py migrate
```

`~/django_app/crud_app/views.py`を編集し，リクエストに応じてDBを操作する．

```python
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from django.http.response import JsonResponse
import json

from .models import DjangoModel


@csrf_exempt
def control_data(request):
    """データの新規作成，確認，削除を行うインターフェース"""

    # 確認
    if request.method == 'GET':
        # `name`で昇順にソートし，全件取得
        models = DjangoModel.objects.order_by('name')
        # レスポンス作成
        all_models_dict = {}
        for model in models:
            all_models_dict[model.name] = model.amount
        return JsonResponse(all_models_dict)

    # 新規作成
    if request.method == 'POST':
        # HTTPリクエストbodyのデータをjson形式で取得する
        data = json.loads(request.body)
        # `amount`がinputになければ，デフォルト値を代入する
        if not 'amount' in data:
            data['amount'] = 1
        # 新規作成
        model = DjangoModel(name=data['name'], amount=data['amount'])
        model.save()
        # レスポンス作成
        response = HttpResponse('created!')
        return response

    # 全て削除
    elif request.method == 'DELETE':
        # レコード削除
        DjangoModel.objects.all().delete()
        # レスポンス作成
        response = HttpResponse('deleted!')
        return response

    # 未実装のHTTP methodへの処理
    else: 
        return HttpResponse('Not Implemented.')
```

特定のURLにアクセスしたとき，呼び出す処理を記載する．
`~/django_app/crud_app/urls.py`を作成する．

```python
from django.urls import include, path
from .views import control_data

urlpatterns = [
    path('', control_data, name='control-data'),
]
```

rootの`urls.py`から`django_app`アプリの`urls.py`へ接続するようにする．
`~/django_app/website/urls.py`を編集する．

```python
from django.urls import path, include

urlpatterns = [
    path('/', include('django_app.urls')),
]
```

以上により，簡易なAPIの実装を終えた．
Djangoアプリの機能がいくつか変更・追記されたので，gunicornを再度起動し，変更を適用する．

```bash
$ pkill gunicorn

$ gunicorn --bind 127.0.0.1:8000 website.wsgi -D
```

### APIのテスト

実装したAPIが正常に動作確認するために，`curl`でテストできる．

```bash
# コマンド例

## 新規登録
$ curl -d '{"name": "computer", "amount": 3}' -H 'Content-Type: application/json' http://{Your IP}/
created!

## 確認
$ curl http://{Your IP}/
{"computer":3}

## 削除
$ curl -X DELETE http://{Your IP}/
deleted!

## 確認
$ curl http://{Your IP}/
{}
```

## 改善点など

この記事の設定では，nginxとDjangoはHTTPで通信するが，UNIXソケットを使用するようにすると高速な通信が期待できる．おそらく，多くの同時アクセスを想定する場合は，UNIXソケットを使用した方が良いという予想．
他にも改善できる設定は存在すると思うので，勉強中．

## 参考

1. Request and response objects, Documentation, django (https://docs.djangoproject.com/en/4.1/ref/request-response/#httpresponse-objects)
1. リクエストとレスポンスのオブジェクト, Django 4.0.6 ドキュメント  (https://man.plustar.jp/django/ref/request-response.html)