AnsibleでUbuntu18.04にDjangoアプリをデプロイ(Apache+PostgreSQL+Miniconda)

はじめに

Ansibleを使ってUbuntu18.04にDjangoアプリをデプロイしてみました。
作成したプレイブックの動作確認にはEC2を使用しました。
構築するシステムの構成は下図の通りです。 CondaでDjangoの実行環境を管理し、DBにはCondaでインストールしたPostgreSQL、WebサーバはApache(+mod_wsgi)を使います。

f:id:hiroki-sawano:20200801022123p:plain

Ansibleのインストール

pipでAnsibleをインストールします。また、ユーザ作成時に使用する password_hash で必要となる passlib もインストールします。

$ pip install ansible
$ pip install passlib # パスワードのハッシュ化に必要

ディレクトリ構成

site.yml がシステムの構成を定義したプレイブックです。
通常は色々とファイルを分けるべきですが、今回はシンプルさ優先で一つのファイルに全て詰め込んでます。
example がデプロイするDjangoのプロジェクト、 example/environments.yml が実行環境を定義したConda環境の設定ファイルです。

site.yml
example
├── manage.py
├── example
│  ├── __init__.py
│  ├── urls.py
│  ├── asgi.py
│  ├── wsgi.py
│  └── settings.py
├── environments.yml

用意したDjangoプロジェクトはほとんど django-admin startproject example で生成したままの状態ですが、 settings.py のみ、以下の記事に記載した方法でデータベースへの接続情報を管理するように手を加えています。

hiroki-sawano.hatenablog.com

今回動作環境のために用意したConda環境には最低限のパッケージのみインストールすることにします。

name: my_env
channels:
  - defaults
dependencies:
  - django=3.0.3
  - postgresql=11.2
  - psycopg2=2.7.6.1
  - python=3.7.3

作成したプレイブック

以下が作成したプレイブックです。各設定内容について後述します。
初プレイブックなので不備等あるかもしれません。

共通設定

1つのEC2インスタンスに対してコマンドを実行するだけなのでホストは全指定( hosts: all )にします。 ユーザはUbuntu AMIのデフォルトユーザで( user: ubuntu )、 root 権限へのエスカレーションを可能とします( become: yes )。

- hosts: all
  user: ubuntu
  become: yes

変数

vars に各種変数を指定します。用途は以下のコメントに記載の通りです。

  vars:
    user: "foo" # Djangoアプリを実行するユーザ
    password: "bar" # ユーザのパスワード
    pub_key: "<your-pub-key>" # SSHで使用する公開鍵
    conda_env_name: "my_env" # Conda環境の名前
    db_name: "my_db" # DBの名前
    db_user: "my_db_user" # DBのユーザ
    db_password: "my_db_password" # DBユーザのパスワード
    app_name: "example" # デプロイするアプリの名前
    app_src: "example" # ローカルにあるDjangoプロジェクトへのパス
    conda_yml_file: "example/environments.yml" # ローカルにあるCondaの環境設定ファイルへのパス
    home: "/home/{{ user }}" # ユーザのホーム(変更不要)
    conda_prefix: "{{ home }}/miniconda3" # Minicondaのインストール先
    conda_env: "{{ conda_prefix }}/envs/{{ conda_env_name }}" # Conda環境のパス(変更不要)
    project_dir: "{{ home }}/{{ app_name }}" # Djangoプロジェクトのデプロイ先
    project_package: "{{ project_dir }}/example" # wsgi.pyを持つプロジェクトパッケージ

環境変数

environment環境変数を指定します。 PostgreSQLPGDATADjangoDJANGO_SETTINGS_MODULE のみ指定しておきます。

  environment:
    PGDATA: "{{ project_dir }}/postgres_data"
    DJANGO_SETTINGS_MODULE: "example.settings"

タスク

以降に実装した各タスクと対応するコマンドイメージを記載します。
Ansibleの変数参照部分 {{...}} はコマンド例では ${...} と記述します。

aptパッケージのアップデート、アップグレード

aptを使います。
キャッシュのアップデートは1日( cache_valid_time: 86400 )です。
upgradeupdate_cache と同じく boolean かと思いきや文字列なのでクォーテーションで囲まないと警告が出ました。

