Ansibleを練習した
2014-12-22

Python製の構成管理ツールのAnsibleを練習がてらに使ってみました。

既に素敵な記事がたくさんのあるようですし、同じようなことを書いたところで私の記事など足元にも及ばないので(感じ悪い)、以下の課題でやっていこうと思います。

  • VM(Vagrant)上にでDjangoの開発環境を作る
  • 1台構成だと寂しいのでデータベースは別のホストにする
  • できるだけ再利用できるようにする

環境

対象ホスト ホスト名 OS IPアドレス
Webサーバ web CentOS 6 192.168.33.10
DBサーバ db CentOS 6 192.168.33.11

上記のサーバをVagrantで作成します。以下はVagrantfileの内容です。

VAGRANTFILE_API_VERSION = "2"

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  config.vm.define :db do |db|
    db.vm.box = "chef/centos-6.5"
    db.vm.hostname = "db"
    db.vm.network :forwarded_port, guest: 22, host: 2001, id: "ssh"
    db.vm.network :private_network, ip: "192.168.33.11"
  end

  config.vm.define :web do |web|
    web.vm.box = "chef/centos-6.5"
    web.vm.hostname = "web"
    web.vm.network :forwarded_port, guest: 22, host: 2000, id: "ssh"
    web.vm.network :private_network, ip: "192.168.33.10"
  end

  config.vm.provision :ansible do |ansible|
    ansible.playbook = "site.yml"
    ansible.inventory_path = "hosts"
  end

end

~/.ssh/config に以下を追記します。

Host web
  HostName 192.168.33.10
  User vagrant
  Port 2000
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/user/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

Host db
  HostName 192.168.33.11
  User vagrant
  Port 2001
  UserKnownHostsFile /dev/null
  StrictHostKeyChecking no
  PasswordAuthentication no
  IdentityFile /home/user/.vagrant.d/insecure_private_key
  IdentitiesOnly yes
  LogLevel FATAL

vagrant ssh-config >> ~/.ssh/config としただけでは HostNameが 127.0.0.1 となっているためかうまくいかなかったので、IPアドレスの部分だけ変更してあります。

Playbook

site.ymlの処理を書いていきます。

- hosts: db
  user: vagrant
  sudo: yes
  tasks:
    # dbに対する処理をリスト形式で記述する

- hosts: web
  user: vagrant
  sudo: yes
  tasks:
    # webに対する処理をリスト形式で記述する

以下にtasksに記述する処理を列挙していきます。

[db/web] 共通の処理

どちらのサーバでも必要な処理

# ファイルを転送するために必要らしい(selinux無効にしたのに)
- name: selinux python
  yum: name=libselinux-python state=installed

# iptablesは切る
- name: iptables disabled
  service: name=iptables state=stopped enabled=False

[db] MySQLのインストール

- name: install mysql-server
  yum: name=mysql-server state=installed

- name: run mysql-server
  service: name=mysqld state=running enabled=yes

# 文字コード的な問題を回避するために文字コード設定したmy.cnfを使いたい
- name: transfer my.cnf
  copy: src=my.cnf.local dest=/etc/my.cnf
  notify: mysql restart

# my.cnfが更新されたら再起動する
- name: mysql restart
  service: name=mysqld state=restarted

コピー元の設定ファイルをmy.cnf.localとしてカレントパスに置きます。

[client]
port=3306

default-character-set = utf8

[mysqld]
port = 3306

default-character-set = utf8
default-storage-engine = innodb
character-set-server = utf8
collation-server = utf8_general_ci

datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
symbolic-links=0


[mysqld_safe]
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid

[db] MySQLの操作

Djangoで使うためのユーザとデータベースを用意します。MySQLを操作するためのモジュールがあるので是非それを使いましょう。

# MySQL-pythonのインストールに必要
- name: install mysql-devel
  yum: name=mysql-devel state=installed

# mysql_user,mysel_dbに必要
- name: install MySQL-python
  yum: name=MySQL-python state=installed

# ユーザ:testuser/testpassを作る
- name: create mysql user
  mysql_user: name=testuser password=testpass priv=*.*:ALL state=present host={{item}}
  with_items:
    - '%'
    - 'localhost'

# データベース:testdbを作る
- name: create mysql database
  mysql_db: name=testdb state=present

Djangoを動かす

