Django+MySQLのアプリケーションをAWSのElastic Beanstalkにデプロイする (UTokyo Project Sprint 用)


UTokyo Project Sprint 用の記事です。主に、[Django Sprint](https://qiita.com/tmgauss/private/a0dbc8b90fd5c95732af) で構築した方を想定して書かれています。

この記事では **EB CLI** は使用せずに、コンソールからデプロイします。
また、**この記事の内容は一例で、もちろんアプリ内で使用するフレームワークや、ライブラリによって異なりますので、適宜自分のプロジェクトに対応させてください。**

# 目次
0. [```requirements.txt``` の修正](#0-requirementstxt-の修正)
1. [Elastic Beanstalkのアプリケーションと環境を作成する](#1-elastic-beanstalkのアプリケーションと環境を作成する)
2. [データベースの設定を行う](#2-データベースの設定を行う)
3. [アプリケーションソースバンドルを準備する](#3-アプリケーションソースバンドルを準備する)
4. [アプリケーションソースバンドルを作成する](#4-アプリケーションソースバンドルを作成する)

# 0. ```requirements.txt``` の修正

```requirements.txt``` の ```Django``` と ```mysqlclient``` のバージョンを以下のように書き換えてください。

```python:requirements.txt
...
Django>=2.1,<2.2
mysqlclient==1.3.12
...
```



# 1. Elastic Beanstalkのアプリケーションと環境を作成する

[AWS マネジメントコンソール](https://aws.amazon.com/jp/console/) にログインしてください。
ただし、AWS Educate のアカウントからの場合はこの記事を参考にしてください。

- [AWS Educate のアカウントで AWS マネジメントコンソールにログインする](https://qiita.com/Chrg/private/3b8eab94364a7a4e5f5d)

**Elastic Beanstalk** を選び、**Create Application** からアプリケーションを作成します。

- **アプリケーション情報** > **アプリケーション名** : 自身のアプリ名
- **アプリケーションタグ** : 任意。
- **プラットフォーム** : 今回は、**プラットフォーム** は ```Python```、**プラットフォームのブランチ** は ```Python 3.6 running on 64bit Amazon Linux```、**プラットフォームのバージョン** は ```2.9.10``` を選択しました。
- **アプリケーションコード** : とりあえず「サンプルアプリケーション」を選んでおきます。あとから自分のソースをアップロードします。

**[アプリケーションの作成]** を押します。あとは自動で環境まで作成してくれるので数分待っておきます。
無事終了したら、その環境のダッシュボードのような画面になると思います。左のメニューの、「環境に移動する」をクリックするとサンプルアプリケーションの画面が出てきます。
出てきたURLの ```http://``` などの部分を除く ```XXX.YYY.ZZZ.elasticbeanstalk.com``` が自身のアプリのドメインになります。
**あとでこれを使うので控えておいてください。**

# 2. データベースの設定を行う
左のメニューから **設定** を選びます。一番下の **データベース** の右の **編集** ボタンを押します。
**データベース設定** の各欄について以下の選択をします。**Amazon RDS DB** に作成されます。

- **エンジン** : ```mysql```
- **エンジンバージョン** : ```5.7.22``` を選択
- **インスタンスクラス** : ```db.t2.micro``` を選択
- **ストレージ** : ```5``` 
- **ユーザー名** : 各自作成して(一応)覚えておいてください。
- **パスワード** : 各自作成して(一応)覚えておいてください。
- **保持期間** : ```スナップショットの作成``` 
- **アベイラビリティー** : ```低(1つのAZ)``` 

**[適用]** を押してください。変更が反映されるまで待っておいてください。

# 3. アプリケーションソースバンドルを準備する
ここから主にローカルでの作業に移ります。以下、```cms``` というアプリを作成している前提で進めているで、**適宜自分のアプリ名に置き換えてください。**
最初に、**このセクションをすべて終えたときに出来上がる**ディレクトリ構造の一例を以下に示します。適宜参考にしてください。

```
├─ .ebextensions         # Added
│  └─ python.config      # Added
├─ requirements.txt
├─ manage.py             # Updated
├─ cms
│  ├─ management         # Added
│  |  ├─ commands        # Added
|  |  |  ├─ createsu.py  # Added
|  |  |  └─ __init__.py  # Added
|  |  └─ __init__.py     # Added
│  ├─ templates
|  ︙
│  └─ views.py
├─ config
│  ├─ __init__.py
│  ├─ settings           # Added (settings.py removed)
|  |  ├─ common.py       # Added
|  |  ├─ local.py        # Added
|  |  └─ production.py   # Added
│  ├─ urls.py
│  └─ wsgi.py            # Updated
├─ docker-compose.yml
└─ Dockerfile
```

## 3-1. ```.ebextensions``` ディレクトリを作成する
Elastic Beanstalk にデプロイする際に必要となるディレクトリです。
プロジェクトのディレクトリ直下に ```.ebextensions``` ディレクトリを作成し、その中に ```python.config``` というファイルを作成してください。そのファイルに、以下を書き込んでください。一例なので、適宜書き換えてください。

**なお、このファイルのインデントはtabだとエラーが出るので必ずspaceを使用してください。（コピペするとtabになると思うので、spaceに直してください。）**

```yaml:.ebextensions/python.config
container_commands:
  01_migrate:
    command: 'python manage.py migrate'
    leader_only: true
  02_collectstatic:
    command: 'python manage.py collectstatic --noinput'
  03_createsu:
    command: 'python manage.py createsu'
    leader_only: true

option_settings:
  'aws:elasticbeanstalk:application:environment':
    DJANGO_SETTINGS_MODULE: 'config.settings.production'
    PYTHONPATH: '$PYTHONPATH'
  'aws:elasticbeanstalk:container:python':
    StaticFiles: '/static/=www/static/'
    WSGIPath: 'config/wsgi.py'
```

以下、上記の内容の解説です。

### 3-1-1. ```container_commands```

```01_migrate``` ではマイグレーション
```02_collectstatic``` 静的ファイルの処理
```03_createsu``` スーパーユーザーの作成
のコマンドを実行しています。なお、```03_createsu``` の ```python manage.py createsu``` については[後述](#3-3-スーパーユーザー作成用のコマンドを準備する)します。

### 3-1-2. ```option_settings```

```DJANGO_SETTINGS_MODULE``` の部分で、後ほど作成する、 ```production.py``` を読み込むように環境変数を設定しています。
```WSGIPath``` や ```StaticFiles``` などについては、コンソールの設定のところでも変更可能です。
```option_settings``` について詳しく知りたい方は、以下を参照してください。

- [Elastic Beanstalk Python プラットフォームを使用する](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create-deploy-python-container.html)
- [環境プロパティとその他のソフトウェアの設定](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/environments-cfg-softwaresettings.html)

## 3-2. ```settings.py``` を分割する

次に、```/config/settings.py``` を、開発用（Local）と本番用（Production）に分割します。
```/config/settings.py``` は、Local 環境と Production 環境で別々のものを読み込む必要があるのですが、毎回書き換えていると大変なので、自動で切り替わるようにします。
具体的には、 
1. ```common.py``` に Local 環境と Production 環境で共通の設定項目を書き込む
2. ```local.py``` は、 ```common.py``` の内容に加えて、Local 環境固有の設定項目を書き込む
3. ```production.py``` は、 ```common.py``` の内容に加えて、Production 環境固有の設定項目を書き込む
4. ```manage.py``` と ```wsgi.py``` を修正する

どちらの環境にも共通している設定項目は、 ```common.py``` に書き込みます。

#### 個別に設定すべき内容
Local 環境と Production 環境でそれぞれ固有の設定は例えば以下のようなものです。

- ```DEBUG```
- ```DATABASE```
- ```ALLOWED_HOSTS```
- ```MEDIA_ROOT```
- ```STATIC_ROOT```
- ```LOGGING```

ただし、```common.py``` に書いている設定項目に、```local.py``` や ```production.py``` で追加したい場合は、例えば以下のようにします。（配列に追加）

```python
INSTALLED_APPS += (
    'django.contrib.admin',
)
```


### 3-2-1. ```common.py```, ```local.py```, ```production.py``` を作成する

まず、 ```/config``` 直下に、 ```settings``` というディレクトリを作成し、その中に ```/config``` 下の ```settings.py``` を移動させ、 ```common.py``` にリネームしてください。
また、 ```/config/settings``` ディレクトリの中に ```local.py```, ```production.py``` の2つのファイルを新規に作成してください。
以下のようなディレクトリ構造になっていることを確認してください。

```
...
├─ config
│  ├─ settings           # Added
|  |  ├─ common.py       # Added (settings.py をリネームしたもの)
|  |  ├─ local.py        # Added
|  |  └─ production.py   # Added
...
```

### 3-2-2. ```BASE_DIR``` を修正する

```common.py``` の最初のほうにある ```BASE_DIR``` を以下のように変更してください。

```python:/config/settings/common.py
...
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
...
```
↓

```python:/config/settings/common.py
...
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
...
```

### 3-2-3. ```local.py``` にLocal 環境固有の設定を書き込む

```local.py``` に、 ```common.py``` の内容に加えて、Local 環境で固有の設定項目を書き込みます。
```common.py``` の内容を以下のようにimportします。

```python:/config/settings/local.py
from .common import *
```
あとは、[個別に設定すべき内容](#個別に設定すべき内容) を書けばよいだけです。
これを踏まえると、 ```local.py``` は例えば以下のようになります。
これはローカルでPostgreSQLを使っていた場合なのですが、各自ローカルで使っていたDBの設定をそのまま移していただいて結構です。

```python:/config/settings/local.py
from .common import *

DEBUG = True

ALLOWED_HOSTS = [
    '0.0.0.0',
    '127.0.0.1',
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': 'postgres',
        'USER': 'postgres',
        'HOST': 'db',
        'PORT': 5432,
        'PASSWORD': 'somepassword',
    }
}
```

### 3-2-3. ```production.py``` にProduction 環境固有の設定を書き込む

```production.py``` に、 ```common.py``` の内容に加えて、Production 環境で固有の設定項目を書き込みます。
```local.py``` と同様に、 ```common.py``` の内容を以下のようにimportします。

```python:/config/settings/production.py
from .common import *
```
あとは、[個別に設定すべき内容](#個別に設定すべき内容) を書けばよいだけです。以下の項目は一例です。自身の環境に合わせて、適宜対応してください。

#### 3-2-3-1. ```DEBUG```

Production 環境では、```DEBUG``` を ```FALSE``` にします。

```python:/config/settings/production.py
...
DEBUG = False
```

#### 3-2-3-2. ```ALLOWED_HOSTS```

```ALLOWED_HOSTS``` に自身のアプリのドメインを追加します。ただし、 ```XXX.YYY.ZZZ.elasticbeanstalk.com``` の部分は[1で確認](#1-elastic-beanstalkのアプリケーションと環境を作成する)した自分のアプリのドメインに置き換えてください。

```python:/config/settings/production.py
...
ALLOWED_HOSTS = [
    'XXX.YYY.ZZZ.elasticbeanstalk.com',
]
```

#### 3-2-3-3. ```DATABASES```

```DATABASES``` を、本番環境の ```RDS``` のものに書き換えます。 ```'RDS_~'``` という変数を使えば、Elastic Beanstalk のほうで勝手に書き換えてくれます。
```'ENGINE'``` が ```local.py``` とは変わっていることに注意してください。

```python:/config/settings/production.py
...
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': os.environ['RDS_DB_NAME'],
        'USER': os.environ['RDS_USERNAME'],
        'PASSWORD': os.environ['RDS_PASSWORD'],
        'HOST': os.environ['RDS_HOSTNAME'],
        'PORT': os.environ['RDS_PORT'],
    }
}
```

#### 3-2-3-4. ```STATIC_ROOT```

```STATIC_ROOT``` を追加します。

```python:/config/settings/production.py
...
STATIC_ROOT = os.path.join(BASE_DIR, 'www', 'static')
```

以上を踏まえると、全体として ```production.py``` は例えば以下のようになります。ただし、 ```XXX.YYY.ZZZ.elasticbeanstalk.com``` の部分は[1で確認](#1-elastic-beanstalkのアプリケーションと環境を作成する)した自分のアプリのドメインに置き換えてください。

```python:/config/settings/production.py
from .common import *

DEBUG = False

ALLOWED_HOSTS = [
    'XXX.YYY.ZZZ.elasticbeanstalk.com',
]

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': os.environ['RDS_DB_NAME'],
        'USER': os.environ['RDS_USERNAME'],
        'PASSWORD': os.environ['RDS_PASSWORD'],
        'HOST': os.environ['RDS_HOSTNAME'],
        'PORT': os.environ['RDS_PORT'],
    }
}

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

### 3-2-5. ```common.py``` を修正する

```common.py``` の、Local 環境とProduction 環境に [個別に設定すべき内容](#個別に設定すべき内容) の項目とその値(中身)をすべて削除してください。
例えば今回であれば、 ```DEBUG```、```ALLOWED_HOSTS```、```DATABASES``` などが該当します。

### 3-2-6. ```common.py``` に ```STATIC_URL``` を追加する

```common.py``` に以下を追記します。すでにある場合は飛ばして構いません。

```python:/config/settings/common.py
...
STATIC_URL = '/static/'
...
```

なお、Djangoの静的ファイルの扱いについては、[この記事](https://qiita.com/saira/items/a1c565c4a2eace268a07)がとても分かりやすいので、是非読んでみてください。

### 3-2-7. ```manage.py``` と ```wsgi.py``` を修正する

ルート直下の ```manage.py``` と、```/config``` 直下の ```wsgi.py``` の以下の部分を修正します。


```python
...
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "project.settings")
...
```
↓

```python:manage.py
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
```

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

## 3-3. スーパーユーザー作成用のコマンドを準備する

最後に、スーパーユーザー作成用のファイルを作成していきます。スーパーユーザーの作成は、Local 環境であれば、

```terminal
$ python manage.py createsuperuser
```
などとして、ユーザー名、メールアドレス、パスワードを入力したと思いますが、今回はサーバーに実行してもらうため、それらを手動で入力することができません。なので、事前に入力する値を設定しておいて、それを実行させます。それが前述の [3-1-2](#3-1-2-container_commands) で出てきたものです。

### 3-3-1. ```createsu.py``` を作成する

```cms```（アプリ名なので置き換えてください。）の下に ```management``` フォルダ、その下に ```commands``` フォルダを作成し、そこに ```createsu.py``` を作成してください。そのファイルに以下を書き込んでください。

**8行目の ```XXX```、9行目の ```XXX```、```YYY```、```ZZZ``` は各自で書き換えて、どこかに控えておいてください。また、8行目の ```XXX``` と 9行目の ```XXX``` は一致させてください。**
これがスーパーユーザーアカウントになります。複数作りたい場合は、 ```if``` 文の数を増やせばよいだけです。

```python:cms/management/commands/createsu.py
from django.core.management.base import BaseCommand, CommandError
from django.contrib.auth import get_user_model

User = get_user_model()

class Command(BaseCommand):
    def handle(self, *args, **options):
        if not User.objects.filter(username="XXX").exists():
            User.objects.create_superuser(username="XXX", email="YYY", password="ZZZ")
            self.stdout.write(self.style.SUCCESS('Successfully created new super user'))
```

### 3-3-2. ```__init__.py``` を作成する

次に、作成した ```management```, ```commands``` フォルダそれぞれの直下に ```__init__.py``` というファイルを作成してください。これらのファイルは作成するだけで、中身は何も書き込みません。

**これで「アプリケーションソースバンドルの準備」は完了です。このセクションの最初にお見せした、ディレクトリ構造と同じようになっているかどうかを確認してください！**

# 4. アプリケーションソースバンドルを作成する
あとは、作成したものを ```zip``` にして、デプロイするだけです。

## 4-1. ```zip``` ファイルを作成する

何も難しいことはないかと思いますが、 ```zip``` にする対象について。
基本的にプロジェクト直下のファイル、フォルダをまとめて ```zip``` にします。（**プロジェクトのディレクトリそのもの**を ```zip``` にするわけではない）
ただし、例えば ```git``` を使っているのなら、 ```.git``` フォルダや、 ```.gitignore``` などのファイルがあると思いますが、**それらは含めません。**アプリケーションに必要なものだけを対象にしてください。
必要であれば以下を参照してください

- [アプリケーションソースバンドルを作成する](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/applications-sourcebundle.html)

### Mac, Linux の場合

例えばプロジェクトのルートディレクトリで、以下のようなを実行すれば、プロジェクトの一つ上の階層に ```myapp.zip``` が生成されます。（ ```.``` から始まるものは、 ```.ebextensions``` しか含まれないので、ほかに含める必要のあるファイルやフォルダがあれば適宜対応してください。）

```terminal
$ zip ../myapp.zip -r * .ebextensions
```

### Windowsの場合

コマンドプロンプトやPowershellからもできますが、コマンドが長すぎるのと、含めないファイル(フォルダ)の設定がややこしいのでGUIでやります。以下のように、必要なファイルとフォルダを選択して、
**右クリック→送る→圧縮(zip形式)フォルダー**
とすれば作成できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/628276/6db83067-093e-e2f6-b811-1547f750a36f.png)


## 4-2. アップロードしてデプロイする

いよいよ、サーバーにアップロードしてデプロイします。
コンソールの実行バージョンの下の、「アップロードとデプロイ」をクリックして、先ほど作成した ```zip``` ファイルを選択します。
バージョンラベルは自分でバージョンを区別できるわかりやすいものを付けると良いでしょう。

あとは、「デプロイ」ボタンを押して完了です。
最初のデプロイは完了までに数分かかることがあるので辛抱強く待ってください。

エラーが出ていなければデプロイ完了です。自身のアプリのURLにアクセスして、スーパーユーザーアカウントにログインできるか、などを確認してみましょう。

以上です。お疲れさまでした。

# 参考
- [データベースを Elastic Beanstalk 環境に追加する](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/using-features.managing.db.html)
- [Python アプリケーション環境に Amazon RDS DB インスタンスを追加](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create-deploy-python-rds.html#python-rds-create)
- [アプリケーションソースバンドルを作成する](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/applications-sourcebundle.html)
- [Tutorial: Deploying Python 3, Django, PostgreSQL to AWS Elastic Beanstalk](https://www.1strategy.com/blog/2017/05/23/tutorial-django-elastic-beanstalk/)
- [Deploying Django + Python 3 + PostgreSQL to AWS Elastic Beanstalk](https://realpython.com/deploying-a-django-app-and-postgresql-to-aws-elastic-beanstalk/#configuring-a-database)
- [[Django] プロジェクト構成のベストプラクティスを探る - ２．設定ファイルを本番用と開発用に分割する](https://qiita.com/okoppe8/items/e60d35f55188c0ab9ecc)
- [Elastic Beanstalk Python プラットフォームを使用する](https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/create-deploy-python-container.html#python-namespaces)
- [Djangoにおける静的ファイル(static file)の取り扱い](https://qiita.com/saira/items/a1c565c4a2eace268a07)