- name: Update and upgrade apt packages
  apt:
    upgrade: "yes"
    update_cache: yes
    cache_valid_time: 86400

$ sudo apt update
$ sudo apt upgrade -y

タイムゾーンの設定

timezoneタイムゾーンAsia/Tokyo に設定します。

- name: Set timezone to Asia/Tokyo
  timezone:
    name: Asia/Tokyo

$ sudo timedatectl set-timezone Asia/Tokyo

ユーザの追加

user{{ user }} を作成し、 sudo 権限を与えます。

- name: Add user and add it to sudo
  user:
    name: "{{ user }}"
    shell: /bin/bash
    state: present
    groups: sudo
    append: yes
    password: "{{ password | password_hash('sha512') }}"

$ sudo adduser ${user}
$ sudo gpasswd -a ${user} sudo
$ sudo su - ${user}

SSH接続の設定

authorized_key.ssh/authorized_keys を設定し、 {{ user }}SSH接続できるようにします。

- name: Set authorized key for newly created user
  authorized_key:
    user: "{{ user }}"
    state: present
    key: "{{ pub_key }}"

$ cd ~
$ mkdir .ssh
$ chmod 700 .ssh
$ touch .ssh/authorized_keys
$ chmod 600 .ssh/authorized_keys
$ echo ${pub_key} > ./.ssh/authorized_keys

Minicondaのインストール

get_urlでMinicondaのインストーラーをダウンロードします。

- name: Download Miniconda
  get_url:
    url: https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
    dest: "{{ home }}/miniconda3.sh"
    mode: '0755'

shellでMinicondaをインストールします。
スクリプトはバッチモード( -b オプション)で起動することで対話を抑止し、続けて conda init を別途実行しています。

- name: Install Miniconda
  shell: "bash {{ home }}/miniconda3.sh -b -p {{ conda_prefix }}"
  args:
    creates: "{{ conda_prefix }}"
  become: yes
  become_user: "{{ user }}"
- name: Init conda
  shell: "{{ conda_prefix }}/bin/conda init bash"
  args:
    executable: /bin/bash
  become: yes
  become_user: "{{ user }}"

$ wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh
$ bash Miniconda3-latest-Linux-x86_64.sh -p ${conda_prefix}
...
Do you wish the installer to initialize Miniconda3
by running conda init? [yes|no]
[no] >>> yes

Djangoプロジェクトのアップロード

fileDjangoプロジェクトのインストール先ディレクトリを作成し、 copyでローカルのプロジェクトをアップロードします。

- name: Create project directory
  file:
    path: "{{ project_dir }}"
    state: directory
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: '0755'
- name: Upload django project
  copy:
    src: "{{ app_src }}/"
    dest: "{{ project_dir }}/"
    owner: "{{ user }}"
    group: "{{ user }}"
    mode: '0644'
    force: no

$ scp -i /path/to/private/key -r ${app_src} ${user}@${ansible_host}:${project_dir}

Conda環境の作成

copyでCondaの環境設定ファイルをアップロードし、shellでConda環境を作成します。

- name: Upload environments.yml
  copy:
    src: "{{ conda_yml_file }}"
    dest: "{{ home }}/environments.yml"
    owner: "{{ user }}"
    group: "{{ user }}"
- name: Create conda environment
  shell: "{{ conda_prefix }}/bin/conda env create -f {{ home }}/environments.yml -n {{ conda_env_name }}"
  args:
    executable: /bin/bash
    creates: "{{ conda_env }}"
  become: yes
  become_user: "{{ user }}"

$ scp -i /path/to/private/key ${conda_yml_file} ${user}@${ansible_host}:${home}/environments.yml
...
$ conda env create -f ${home}/environments.yml -n ${conda_env_name}

機密情報を記述したJSONファイルの作成

copysecrets.json を作成します。
ファイルではなくテキストを直接指定する場合は src ではなく content を使用します。
SECRET_KEY の部分は本来はランダムな値を生成すべきです。

- name: Create secrets.json
  copy:
    content: '{
      "SECRET_KEY": "t43m32_02dr5popsxczu*_!qyo@d7g!+e*0*@3u#_%vil=)i(u",
      "DB_NAME": "{{ db_name }}",
      "DB_USER": "{{ db_user }}",
      "DB_PASSWORD": "{{ db_password }}",
      }'
    dest: "{{ project_dir }}/secrets.json"
    owner: "{{ user }}"
    group: "{{ user }}"