[web] Pythonのインストール

CentOS6ではデフォルトでPython2.6が入っていますが、色々とアレなのでPython2.7を入れましょう。

# 依存ライブラリを入れる
- name: install libs
  yum: name={{item}} state=installed
  with_items:
    - zlib-devel
    - bzip2-devel
    - openssl-devel

# 既にPython2.7があるか確認して、結果をstatusに格納
- name: check python2.7
  shell: test -a /usr/local/bin/python2.7
  register: status
  ignore_errors: True

# ダウンロードする(Python2.7がないときだけ)
- name: download python2.7
  when: status|failed
  get_url: url=http://www.python.org/ftp/python/2.7/Python-2.7.tgz dest=/usr/local/src/

# コンパイルに必要
- name: install gcc
  yum: name=gcc state=installed

# コンパイル(Python2.7がないときだけ) 結構時間かかるよ!
- name: compile python2.7
  when: status|failed
  shell: |
    cd /usr/local/src/
    tar fxz Python-2.7.tgz
    cd Python-2.7
    ./configure --with-threads --prefix=/usr/local
    make && make altinstall

- name: link python
  shell: |
    ln -sf /usr/local/bin/python2.7 /usr/bin/python2.7
    ln -sf /usr/local/bin/python2.7 /usr/local/bin/python

/usr/bin/python はシステムで使われているらしく、書き換えるとyumが動かなくなるので注意しましょう。 (環境変数的なものをいじったら動くかもしれません)

[web] pipのインストール

まだpipが入ってないので手動でインストールします。

- name: download get-pip
  get_url: url=https://bootstrap.pypa.io/get-pip.py dest=/usr/local/src

- name: install pip
  shell: |
    cd /usr/local/src
    /usr/local/bin/python get-pip.py
    ln -sf /usr/local/bin/pip /usr/bin/pip

# virtualenvをインストール
- name: install venv
  pip: name=virtualenv

[web] Djangoのインストール

pipモジュールでvirtualenv上にdjangoをインストールします。

# djangoをインストール
- name: install django
  pip: name=django virtualenv=/vagrant/venv-project virtualenv_command=/usr/local/bin/virtualenv-2.7
  shell=/usr/local/bin/virtualenv-2.7

# mysql-pythonをインストールするために必要
- name: install mysql-devel
  yum: name=mysql-devel state=installed

# MySQLドライバ
- name: install mysql-python
  pip: name=mysql-python virtualenv=/vagrant/venv-project virtualenv_command=/usr/local/bin/virtualenv-2.7

プロジェクト関連のファイルは本来リポジトリからcloneするものだと思いますが、そんなものはないのでAnsibleでやります。

# プロジェクトディレクトリの有無を確認
- name: check project dir
  shell: test -a /vagrant/myproject
  register: status

# プロジェクトディレクトを作る
- name: mkdir project directory
  shell: mkdir -p /vagrant/myproject
  when: status|failed

# プロジェクトディレクトリにdjangoのプロジェクトを作成する
- name: start project
  shell: /vagrant/venv-project/bin/django-admin startproject myproject /vagrant/myproject
  ignore_errors: True
  when: status|failed

# settings.pyをコピーする
- name: copy settings.py
  copy: src=settings.py.local dest=/vagrant/myproject/myproject/settings.py
  when: status|failed

- name: create tables
  shell: /vagrant/venv-project/bin/python /vagrant/myproject/manage.py migrate

settings.py.localは以下の内容です。DATABASESしか変えてません。

import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

SECRET_KEY = '=5to#)i*pq^mjyguv1lq1j9dqm))4*1k_51)*gs8@%1pz=f$es'
DEBUG = True

TEMPLATE_DEBUG = True

ALLOWED_HOSTS = []
INSTALLED_APPS = (
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
)
MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'myproject.urls'
WSGI_APPLICATION = 'myproject.wsgi.application'
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'testdb',
        'USER': 'testuser',
        'PASSWORD': 'testpass',
        'HOST': '192.168.33.11',
        'PORT': 3306,
    }
}
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
STATIC_URL = '/static/'

site.ymlができたのでVMを起動してみます。

