DjangoをAmazon Linux 2にAnsibleで構成する、Aurora (MySQL) をCloudFormationでUTF-8に構成する、そして接続するまでが地味に大変だったこと

タイトルの環境をわりと簡単に構成できると思って始めたのですが、次々に発生するエラーを1つずつ地道に解決して、やっとできたと思ったら文字コードが latin ということに後から気付いたりして、思いのほか手間がかかってしまいました。
何とか解決したコードをまとめたので、誰かのお役に立てたら嬉しいです。

![django-runserver.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/0d5d0d02-c615-ad72-fd13-eb0903834233.jpeg)

# 環境
本番環境なので venv などの仮想環境は導入せず、Django 専用の EC2 とします。

* OS: Amazon Linux 2（Amazon EC2）
* データベース: Amazon Aurora Serverless（MySQL 5.6.10）

Aurora と Django プロジェクトの文字コードを UTF-8 に、地域と言語を日本／日本語に設定します。

# インストール
以下、Ansible と AWS CloudFormation を使用します。

## Python3

Ansible で python3、python3-devel、python3-libs、python3-pip をインストールします。python3-devel 以外は依存パッケージとしてインストールされます。

```Yaml:/roles/python3/tasks/main.yml
- name: Install Python3 packages
  yum:
    name:
      - python3-devel
    state: present
```

## Django
Django と一緒に pip でインストールする「mysqlclient」の依存パッケージ（RPM）を先にインストールします。

まず MySQL の yum リポジトリをインストールします。

```Yaml:/roles/django/tasks/main.yml
- name: Install MySQL 5.7 repository
  yum:
    name:
      - https://dev.mysql.com/get/mysql80-community-release-el7-3.noarch.rpm
    state: present
```

MySQL 関連パッケージと gcc をインストールします。Aurora を使用するので、MySQL Server はインストールしません。
Aurora Serverless の MySQL が 5.6 なので、近いところで 5.7 を使うことにしました。

また Amazon Linux 2 にデフォルトでインストールされている mariadb-libs は自動的にアンインストールされます。

```Yaml:/roles/django/tasks/main.yml
- name: Install rpm packages required by mysqlclient
  yum:
    # 'mysql*'にマッチするリポジトリを全部無効化して
    # 'mysql57-community'を有効化し、5.7だけをインストール対象にする
    disablerepo: "mysql*"
    enablerepo: mysql57-community
    name:
      - gcc
      - mysql-community-devel
      - mysql-community-client
    state: present
```

以上の RPM パッケージをインストール後に、pip を使用して django と mysqlclient、および nginx と接続するための gunicorn をインストールします。gcc は mysqlclient をインストールするために使用されます。

```Yaml:/roles/django/tasks/main.yml
- name: Install Django packages with pip3
  pip:
    executable: pip3
    name:
      - django
      - gunicorn
      - mysqlclient
    state: present
```
動作確認のために Django のプロジェクトを作って、Django 開発サーバを起動します。

```Shell:
django-admin.py startproject mysite
cd mysite/
python3 manage.py runserver
```
ところが以下のように SQLite のバージョンが古すぎるというエラーで、開発サーバを起動することができません。

```Shell:
django.core.exceptions.ImproperlyConfigured: SQLite 3.8.3 or later is required (found 3.7.17).
```

このエラーは SQLite を 3.8.3 以上にするか、Django 2.2.x を 2.1.x にダウングレードすることで回避することができます。
しかし MySQL を使う場合は SQLite を使わないので、このエラーを無視して MySQL の環境を構築します。

## Aurora Serverless (MySQL 5.6)
ここでは AWS CloudFormation を使用します。
-> [Amazon Aurora MySQLリファレンス](https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Reference.html)

まず Aurora の主なパラメータを定義します。
-> [RDS リソースタイプのリファレンス » AWS::RDS::DBCluster](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbcluster.html)

```Yaml:
Parameters:
  # 指定した名前で自動的に CREATE DATABASE される
  MyRdsDatabaseName:
    Description: RDS database name.
    Type: String
    Default: MySchema

  MyRdsMasterUsername:
    Description: RDS master user name.
    Type: String
    Default: myadmin

  MyRdsMasterUserPassword:
    Description: RDS master user password.
    Type: String
    Default: myp@ssword

  # 最悪 Aurora が応答しなくてもよい時間帯（毎日 30分間以上）を UTC で指定する
  MyRdsPreferredBackupWindow:
    Description: RDS PreferredBackupWindow.
    Type: String
    Default: '18:10-18:40'

  # 最悪 Aurora が応答しなくてもよい時間帯（毎週 30分間以上）を UTC で指定する
  MyRdsPreferredMaintenanceWindow:
    Description: RDS PreferredMaintenanceWindow.
    Type: String
    Default: 'Sun:18:40-Sun:19:10'

  # 高負荷時にここで指定した Aurora DB クラスターの最大容量までスケーリングする
  MyRdsScalingMaxCapacity:
    Description: RDS ScalingConfiguration MaxCapacity.
    Type: Number
    Default: 4

  # 指定した秒数の間アクセスがないと Aurora が一時停止して課金されなくなる
  MyRdsScalingSecondsUntilAutoPause:
    Description: RDS ScalingConfiguration SecondsUntilAutoPause.
    Type: Number
    Default: 300
```

