はじめに
Ansibleを使ってUbuntu18.04にDjangoアプリをデプロイしてみました。
作成したプレイブックの動作確認にはEC2を使用しました。
構築するシステムの構成は下図の通りです。
CondaでDjangoの実行環境を管理し、DBにはCondaでインストールしたPostgreSQL、WebサーバはApache(+mod_wsgi)を使います。
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
のみ、以下の記事に記載した方法でデータベースへの接続情報を管理するように手を加えています。
今回動作環境のために用意した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
に環境変数を指定します。
PostgreSQLの PGDATA
とDjangoの DJANGO_SETTINGS_MODULE
のみ指定しておきます。
environment: PGDATA: "{{ project_dir }}/postgres_data" DJANGO_SETTINGS_MODULE: "example.settings"
タスク
以降に実装した各タスクと対応するコマンドイメージを記載します。
Ansibleの変数参照部分 {{...}}
はコマンド例では ${...}
と記述します。
aptパッケージのアップデート、アップグレード
aptを使います。
キャッシュのアップデートは1日( cache_valid_time: 86400
)です。
upgrade
は update_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プロジェクトのアップロード
fileでDjangoプロジェクトのインストール先ディレクトリを作成し、 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ファイルの作成
copyで secrets.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
データベースクラスタの作成
shellで initdb
を呼び出し、データベースクラスタを作成します。
- 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サービスの作成・起動
copyでPostgreSQLサービスを作成します。
直接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"
- 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のインストール
unarchiveでmod_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 }}"
shellで mod_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
サイトの設定
copyでApacheのサイトを設定し、commandで有効化します。
最後にsystemdでApacheを再起動します。
- 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
です。
プレイブックの実行
起動したインスタンスには標準で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
にアクセスすると、無事ログイン画面が表示されました。
さいごに
初めてAnsibleを触ってみましたが、思っていたよりも簡単に利用できて好印象でした。 ドキュメントも充実していて、今回試した範囲だとそれほど悩むこともなかったです。 対象ホストにPythonさえ入っていれば良いので導入の障壁も低いと思います。 今後本格的に利用を検討していきたいです。