$ echo '{"SECRET_KEY": ... }' > ${project_dir}/secrets.json

データベースクラスタの作成

shellinitdb を呼び出し、データベースクラスタを作成します。

- name: Create database cluster
  shell: "{{ conda_env }}/bin/initdb"
  register: result
  args:
    executable: /bin/bash
    creates: "{{ project_dir }}/postgres_data"
  become: yes
  become_user: "{{ user }}"

$ initdb

PostgreSQLサービスの作成・起動

copyPostgreSQLサービスを作成します。
直接PostgreSQLをサーバにインストールする場合は自動的にユニットファイルが作成されますが、 今回はConda環境のPostgreSQLを使いたいので自前で作成します。

- name: Create postgres service
  copy:
    content:
      "[Unit]\n
      Description=PostgreSQL database server\n
      After=network.target\n\n
      [Service]\n
      Type=simple\n
      User={{ user }}\n
      ExecStart={{ conda_env }}/bin/postgres -D {{ project_dir }}/postgres_data\n
      ExecReload=/bin/kill -HUP $MAINPID\n
      KillMode=mixed\n
      KillSignal=SIGINT\n
      TimeoutSec=0\n\n
      [Install]\n
      WantedBy=multi-user.target\n"
    dest: "/etc/systemd/system/postgres.service"

systemdでサービスを起動し、自動起動を有効化します。

- name: Start and enable postgres service
  systemd:
    state: started
    name: postgres
    enabled: yes

$ cat << EOF > /etc/systemd/system/postgres.service
[Unit]
Description=PostgreSQL database server
...
WantedBy=multi-user.target
EOF

$ sudo systemctl daemon-reload
$ sudo systemctl start postgres
$ sudo systemctl enable postgres

Djangoの各種初期設定

shellでConda環境をアクティベートした後、DBのマイグレーションや静的ファイルの収集などの作業を実施します。
Conda環境を有効にした状態で複数のコマンドを実行したいので、 with_items を活用しています。
Adminユーザの名前、パスワードは本来変数化すべきです。

- name: Database settings & Collect static files
  shell: ". {{ conda_prefix }}/etc/profile.d/conda.sh && conda activate {{ conda_env_name }} && {{ item }}"
  args:
    executable: /bin/bash
    chdir: "{{ project_dir }}"
  with_items:
    - psql postgres -c "create role {{ db_user }} with login password '{{ db_password }}';"
    - createdb {{ db_name }} -O {{ db_user }}
    - python manage.py migrate
    - python manage.py collectstatic --noinput
    - echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell
  when: result is changed
  become: yes
  become_user: "{{ user }}"

$ conda activate ${conda_env_name}
$ cd ${project_dir}
$ psql postgres -c "create role ${db_user} with login password '${db_password}';"
$ createdb ${db_name} -O ${db_user}
$ python manage.py migrate
$ python manage.py collectstatic --noinput
$ echo "from django.contrib.auth.models import User; User.objects.create_superuser('admin', 'admin@example.com', 'admin')" | python manage.py shell

Apacheのインストール

aptでApache2をインストールします。

- name: Install Apache
  apt:
    pkg:
    - apache2
    - apache2-dev

$ sudo apt-get install -y apache2 apache2-dev

mod_wsgiのインストール

unarchivemod_wsgiをダウンロード・展開します。

- name: Download mod_wsgi
  unarchive:
    src: https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.14.tar.gz
    dest: "{{ home }}"
    remote_src: yes
    owner: "{{ user }}"
    group: "{{ user }}"

shellmod_wsgi をインストールします。

- name: Make mod_wsgi
  shell: "{{ item }}"
  with_items:
    - "./configure --with-python={{ conda_env }}/bin/python"
    - "make"
  args:
    executable: /bin/bash
    chdir: "{{ home }}/mod_wsgi-4.5.14"
    creates: "/usr/lib/apache2/modules/mod_wsgi.so"
  become: yes
  become_user: "{{ user }}"
- name: Install mod_wsgi
  shell: "make install"
  args:
    executable: /bin/bash
    chdir: "{{ home }}/mod_wsgi-4.5.14"
    creates: "/usr/lib/apache2/modules/mod_wsgi.so"

