LoginSignup
3
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-10-13

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

django-wsgi-full.jpg

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

環境

  • 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 つのロールに分割して再利用しやすくします。

├── inventories/
├── roles/
│   ├── django/
│   ├── nginx/
│   └── python3/

Python3ロール

Python3 のロールは前々回の記事と同じで、python3、python3-devel、python3-libs、python3-pip の RPM パッケージをインストールします。yum で python3-devel をインストールすれば、その他は依存関係で自動的にインストールされます。

roles/python3/
│   ├── python3/
│   │   └── tasks/
│   │       └── main.yml
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)

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

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

Nginxのインストール

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

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/を作ります。

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/にあるファイルはインクルードされません。

/etc/nginx/nginx.conf
    include /etc/nginx/conf.d/*.conf;

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

roles/nginx/files/etc/nginx/conf.d/sites-enabled.conf
include /etc/nginx/sites-enabled/*;

ファイルをconf.d/にデプロイするタスクです。

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 サービスを起動状態にします。

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 ドメインソケットの構成をまとめたので、少し複雑になります。先に前回の記事で構成と設定を把握してください。

  • files/にはデプロイする Django プロジェクトのファイルをコピーしておきます。ただしsettings.pyは別途テンプレートでデプロイするのでここには含めません。
  • templates/には前回の記事で説明した各種設定ファイルとユニットファイルのテンプレートを用意します。
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 ファイルの変数にまとめて定義します。

├── inventories/
│   ├── development/
│   │   ├── group_vars/
│   │   │   ├── all.yml
  • django_project_base_dir: Django プロジェクトをデプロイするディレクトリで、settings.pyBASE_DIRと同じになります。どこでもいいのですが、本稼働環境へのデプロイなので/opt/の下にしました。
  • gunicorn_run_dir: Gunicorn の UNIX ドメインソケットと PID ファイルを置くディレクトリです。変更することはないのでハードコーディングでもいいのですが、複数の設定ファイル/ユニットファイルの中で明示的に同じ設定値にするために定義します。
  • service_userservice_group: Gunicorn サービスの実行ユーザーとグループです。任意ですが、ここでは Nginx のユーザーを使用します。
  • データベースの各接続パラメータは前々回の記事と同様です。
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パッケージをインストールするタスク

前々回の記事と同様に、RPM と pip のパッケージをインストールします。下記は Aurora の MySQL と近いバージョンにするために MySQL 5.7 のクライアントをインストールしていますが、MySQL 8.0 や MariaDB に変更しても構いません。

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 プロジェクトのファイルをデプロイします。

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をテンプレートから生成してデプロイします。

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 で定義した変数を適用する部分です。下記以外の部分はプロジェクトの設定に合わせて作成します。

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

前回の記事で説明したように、OS の起動時にソケット用のディレクトリを systemd-tmpfiles に生成してもらうために、設定ファイルgunicorn.confをテンプレートから生成してデプロイします。

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"
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コマンドを実行します。

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

ソケットのユニットファイルをテンプレートから生成するタスク

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

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 公式ドキュメントにはありませんが、ソケット起動前に確実にディレクトリが生成されるように追加してみました。

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コマンドを実行します。

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

Gunicornサービスのユニットファイルをテンプレートから生成して起動するタスク

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

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を削除してみました。

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

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

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/に作成して有効化します。

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 ソケットを設定します。

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 をリロードします。

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

記事一覧

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4