Aurora 用のサブネットグループを定義します。冗長化のために 2つのサブネットを指定します。
-> [RDS リソースタイプのリファレンス » AWS::RDS::DBSubnetGroup](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbsubnet-group.html)

```Yaml:
Resources:
  MyRdsDbSubnetGroup:
    Type: AWS::RDS::DBSubnetGroup
    Properties:
      DBSubnetGroupDescription: Database subnets for Aurora Serverless
      DBSubnetGroupName: my-subnet-group
      SubnetIds:
        - !Ref MySubnet1
        - !Ref MySubnet2
      Tags:
        - Key: 'Name'
          Value: 'my-rds-subnet-group'
```

Aurora のパラメータグループです。MySQL Server のように my.cnf ファイルを使って設定することができないので、パラメータグループに必要なパラメータを設定します。
-> [RDS リソースタイプのリファレンス » AWS::RDS::DBClusterParameterGroup](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-rds-dbclusterparametergroup.html)

```Yaml:
  MyRdsDbClusterParameterGroup:
    Type: AWS::RDS::DBClusterParameterGroup
    Properties:
      Description: CloudFormation Aurora Cluster Parameter Group
      Family: aurora5.6
      Parameters:
        collation_server:       utf8_general_ci
        character_set_client:   utf8
        character_set_database: utf8
        character_set_server:   utf8
        server_audit_logging: 0
        time_zone: 'Asia/Tokyo'
      Tags:
        - Key: 'Name'
          Value: 'my-rds-cluster-param-group'
```

`character_set_*`を指定しないと全部 latin1 になってしまうので、必要に応じて utf8 や utf8mb4 に変更しておきます。同時に照合順序も`collation_server`に設定します。

個人的にはこれまでずっと SQL文で`CREATE TABLE ... DEFAULT CHARSET=utf8`のようにテーブルごとに詳細設定をコーディングしてきたので、データベースのデフォルトの設定はあまり気にしていませんでした。
しかし今回は Django の機能を使って Python のモデルクラスからテーブルを生成することにしたので、デフォルト値をしっかり設定しておく必要があります。

以上の定義を使って DBCluster 本体を定義します。

```Yaml:
  MyDbCluster:
    Type: AWS::RDS::DBCluster
    Properties:
      DBClusterIdentifier: my-main-db
      DBClusterParameterGroupName:
        !Ref MyRdsDbClusterParameterGroup
      DBSubnetGroupName:
        !Ref MyRdsDbSubnetGroup
      DatabaseName:
        !Ref MyRdsDatabaseName
      Engine: aurora
      EngineMode: serverless
      EngineVersion: '5.6.10a'
      MasterUsername:
        !Ref MyRdsMasterUsername
      MasterUserPassword:
        !Ref MyRdsMasterUserPassword
      PreferredBackupWindow:
        !Ref MyRdsPreferredBackupWindow
      PreferredMaintenanceWindow:
        !Ref MyRdsPreferredMaintenanceWindow
      ScalingConfiguration:
        AutoPause: true
        MaxCapacity:
          !Ref MyRdsScalingMaxCapacity
        MinCapacity: 1
        SecondsUntilAutoPause:
          !Ref MyRdsScalingSecondsUntilAutoPause
      VpcSecurityGroupIds:
        - !Ref MyDbSecurityGroup
```

データベースが生成されたらエンドポイントに接続して、パラメータグループに設定したとおりの文字セットと照合順序になっていることを確認します。

```Shell:
mysql -h my-main-db.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com -u myadmin -p
Enter password:
```

```Shell:
mysql> show variables like '%char%';
+--------------------------+----------------------------------------------------------+
| Variable_name            | Value                                                    |
+--------------------------+----------------------------------------------------------+
| character_set_client     | utf8                                                     |
| character_set_connection | utf8                                                     |
| character_set_database   | utf8                                                     |
| character_set_filesystem | binary                                                   |
| character_set_results    | utf8                                                     |
| character_set_server     | utf8                                                     |
| character_set_system     | utf8                                                     |
| character_sets_dir       | /rdsdbbin/oscar-5.6.serverless_10a.184.0/share/charsets/ |
+--------------------------+----------------------------------------------------------+

mysql> show variables like '%collat%';
+----------------------+-----------------+
| Variable_name        | Value           |
+----------------------+-----------------+
| collation_connection | utf8_general_ci |
| collation_database   | utf8_general_ci |
| collation_server     | utf8_general_ci |
+----------------------+-----------------+
```

