Nginx + Gunicorn + Django + Aurora (MySQL) の本番環境をAnsible Playbookで構成する

Django + MySQL で開発したアプリケーションの本稼働用の環境を、Ansible を使って Amazon EC2 上に構成します。

![django-wsgi-full.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/5352d4c0-6887-e83e-cf1e-cb28be754bcb.jpeg)

以下の 2つの記事の続きで、今回で完成です。

* 前々回: [DjangoをAmazon Linux 2にAnsibleで構成する、Aurora (MySQL) をCloudFormationでUTF-8に構成する、そして接続するまでが地味に大変だったこと](https://qiita.com/3244/items/6000822f3a165e71c56d)
* 前回: [Nginx + Gunicorn + Django + Aurora (MySQL) の構成を図で説明してみる](https://qiita.com/3244/items/c83d9449f0286707b2fb)

# 環境

* OS: Amazon Linux 2（Amazon EC2）
* アプリケーションフレームワーク: Django 2.2.6
* アプリケーションサーバ: Gunicorn 19.9
* Web サーバ: Nginx 1.12 (Amazon Extras)
* データベース: Amazon Aurora Serverless（MySQL 5.6.10 互換）
* 構成ツール: Ansible 2.8.5、AWS CloudFormation

※ venv などの仮想環境は導入せず、Django アプリケーション専用の EC2 とします。
※ Nginx と Gunicorn は同一インスタンス上で UNIX ドメインソケットを使って接続します。
※ オンプレミスの CentOS 7 でも同様の構成が可能です。

# Ansible Playbook
Python3 と Nginx は他の Playbook でも使用するので、以下のように Playbook を 3 つのロールに分割して再利用しやすくします。

```Shell:
├── inventories/
├── roles/
│   ├── django/
│   ├── nginx/
│   └── python3/
```
## Python3ロール
Python3 のロールは[前々回の記事](https://qiita.com/3244/items/6000822f3a165e71c56d#python3)と同じで、python3、python3-devel、python3-libs、python3-pip の RPM パッケージをインストールします。yum で python3-devel をインストールすれば、その他は依存関係で自動的にインストールされます。

```Shell:roles/python3/
│   ├── python3/
│   │   └── tasks/
│   │       └── main.yml
```

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

## Nginxロール
このロールでは Nginx パッケージのインストールと、他の Playbook で使用する場合にも共通の基本設定を行います。

OS が Amazon Linux 2 の場合、Nginx のインストールに少しだけ工夫が必要です。
参考: [AnsibleでEC2のAmazon Linux 2にNginxをインストールする方法の検討（古いJinja2でもOK）](https://qiita.com/3244/items/051a2c44e19ab932dc0f)

また Nginx の標準パッケージでは設定ファイルのインクルードパスが `/etc/nginx/conf.d/`になりますが、最近は仮想サイトの設定ファイルを`/etc/nginx/sites-enabled/`からインクルードするのが人気のようなので、これに合わせるためにもう一工夫します。

```Shell:roles/nginx/
│   ├── nginx/
│   │   ├── files/
│   │   │   └── etc/
│   │   │       └── nginx/
│   │   │           └── conf.d/
│   │   │               └── sites-enabled.conf
│   │   └── tasks/
│   │       └── main.yml
```

### Nginxのインストール
amazon-linux-extras コマンドを使用して nginx1.12 リポジトリを有効化してから、yum で nginx パッケージをインストールします。

```Yaml:roles/nginx/tasks/main.yml
- name: Enable amzn2extra-nginx1.12 repository
  shell: amazon-linux-extras enable nginx1.12
  changed_when: false

- name: Install Nginx packages from amazon-linux-extras
  yum:
    name: nginx
    state: present
```

### 仮想サイト設定ファイルのデプロイ／インクルードディレクトリ
続けて、仮想ホストの設定ファイルをデプロイするディレクトリ`sites-available/`と、インクルードディレクトリ`sites-enabled/`を作ります。

```Yaml:roles/nginx/tasks/main.yml
- name: Create sites-available directory
  file:
    path: "/etc/nginx/sites-available"
    state: directory

- name: Create sites-enabled directory
  file:
    path: "/etc/nginx/sites-enabled"
    state: directory
```

しかしデフォルトの設定ファイルでは、以下のようにインクルードパスが`conf.d/`になっているので、`sites-enabled/`にあるファイルはインクルードされません。

```Nginx:/etc/nginx/nginx.conf
    include /etc/nginx/conf.d/*.conf;
```

デフォルトの設定ファイルを変更しないで`sites-enabled/`をインクルードパスにするために、以下のファイルを`conf.d/`にデプロイします。

```Nginx:roles/nginx/files/etc/nginx/conf.d/sites-enabled.conf
include /etc/nginx/sites-enabled/*;
```
ファイルを`conf.d/`にデプロイするタスクです。

```Yaml:roles/nginx/tasks/main.yml
- name: Copy Nginx configuration files into nginx/conf.d/
  copy:
    dest: "/etc/nginx/conf.d/"
    src:  "{{ role_path }}/files/etc/nginx/conf.d/"
```

最後に Nginx サービスを起動状態にします。

```Yaml:roles/nginx/tasks/main.yml
- name: Start and enable nginx service
  systemd:
    enabled: yes
    name: nginx.service
    state: started
```

これで仮想サイトの設定ファイルをデプロイする時には、ファイルを`sites-available/`にデプロイしてシンボリックリンクを`sites-enabled/`に作成すれば、デフォルトの設定ファイルからインクルードされるようになります。

## Djangoロール
Django のロールには Gunicorn と UNIX ドメインソケットの構成をまとめたので、少し複雑になります。先に[前回の記事](https://qiita.com/3244/items/c83d9449f0286707b2fb)で構成と設定を把握してください。

* `files/`にはデプロイする Django プロジェクトのファイルをコピーしておきます。ただし`settings.py`は別途テンプレートでデプロイするのでここには含めません。
* `templates/`には[前回の記事](https://qiita.com/3244/items/c83d9449f0286707b2fb)で説明した各種設定ファイルとユニットファイルのテンプレートを用意します。

```Shell:roles/django/
│   ├── django/
│   │   ├── files/
│   │   │   └── opt/
│   │   │       └── django/
│   │   │           └── mysite/
│   │   │               ├── myapp/
│   │   │               │   ├── __init__.py
│   │   │               │   ├── admin.py
│   │   │               │   ├── apps.py
│   │   │               │   ├── models.py
│   │   │               │   ├── tests.py
│   │   │               │   ├── urls.py
│   │   │               │   └── views.py
│   │   │               ├── mysite/
│   │   │               │   ├── __init__.py
│   │   │               │   ├── urls.py
│   │   │               │   └── wsgi.py
│   │   │               └── manage.py
│   │   ├── handlers/
│   │   │   └── main.yml
│   │   ├── tasks/
│   │   │   └── main.yml
│   │   └── templates/
│   │       ├── gunicorn.conf.j2
│   │       ├── gunicorn.service.j2
│   │       ├── gunicorn.socket.j2
│   │       ├── nginx.conf.j2
│   │       └── settings.py.j2
```

### varsファイル
多数の設定ファイルとユニットファイルで設定値を合わせる必要があるので、共通の設定値を vars ファイルの変数にまとめて定義します。

```Shell:
├── inventories/
│   ├── development/
│   │   ├── group_vars/
│   │   │   ├── all.yml
```

*  **django_project_base_dir**: Django プロジェクトをデプロイするディレクトリで、`settings.py`の`BASE_DIR`と同じになります。どこでもいいのですが、本稼働環境へのデプロイなので`/opt/`の下にしました。
*  **gunicorn_run_dir**: Gunicorn の UNIX ドメインソケットと PID ファイルを置くディレクトリです。変更することはないのでハードコーディングでもいいのですが、複数の設定ファイル／ユニットファイルの中で明示的に同じ設定値にするために定義します。
*  **service_user**／**service_group**: Gunicorn サービスの実行ユーザーとグループです。任意ですが、ここでは Nginx のユーザーを使用します。
* データベースの各接続パラメータは[前々回の記事](https://qiita.com/3244/items/6000822f3a165e71c56d#settingspy)と同様です。

```Yaml:inventories/development/group_vars/all.yml
# Django / Gunicorn
django_project_name:      "mysite"
django_project_base_dir:  "/opt/django/{{ django_project_name }}"

service_user:       "nginx"
service_group:      "nginx"
gunicorn_run_dir:   "/run/gunicorn"

# DB (Aurora Serverless)
db_server_addres:   "my-main-db.cluster-xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com"
db_default_schema:  "MySchema"
db_user:            "myuser"
db_password:        "myp@ssword"
```

### RPM/pipパッケージをインストールするタスク
[前々回の記事](https://qiita.com/3244/items/6000822f3a165e71c56d#django)と同様に、RPM と pip のパッケージをインストールします。下記は Aurora の MySQL と近いバージョンにするために MySQL 5.7 のクライアントをインストールしていますが、MySQL 8.0 や MariaDB に変更しても構いません。

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

- 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

- name: Install Django packages with pip3
  pip:
    executable: pip3
    name:
      - django
      - gunicorn
      - mysqlclient
    state: present
```

### プロジェクトファイルをデプロイするタスク
デプロイ先ディレクトリを作成して、`files/`にコピーしておいた Django プロジェクトのファイルをデプロイします。

```Yaml:roles/django/tasks/main.yml
- name: Create directory for Django project
  file:
    group:  "{{ service_group }}"
    mode:   0755
    owner:  "{{ service_user }}"
    path:   "{{ django_project_base_dir }}"
    state:  directory

- name: Deploy Django project files
  synchronize:
    checksum: yes
    delete:   yes
    dest:   "{{ django_project_base_dir }}/"
    recursive:  yes
    src:    "{{ role_path }}/files{{ django_project_base_dir }}/"

- name: Change owner of Django project files
  file:
    group:  "{{ service_group }}"
    owner:  "{{ service_user}}"
    path:   "{{ django_project_base_dir}}"
    recurse:  yes
    state:  directory
```

### settings.pyをテンプレートから生成するタスク
プロジェクトのファイルに含めなかった`settings.py`をテンプレートから生成してデプロイします。

```Yaml:roles/django/tasks/main.yml
- name: Deploy settings.py for Django project
  template:
    path:   "{{ django_project_base_dir }}/{{ django_project_name }}/settings.py"
    group:  "{{ service_group }}"
    owner:  "{{ service_user }}"
    src:    "{{ role_path }}/templates/settings.py.j2"
```

以下は`settings.py`のテンプレートの、vars で定義した変数を適用する部分です。下記以外の部分はプロジェクトの設定に合わせて作成します。

```Shell:roles/django/templates/settings.py.j2（一部）
ROOT_URLCONF = '{{ django_project_name }}.urls'

WSGI_APPLICATION = '{{ django_project_name }}.wsgi.application'

# Database
DATABASES = {
    'default': {
        'ENGINE':   'django.db.backends.mysql',
        'NAME':     '{{ db_default_schema }}',
        'USER':     '{{ db_user }}',
        'PASSWORD': '{{ db_password }}',
        'HOST':     '{{ db_server_addres }}',
        'PORT':     '3306',
        'OPTIONS': {
            'charset': 'utf8',
            'sql_mode': 'TRADITIONAL,NO_AUTO_VALUE_ON_ZERO,ONLY_FULL_GROUP_BY',
        },
    }
}
```

### systemd-tmpfilesファイルをテンプレートから生成するタスク
以下、Gunicorn の公式ドキュメントの設定例を元にしています。
参考: Deploying Gunicorn (Systemd): http://docs.gunicorn.org/en/latest/deploy.html#systemd

![gunicorn-settings.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169872/bd7d0531-8748-c27e-d5eb-aba97acf5b47.png)

[前回の記事](https://qiita.com/3244/items/c83d9449f0286707b2fb#unix%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E3%82%BD%E3%82%B1%E3%83%83%E3%83%88)で説明したように、OS の起動時にソケット用のディレクトリを systemd-tmpfiles に生成してもらうために、設定ファイル`gunicorn.conf`をテンプレートから生成してデプロイします。

```Yaml:roles/django/tasks/main.yml
- name: Deploy tmpfiles.d/gunicorn.conf from template
  template:
    path: "/etc/tmpfiles.d/gunicorn.conf"
    group: root
    owner: root
    src:  "{{ role_path }}/templates/gunicorn.conf.j2"
  notify: "systemd-tmpfiles changed"
```

```Shell:roles/django/templates/gunicorn.conf.j2
#Type Path                    Mode  UID                GID                  Age Argument
d     {{ gunicorn_run_dir }}  0755  {{ service_user }} {{ service_group }}  -
```
`gunicorn.conf`をデプロイしたタイミングでディレクトリが作成されるように、ハンドラで`systemd-tmpfiles`コマンドを実行します。

```Yaml:roles/django/handlers/main.yml
- name: Create systemd-tmpfiles
  command: /usr/bin/systemd-tmpfiles --create
  listen: "systemd-tmpfiles changed"
```

### ソケットのユニットファイルをテンプレートから生成するタスク
UNIX ドメインソケットのユニットファイル`gunicorn.socket`をテンプレートから生成してデプロイします。Gunicorn サービスが起動する時に systemd に起動されるので、ソケットの起動設定は必要ありません。

```Yaml:roles/django/tasks/main.yml
- name: Deploy gunicorn.socket unit file from template
  template:
    path: "/etc/systemd/system/gunicorn.socket"
    group: root
    owner: root
    src:  "{{ role_path }}/templates/gunicorn.socket.j2"
  notify: "gunicorn.socket unitfile changed"
```

以下はユニットファイルのテンプレートです。
`ExecStartPre`は Gunicorn 公式ドキュメントにはありませんが、ソケット起動前に確実にディレクトリが生成されるように追加してみました。

```Ini:roles/django/templates/gunicorn.socket.j2
[Unit]
Description=gunicorn socket

[Socket]
ListenStream={{ gunicorn_run_dir }}/socket
ExecStartPre=/usr/bin/systemd-tmpfiles --create

[Install]
WantedBy=sockets.target
```

`gunicorn.conf`をデプロイした後すぐにディレクトリが生成されるように、ハンドラでも`systemd-tmpfiles`コマンドを実行します。

```Yaml:roles/django/handlers/main.yml
- name: Create systemd-tmpfiles
  command: /usr/bin/systemd-tmpfiles --create
  listen: "systemd-tmpfiles changed"
```

### Gunicornサービスのユニットファイルをテンプレートから生成して起動するタスク
Gunicorn サービスのユニットファイル`gunicorn.service`をテンプレートから生成してデプロイします。

```Yaml:roles/django/tasks/main.yml
- name: Deploy gunicorn.service unit file from template
  template:
    path: "/etc/systemd/system/gunicorn.service"
    group: root
    owner: root
    src:  "{{ role_path }}/templates/gunicorn.service.j2"
  notify: "gunicorn.service unitfile changed"

- name: Start and enable gunicorn.service
  systemd:
    enabled: yes
    name: "gunicorn.service"
    state: started
```

以下はユニットファイルのテンプレートです。
`Requires=gunicorn.socket`の設定により、ソケットは systemd が自動的に起動します。
ソケット用のディレクトリは systemd-tmpfiles が生成するので、Gunicorn 公式ドキュメントの設定例から`RuntimeDirectory=gunicorn`を削除してみました。

```Ini:roles/django/templates/gunicorn.service.j2
[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target

[Service]
PIDFile={{ gunicorn_run_dir }}/pid
User={{ service_user }}
Group={{ service_group }}
WorkingDirectory={{ django_project_base_dir }}
ExecStart=/usr/local/bin/gunicorn --pid {{ gunicorn_run_dir }}/pid   \
          --bind unix:{{ gunicorn_run_dir }}/socket {{ django_project_name }}.wsgi
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PrivateTmp=true

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

ユニットファイルが変更されたらハンドラでサービスを再起動します。

```Yaml:roles/django/handlers/main.yml
- name: Restart gunicorn.service
  systemd:
    daemon_reload: yes
    name: "gunicorn.service"
    state: restarted
  listen: "gunicorn.service unitfile changed"
```

### Nginxの仮想ホスト設定ファイルをテンプレートから生成するタスク
ソケットで Gunicorn と接続する仮想ホストの設定ファイルを、テンプレートから生成して`sites-available/`にデプロイし、そのシンボリックリンクを`sites-enabled/`に作成して有効化します。

```Yaml:roles/django/tasks/main.yml
- name: Deploy Nginx virtual site configuration from template
  template:
    path:   "/etc/nginx/sites-available/nginx.{{ django_project_name }}.conf"
    src:    "{{ role_path }}/templates/nginx.conf.j2"
  notify:   "nginx configuration changed"

- name: Enable Nginx virtual site
  file:
    dest:   "/etc/nginx/sites-enabled/nginx.{{ django_project_name }}.conf"
    src:    "/etc/nginx/sites-available/nginx.{{ django_project_name }}.conf"
    state:  link
  notify:   "nginx configuration changed"
```

リバースプロキシの送信先`proxy_pass`に gunicorn ソケットを設定します。

```Shell:roles/django/templates/nginx.conf.j2
server {
    listen       80;
    server_name  {{ ansible_host }};
    server_tokens off;

    location / {
        proxy_pass http://unix:{{ gunicorn_run_dir }}/socket;
        proxy_set_header Host               $host;
        proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Host   $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP          $remote_addr;
    }

    location /static {
        alias {{ django_project_base_dir }}/static;
    }
}
```

設定ファイルが変更されたらハンドラで Nginx をリロードします。

```Yaml:roles/django/handlers/main.yml
- name: Reload nginx.service
  systemd:
    name: "nginx.service"
    state: reloaded
  listen: "nginx configuration changed"
```

以上の Playbook で冒頭の図の構成を実現することができます。

# 参考
元になった Gunicorn 公式ドキュメントの設定
Deploying Gunicorn (Systemd): http://docs.gunicorn.org/en/latest/deploy.html#systemd

Ansible Playbook のベストプラクティス
Working With Playbooks » Best Practices: https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html

## 記事一覧

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