Django + MySQL で開発したアプリケーションの本稼働用の環境を、Ansible を使って Amazon EC2 上に構成します。
以下の 2つの記事の続きで、今回で完成です。
- 前々回: DjangoをAmazon Linux 2にAnsibleで構成する、Aurora (MySQL) をCloudFormationでUTF-8に構成する、そして接続するまでが地味に大変だったこと
- 前回: Nginx + Gunicorn + Django + Aurora (MySQL) の構成を図で説明してみる
環境
- 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 をインストールすれば、その他は依存関係で自動的にインストールされます。
│ ├── 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/
からインクルードするのが人気のようなので、これに合わせるためにもう一工夫します。
│ ├── nginx/
│ │ ├── files/
│ │ │ └── etc/
│ │ │ └── nginx/
│ │ │ └── conf.d/
│ │ │ └── sites-enabled.conf
│ │ └── tasks/
│ │ └── main.yml
Nginxのインストール
amazon-linux-extras コマンドを使用して nginx1.12 リポジトリを有効化してから、yum で nginx パッケージをインストールします。
- 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/
を作ります。
- 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/
にあるファイルはインクルードされません。
include /etc/nginx/conf.d/*.conf;
デフォルトの設定ファイルを変更しないでsites-enabled/
をインクルードパスにするために、以下のファイルをconf.d/
にデプロイします。
include /etc/nginx/sites-enabled/*;
ファイルをconf.d/
にデプロイするタスクです。
- name: Copy Nginx configuration files into nginx/conf.d/
copy:
dest: "/etc/nginx/conf.d/"
src: "{{ role_path }}/files/etc/nginx/conf.d/"
最後に Nginx サービスを起動状態にします。
- 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/
には前回の記事で説明した各種設定ファイルとユニットファイルのテンプレートを用意します。
│ ├── 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.py
のBASE_DIR
と同じになります。どこでもいいのですが、本稼働環境へのデプロイなので/opt/
の下にしました。 - gunicorn_run_dir: Gunicorn の UNIX ドメインソケットと PID ファイルを置くディレクトリです。変更することはないのでハードコーディングでもいいのですが、複数の設定ファイル/ユニットファイルの中で明示的に同じ設定値にするために定義します。
- service_user/service_group: Gunicorn サービスの実行ユーザーとグループです。任意ですが、ここでは Nginx のユーザーを使用します。
- データベースの各接続パラメータは前々回の記事と同様です。
# 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 に変更しても構いません。
- 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 プロジェクトのファイルをデプロイします。
- 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
をテンプレートから生成してデプロイします。
- 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 で定義した変数を適用する部分です。下記以外の部分はプロジェクトの設定に合わせて作成します。
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
前回の記事で説明したように、OS の起動時にソケット用のディレクトリを systemd-tmpfiles に生成してもらうために、設定ファイルgunicorn.conf
をテンプレートから生成してデプロイします。
- 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"
#Type Path Mode UID GID Age Argument
d {{ gunicorn_run_dir }} 0755 {{ service_user }} {{ service_group }} -
gunicorn.conf
をデプロイしたタイミングでディレクトリが作成されるように、ハンドラでsystemd-tmpfiles
コマンドを実行します。
- name: Create systemd-tmpfiles
command: /usr/bin/systemd-tmpfiles --create
listen: "systemd-tmpfiles changed"
ソケットのユニットファイルをテンプレートから生成するタスク
UNIX ドメインソケットのユニットファイルgunicorn.socket
をテンプレートから生成してデプロイします。Gunicorn サービスが起動する時に systemd に起動されるので、ソケットの起動設定は必要ありません。
- 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 公式ドキュメントにはありませんが、ソケット起動前に確実にディレクトリが生成されるように追加してみました。
[Unit]
Description=gunicorn socket
[Socket]
ListenStream={{ gunicorn_run_dir }}/socket
ExecStartPre=/usr/bin/systemd-tmpfiles --create
[Install]
WantedBy=sockets.target
gunicorn.conf
をデプロイした後すぐにディレクトリが生成されるように、ハンドラでもsystemd-tmpfiles
コマンドを実行します。
- name: Create systemd-tmpfiles
command: /usr/bin/systemd-tmpfiles --create
listen: "systemd-tmpfiles changed"
Gunicornサービスのユニットファイルをテンプレートから生成して起動するタスク
Gunicorn サービスのユニットファイルgunicorn.service
をテンプレートから生成してデプロイします。
- 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
を削除してみました。
[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
ユニットファイルが変更されたらハンドラでサービスを再起動します。
- name: Restart gunicorn.service
systemd:
daemon_reload: yes
name: "gunicorn.service"
state: restarted
listen: "gunicorn.service unitfile changed"
Nginxの仮想ホスト設定ファイルをテンプレートから生成するタスク
ソケットで Gunicorn と接続する仮想ホストの設定ファイルを、テンプレートから生成してsites-available/
にデプロイし、そのシンボリックリンクをsites-enabled/
に作成して有効化します。
- 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 ソケットを設定します。
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 をリロードします。
- 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に構成する、そして接続するまでが地味に大変だったこと
- 2/4: Nginx + Gunicorn + Django + Aurora (MySQL) の構成を図で説明してみる
- 3/4: Nginx + Gunicorn + Django + Aurora (MySQL) の本番環境をAnsible Playbookで構成する
- 4/4: NginxとGunicornの接続をソケットからHTTPに変更した