生成されたデータベースの設定も`status`コマンドで表示して、設定した値になっていることを確認します。

```Shell:
mysql> use MySchema;
Database changed
mysql> status
--------------
mysql  Ver 14.14 Distrib 5.7.27, for Linux (x86_64) using  EditLine wrapper

Connection id:          4
Current database:       MySchema
Current user:           myadmin@1.2.3.4
SSL:                    Cipher in use is AES256-SHA
Current pager:          stdout
Using outfile:          ''
Using delimiter:        ;
Server version:         5.6.10 MySQL Community Server (GPL)
Protocol version:       10
Connection:             my-main-db.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com via TCP/IP
Server characterset:    utf8
Db     characterset:    utf8
Client characterset:    utf8
Conn.  characterset:    utf8
TCP port:               3306
```

# Djangoプロジェクトの設定

## settings.py

Django をインストールした直後に作った動作確認用のプロジェクトは、以下のようなファイル構成になっています。

```
mysite
├── mysite/
│   ├── __pycache__/
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
└── manage.py
```

この中の settings.py を変更します。
データベースを SQLite から MySQL に変更して、Aurora に設定したパラメータとエンドポイントを設定します。

```Python:変更前
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.sqlite3',
        'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
    }
}
```

また Aurora ではデフォルトの`sql_mode`が空なので、必要に応じて SQLモードを設定します。以下は MySQL 5.7 と同じ設定にする場合の例です。

```Python:変更後
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'MySchema',
        'USER': 'myadmin',
        'PASSWORD': 'myp@ssword',
        'HOST': 'my-main-db.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com',
        'PORT': '3306',
        'OPTIONS': {
            'charset': 'utf8',
            'sql_mode': 'ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION',
        },
    },
}
```

Web の接続許可設定を追加

```Python:変更前
ALLOWED_HOSTS = []
```

```Python:変更後
ALLOWED_HOSTS = ["*"]
```

また必要に応じて、言語とタイムゾーンを変更します。

```Python:変更前
LANGUAGE_CODE = 'en-us'

TIME_ZONE = 'UTC'
```

```Python:変更後
LANGUAGE_CODE = 'ja-jp'

TIME_ZONE = 'Asia/Tokyo'
```
## 動作確認

### Aurora との接続
プロジェクトのディレクトリにある manage.py の`dbshell`コマンドを使って Aurora に接続します。

```Shell:
python3 manage.py dbshell
```

正常にプロンプトが表示されれば、上記で settings.py に設定したデータベース接続情報が正しく認識されています。

```
mysql: [Warning] Using a password on the command line interface can be insecure.
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
（略）
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> 
```
`quit`か`\q`を入力して終了します。

### Django 開発サーバ
Django インストール直後と同様に Django 開発サーバを起動します。

```Shell:
python3 manage.py runserver
```

データベースを SQLite から MySQL に変更したので、SQLite のバージョンに関するエラーは発生しなくなりました。正常に起動すると以下のように表示されます。

```Shell:
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
September 29, 2019 - 20:03:56
Django version 2.2.5, using settings 'mysite.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
```

表示された`http://127.0.0.1:8000/`に接続できれば、開発サーバが正しく動作しています。

```Shell:
wget --spider http://127.0.0.1:8000/

Spider mode enabled. Check if remote file exists.
--2019-09-29 11:05:14--  http://127.0.0.1:8000/
Connecting to 127.0.0.1:8000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 16504 (16K) [text/html]
Remote file exists and could contain further links,
but recursion is disabled -- not retrieving.
```

# 次の課題

* Nginx -> Gunicorn -> Django の接続環境を Ansible で構成すること

## 記事一覧
この記事には続きがあります。

* 1/4: DjangoをAmazon Linux 2にAnsibleで構成する、Aurora (MySQL) をCloudFormationでUTF-8に構成する、そして接続するまでが地味に大変だったこと
* 2/4: [Nginx + Gunicorn + Django + Aurora (MySQL) の構成を図で説明してみる](https://qiita.com/3244/items/c83d9449f0286707b2fb)
* 3/4: [Nginx + Gunicorn + Django + Aurora (MySQL) の本番環境をAnsible Playbookで構成する](https://qiita.com/3244/items/0d2ae54ccaf72a3b3559)
* 4/4: [NginxとGunicornの接続をソケットからHTTPに変更した](https://qiita.com/3244/items/8cdcfae9d2ee52f2638d)