$ cd ~
$ wget https://github.com/GrahamDumpleton/mod_wsgi/archive/4.5.14.tar.gz
$ tar -zxvf 4.5.14.tar.gz
$ cd mod_wsgi-4.5.14/
$ ./configure --with-python=${conda_env}/bin/python
$ make
$ sudo make install

サイトの設定

copyApacheのサイトを設定し、commandで有効化します。
最後にsystemdApacheを再起動します。

- name: Create Apache site
  copy:
    content:
      "LoadFile {{ conda_env }}/lib/libpython3.7m.so.1.0\n
      LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.so\n
      WSGIPythonHome {{ conda_env }}\n\n
      <VirtualHost *:80>\n
        ServerName {{ ansible_host }}\n
        WSGIDaemonProcess PROCESS_GROUP user={{ user }} group={{ user }} python-path={{ project_dir }}\n
        WSGIProcessGroup PROCESS_GROUP\n
        WSGIScriptAlias / {{ project_package }}/wsgi.py process-group=PROCESS_GROUP\n\n
        <Directory {{ project_package }}/>\n
          <Files wsgi.py>\n
            Require all granted\n
          </Files>\n
        </Directory>\n\n
        Alias /static/ {{ project_dir }}/static/\n
        <Directory {{ project_dir }}/static/>\n
          Require all granted\n
        </Directory>\n
        CustomLog  {{ project_dir }}/access_log common\n
        ErrorLog   {{ project_dir }}/error_log\n
      </VirtualHost>\n"
    dest: "/etc/apache2/sites-available/{{ app_name }}.conf"
- name: Disable default site
  command: a2dissite 000-default
- name: Enable django app site
  command: a2ensite {{ app_name }}
- name: Restart Apache
  systemd:
    state: restarted
    name: apache2

$ cat << EOF > /etc/apache2/sites-available/${app_name}.conf
LoadFile ${conda_env}/lib/libpython3.7m.so.1.0
LoadModule wsgi_module /usr/lib/apache2/modules/mod_wsgi.s
...
EOF

$ sudo a2dissite 000-default
$ sudo a2ensite ${app_name}
$ sudo systemctl restart apache2

動作確認

EC2インスタンスの起動

Ubuntu Server 18.04のAMIでインスタンスを立ち上げます。
IPアドレス13.231.182.215 です。

f:id:hiroki-sawano:20200730224706p:plain

f:id:hiroki-sawano:20200730224939p:plain

プレイブックの実行

起動したインスタンスには標準でPython3が搭載されているので、即プレイブックを実行できます。
IPアドレス-i オプションで直接指定します。

$ ansible-playbook --private-key /path/to/private/key -u ubuntu -i 13.231.182.215, site.yml
...
TASK [Install Apache] ************************************************************************************************
changed: [13.231.182.215]

TASK [Download mod_wsgi] *********************************************************************************************
changed: [13.231.182.215]

TASK [Make mod_wsgi] *************************************************************************************************
changed: [13.231.182.215] => (item=./configure --with-python=/home/foo/miniconda3/envs/my_env/bin/python)
changed: [13.231.182.215] => (item=make)

TASK [Install mod_wsgi] **********************************************************************************************
changed: [13.231.182.215]

TASK [Create Apache site] ********************************************************************************************
changed: [13.231.182.215]

TASK [Disable default site] ******************************************************************************************
changed: [13.231.182.215]

TASK [Enable django app site] ****************************************************************************************
changed: [13.231.182.215]

TASK [Restart Apache] ************************************************************************************************
changed: [13.231.182.215]

PLAY RECAP ***********************************************************************************************************
13.231.182.215             : ok=25   changed=24   unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Djangoアプリにアクセス

何も実装していないのでAdminサイトにアクセスできることを確認することにします。
以下の通り http://13.231.182.215/admin にアクセスすると、無事ログイン画面が表示されました。

f:id:hiroki-sawano:20200730225805p:plain

さいごに

初めてAnsibleを触ってみましたが、思っていたよりも簡単に利用できて好印象でした。 ドキュメントも充実していて、今回試した範囲だとそれほど悩むこともなかったです。 対象ホストにPythonさえ入っていれば良いので導入の障壁も低いと思います。 今後本格的に利用を検討していきたいです。