$ vagrant up
$ ssh vagrant@192.168.33.11
$ mysql -utestuser -ptestpass testdb
mysql> -- [web] manage.py migrateでテーブルが作成されている
mysql> show tables;
+----------------------------+
| Tables_in_testdb           |
+----------------------------+
| auth_group                 |
| auth_group_permissions     |
| auth_permission            |
| auth_user                  |
| auth_user_groups           |
| auth_user_user_permissions |
| django_admin_log           |
| django_content_type        |
| django_migrations          |
| django_session             |
+----------------------------+
10 rows in set (0.00 sec)

mysql> Ctrl-C -- exit!

$ ssh vagrant@192.168.33.10
$ /vagrant/venv-project/bin/python /vagrant/myproject/manage.py runserver 0.0.0.0:8000
Django version 1.7.1, using settings 'myproject.settings'
Starting development server at http://0.0.0.0:8000/
Quit the server with CONTROL-C.

起動までできました。

再利用する

再利用しやすくするためにroleという機能を使用します。 単純なincludeでは特定のセクションに依存した記述しかできません。 例えばvarsセクションはtasksセクション内で使えないため「tasksセクションでincludeされたyamlの中で変数(vars)を宣言する」などは不可能です。 roleを定義するにはrolesというディレクトリ配下に機能毎のディレクトリ、さらにその下にtasks,vars,defaultsといったディレクトリを配置します。使用するにはrolesモジュール内で機能名を羅列するだけです。 では先ほどのPlaybookをrolesに分けてみます。

roles/iptables

とりあえずデフォルトで有効。無効化するときは呼び出し元で明示的に指定させることにします。

tasks/main.yml

- name: iptables switch
  service: name=iptables state={{iptables_state}} enabled={{iptables_enabled}}

defaults/main.yml

iptables_state: running
iptables_enabled: True

roles/selinux

tasks/main.yml

- name: include redhat
  when: ansible_os_family == "RedHat"
  include: redhat.yml

tasks/redhat.yml

- name: selinux python
  yum: name=libselinux-python state=installed

roles/mysql

tasks/main.yml

- name: include redhat
  when: ansible_os_family == "RedHat"
  include: redhat.yml

- name: copy my.cnf
  template: src=my.cnf.j2 dest=/etc/my.cnf
  changed_when: False
  notify: mysqld restart

tasks/redhat.yml

- name: install mysql-server
  yum: name=mysql-server state=installed

- name: install mysql-devel
  yum: name=mysql-devel state=installed

- name: install MySQL-python
  yum: name=MySQL-python state=installed

- name: run mysql-server
  service: name=mysqld state=running enabled=yes

handlers/main.yml

handlersセクションの内容をこのyamlに書きましょう。

- name: mysqld restart
  service: name=mysqld state=restarted

templates/my.cnf.j2

設定ファイルの中身も動的に変更したいので、モジュールはcopyではなくtemplateを使うことにしました。 使うテンプレートをtemplatesディレクトリに配置すると同じroleから参照できます。

[client]
port = {{mysql_client_port}}

default-character-set = {{mysql_client_charset}}

{{mysql_client_section}}


[mysqld]
port = {{mysql_server_port}}

default-character-set = {{mysql_server_default_charset}}
default-storage-engine = {{mysql_storage_engine}}
character-set-server = {{mysql_server_charset}}
collation-server = {{mysql_collation_server}}

datadir = {{mysql_datadir}}
socket = {{mysql_socket}}
user = {{mysql_user}}
symbolic-links = {{mysql_symbolic_links}}

{{mysql_mysqld_section}}


[mysqld_safe]
log-error = {{mysql_log_error}}
pid-file = {{mysql_pid_file}}

{{mysql_mysqld_safe_section}}

defaults/main.yml

「mysql」role内で有効なデフォルト値を定義していきます。

mysql_client_port: 3306
mysql_client_charset: utf8

mysql_server_port: 3306
mysql_server_default_charset: utf8
mysql_server_charset: utf8
mysql_storage_engine: innodb
mysql_collation_server: utf8_general_ci

mysql_client_section: ''
mysql_mysqld_section: ''
mysql_mysqld_safe_section: ''

mysql_symbolic_links: 0

mysql_datadir: /var/lib/mysql
mysql_socket: /var/lib/mysql/mysql.sock
mysql_user: mysql

mysql_log_error: /var/log/mysqld.log
mysql_pid_file: /var/run/mysqld/mysqld.pid

もし上記と異なる値を使いたいときは呼び出し元などで定義すれば上書きされます。 そのため変数名は重複しないものをつけるのが良いでしょう。

roles/python

tasks/main.yml

- name: include redhat
  when: ansible_os_family == "RedHat"
  include: redhat.yml

- name: check python
  shell: test -a /usr/local/bin/python{{python_ver}}
  register: status
  ignore_errors: True

- name: download python
  when: status|failed
  get_url: url={{python_src}} dest=/usr/local/src/

- name: compile python
  when: status|failed
  shell: |
    cd /usr/local/src/
    tar fxz Python-{{python_ver}}{{python_ver_patch}}{{python_ver_dev}}.tgz
    cd Python-{{python_ver}}{{python_ver_patch}}{{python_ver_dev}}
    ./configure --with-threads --prefix=/usr/local
    make && make altinstall

- name: link python
  shell: |
    ln -sf /usr/local/bin/python{{python_ver}} /usr/bin/python{{python_ver}}
    ln -sf /usr/local/bin/python{{python_ver}} /usr/local/bin/python

- name: download get-pip
  get_url: url=https://bootstrap.pypa.io/get-pip.py dest=/usr/local/src

- name: install pip
  shell: |
    cd /usr/local/src
    /usr/local/bin/python get-pip.py
    ln -sf /usr/local/bin/pip /usr/bin/pip

- name: install virtualenv
  changed_when: False
  pip: name=virtualenv

tasks/redhat.yml

- name: install gcc
  yum: name=gcc state=installed

- name: install libs
  yum: name={{item}} state=installed
  with_items:
    - zlib-devel
    - bzip2-devel
    - openssl-devel

- name: install python-devel
  yum: name=python-devel state=installed

defaults/main.yml

python_ver: '2.7'
python_ver_patch: ''
python_ver_dev: ''

vars/main.yml

正直defaultsでもいいかなと思ってます。

python_src: "http://www.python.org/ftp/python/{{python_ver}}{{python_ver_patch}}/Python-{{python_ver}}{{python_ver_patch}}{{python_ver_dev}}.tgz"
setuptools_filename: "setuptools-0.6c11-py{{python_ver}}.egg"
setuptools_src: "http://pypi.python.org/packages/{{python_ver}}/s/setuptools/{{setuptools_filename}}"

site.yml

上記のroleを使うように site.yml を変更してみました。

空のプロジェクトとして project_model/requirements.txt を配置しました。 これを provision 時にディレクトリごとコピーします。

Django==1.7.1
MySQL-python==1.2.5
- hosts: db
  user: vagrant
  sudo: yes

  vars:
    iptables_state: stopped
    iptables_enabled: False

  roles:
    - selinux
    - iptables
    - mysql

  tasks:
    - name: create mysql user
      mysql_user: name=testuser password=testpass priv=*.*:ALL state=present host={{item}}
      with_items:
        - '%'
        - 'localhost'

    - name: create mysql database
      mysql_db: name=testdb state=present

- hosts: web
  user: vagrant
  sudo: yes

  vars:
    project_name: myproject
    project_dir: "/vagrant/{{project_name}}"
    project_venv: /vagrant/venv-project
    venv_command: /usr/local/bin/virtualenv-2.7
    iptables_state: stopped
    iptables_enabled: False

  # tasksより後ろに書いても先に実行される
  roles:
    - selinux
    - iptables
    - python

  tasks:
    - name: check project dir
      shell: test -a {{project_dir}}
      ignore_errors: True
      register: status

    - name: copy project dir
      shell: cp -rp /vagrant/project_model {{project_dir}}
      when: status|failed

    - name: install mysql-devel
      yum: name=mysql-devel state=installed

    - name: install requirements.txt
      pip: requirements={{project_dir}}/requirements.txt virtualenv={{project_venv}} virtualenv_command={{venv_command}}

    - name: start project
      shell: "{{project_venv}}/bin/django-admin startproject {{project_name}} {{project_dir}}"
      ignore_errors: True
      when: status|failed

    - name: copy settings.py
      copy: src=settings.py.local dest={{project_dir}}/{{project_name}}/settings.py
      when: status|failed

    - name: create tables
      shell: "{{project_venv}}/bin/python {{project_dir}}/manage.py migrate"

だいぶスッキリしましたね。実行結果は先ほどと同じでした。

これから GitHub で管理していきます。

参考