Ansible Tutorial

あんしぼー ちゅーとりある


概要

July Tech Festa にて開催されたハンズオンの資料が公開されていたことに刺激され、Chef の代わりに Ansible を使う資料を作りました始めました。 Ansible を使って WordPress サーバーのセットアップを行います。
まだ Ansible を試し始めたばかりで自分の勉強がてら書いています。
Puppet にも Chef にも乗り遅れたので Ansible に飛び乗ってみようかと。

※鋭意作成中です。
そのため Github にある playbook を実行した結果と必ずしも同じでは無いことがあります。
https://github.com/yteraoka/ansible-tutorial/ コメントなどお待ちしております。@yteraoka

少しずつ更新してます、また来てね。

Ansible とは

Ansible のサイト (AnsibleWorks | Radically simple IT orchestration) には次のように書いてあります。

Ansible は非常にシンプルなIT構成エンジンです。これはあなたのアプリケーションやシステムのデプロイを容易にします。スクリプトや専用のコードを書くことなくアプリケーションをデプロイし、更新することができます。エージェントをインストールすることなく、SSH を使い、自然な英語に近い感覚で自動化します。
Ansible は最もシンプルな構成管理、自動化ツールです。
以下のサイトも参考になります

目次

  1. Vagrant を使ってサーバーを準備する
  2. Ansible のインストール
  3. Ansible を使うための準備
  4. Ansible の疎通確認
  5. 簡単な Playbook を書いて試す
  6. 対象ホストの情報を取得 (GATHERING FACTS)
  7. Best Plactices に沿った構成を真似る
    1. 各ディレクトリ、ファイルの役割・意味
    2. リスト型変数を使った処理
    3. MySQL 関連
  8. serverspec でテストする
    1. Ruby のインストール
    2. ServerSpec のインストール
    3. ServerSpec を実行してみる
    4. mysqld のテストを作成
    5. WordPress のテストを作成
  9. もっと知る

1. Vagrant を使ってサーバーを準備する

何度でも綺麗な環境でやり直せるように VirtualBox + Vagrant を使ってこの Tutorial 用環境を構築します。

あたりを参考に最新版の VirtualBox と Vagrant をインストールしてください。 Sahara plugin なんかを使うとやり直しも簡単ですが、再構築してもそんなに時間かからないのでお好みで。
$ mkdir ansible-tutorial
$ cd ansible-tutorial
$ vagrant init centos6 http://developer.nrel.gov/downloads/vagrant-boxes/CentOS-6.4-x86_64-v20130427.box
Ansible サーバーと、Ansible に制御される側の2台のサーバーを立てることにするので Vagrantfile を編集します。
$ vi Vagrantfile
config.vm.box = "centos6" の行を次のように書き換えます。2台じゃなくて3台でも4台でもOK
  config.vm.define :node1 do |node|
    node.vm.box = "centos6"
    node.vm.network :forwarded_port, guest: 22, host: 2001, id: "ssh"
    node.vm.network :private_network, ip: "192.168.33.11"
  end

  config.vm.define :node2 do |node|
    node.vm.box = "centos6"
    node.vm.network :forwarded_port, guest: 22, host: 2002, id: "ssh"
    node.vm.network :forwarded_port, guest: 80, host: 8000, id: "http"
    node.vm.network :private_network, ip: "192.168.33.12"
  end
編集が終わったら起動させます。
$ vagrant up
起動後、Ansible で node1 から node2 へ ssh するため、Vagrant 用の秘密鍵をコピーする。
$ vagrant ssh-config node1 > ssh_config
$ vagrant ssh-config node2 >> ssh_config
$ scp -F ssh_config ~/.vagrant.d/insecure_private_key node1:.ssh/id_rsa
vagrant ssh コマンドでログインできます
$ vagrant ssh node1
$ vagrant ssh node2
やり直したくなったら destroy して up すれば元の状態に戻ります
$ vagrant destroy node2
$ vagrant up node2

2. Ansible をインストールする

node1 に Ansible をインストールします。CentOS 6 なので EPEL リポジトリから yum でインストールします。

$ sudo rpm -ivh http://ftp-srv2.kddilabs.jp/Linux/distributions/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm
$ sudo rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-6
$ sudo yum -y install ansible
楽ちんですね。

3. Ansible を使うための準備

vagrant ssh を使うと vagrant ユーザーでログインし、パスワードなしで sudo で root としてコマンドが実行可能なので、Ansible の実行も vagrant ユーザーで行う。

4. Ansible の疎通確認

$ ansible 192.168.33.12 -m ping
192.168.33.12 | success >> {
    "changed": false, 
    "ping": "pong"
}
ansible-doc コマンドでモジュールのドキュメントを読むことができます。
$ ansible-doc ping
> PING

  A trivial test module, this module always returns `pong' on
  successful contact. It does not make sense in playbooks, but it is
  useful from `/usr/bin/ansible'

Example:

ansible webservers -m ping
$ ansible 192.168.33.12 -a 'uname -r'
192.168.33.12 | success | rc=0 >>
2.6.32-358.el6.x86_64

5. 簡単な Playbook を書いて試す

まずはインベントリファイルを作成する。yum でインストールした Ansible の場合、デフォルトのインベントリファイルは /etc/ansible/hosts となっている。これは /etc/ansible/ansible.cfg で定義されている。別のファイルを使う場合は -i オプションで指定する。

$ cat <<_EOD_ > hosts
[test-servers]
192.168.33.12
_EOD_
$ cat <<_EOD_ > simple-playbook.yml
- hosts: test-servers
  user: vagrant
  sudo: yes
  tasks:
    - name: be sure httpd is installed
      action: yum pkg=httpd state=installed

    - name: be sure httpd is running and enabled
      service: name=httpd state=running enabled=yes
_EOD_
この playbook の内容は
hosts: test
対象のホストまたはグループを指定する
user: vagrant
リモートホストへ vagrant ユーザとして ssh でログインする
sudo: yes
リモートホストで sudo を使って処理を実行する
tasks:
実行する処理をリストで定義する
tasks の内容
name:
処理の名称。実行時や task の一覧で表示されます
action:
これは省略可能で yum: とも書ける
service:
サービスの状態を制御するモジュール
playbook の syntax チェックを行なってみます。--syntax-check オプションを使います。
$ ansible-playbook -i hosts simple-playbook.yml --syntax-check
Playbook Syntax is fine
次に task の一覧を確認してみましょう。--list-tasks オプションを使います。(その他のオプション一覧はこちら)。
$ ansible-playbook -i hosts simple-playbook.yml --list-tasks

playbook: simple-playbook.yml

  play #1 (test-servers): task count=5
    be sure httpd is installed
    be sure httpd is running and enabled

次は dry-run です、--check オプションを指定することで変更は行わないが、実際に実行するとこうなるという出力がされます。
$ ansible-playbook -i hosts simple-playbook.yml --check

PLAY [test-servers] *********************************************************** 

GATHERING FACTS *************************************************************** 
ok: [192.168.33.12]

TASK: [be sure httpd is installed] ******************************************** 
changed: [192.168.33.12]

TASK: [be sure httpd is running and enabled] ********************************** 
failed: [192.168.33.12] => {"failed": true}
msg: cannot find 'service' binary or init script for service,  possible typo in service name?, aborting

FATAL: all hosts have already failed -- aborting

PLAY RECAP ******************************************************************** 
           to retry, use: --limit @/var/tmp/ansible/simple-playbook.retry

192.168.33.12              : ok=2    changed=1    unreachable=0    failed=1

あららら?なんか failed って出てますね。まぁ、順番に見てみましょう。一つ目のインストールする task は changed となっています。これは、まだ httpd がインストールされていないので、実際に実行するとインストールされて changed となるということを意味しています。次の自動起動と起動している状態にする task は失敗してしまっています。これはまだ httpd がインストールされていないために、chkconfig httpd などに失敗するからです。

ではいよいよ実行してみましょう。
$ ansible-playbook -i hosts simple-playbook.yml

PLAY [test-servers] *********************************************************** 

GATHERING FACTS *************************************************************** 
ok: [192.168.33.12]

TASK: [be sure httpd is installed] ******************************************** 
changed: [192.168.33.12]

TASK: [be sure httpd is running and enabled] ********************************** 
changed: [192.168.33.12]

PLAY RECAP ******************************************************************** 
192.168.33.12              : ok=3    changed=2    unreachable=0    failed=0
うまくいきましたね。

node2 で確認
$ sudo chkconfig --list httpd
httpd          	0:off	1:off	2:on	3:on	4:on	5:on	6:off
$ sudo service httpd status
httpd (pid  2424) is running...
冪等性があるため再度実行しても対象サーバーの状態は変わらない。 ただし、既に package がインストールされていたり、サービスの設定がされていたりするのでコマンドの出力は変わる。(changed が ok だったり skipping だったりする)

2度目の実行の出力
$ ansible-playbook -i hosts simple-playbook.yml

PLAY [test-servers] *********************************************************** 

GATHERING FACTS *************************************************************** 
ok: [192.168.33.12]

TASK: [be sure httpd is installed] ******************************************** 
ok: [192.168.33.12]

TASK: [be sure httpd is running and enabled] ********************************** 
ok: [192.168.33.12]

PLAY RECAP ******************************************************************** 
192.168.33.12              : ok=3    changed=0    unreachable=0    failed=0

6. 対象ホストの情報を取得する

実行結果に「GATHERING FACTS」と出力されています。このような task は playbook に書いていません。これは何でしょう?結構時間かかってます(非力な virtual server だから?)。
名前のとおりですが、これは対象サーバーから情報を収集する処理です。次のようにして内容を確認すことができます。

$ ansible -m setup -i hosts 192.168.33.12
192.168.33.12 | success >> {
    "ansible_facts": {
        "ansible_all_ipv4_addresses": [
            "10.0.2.15", 
            "192.168.33.12"
        ], 
        "ansible_all_ipv6_addresses": [
            "fe80::a00:27ff:fec9:399e", 
            "fe80::a00:27ff:fed5:dab6"
        ], 
        "ansible_architecture": "x86_64", 
        "ansible_bios_date": "12/01/2006", 
        "ansible_bios_version": "VirtualBox", 
        "ansible_cmdline": {
            "KEYBOARDTYPE": "pc", 
            "KEYTABLE": "us", 
            "LANG": "en_US.UTF-8", 
            "SYSFONT": "latarcyrheb-sun16", 
            "quiet": true, 
            "rd_LVM_LV": "VolGroup/lv_root", 
            "rd_NO_DM": true, 
            "rd_NO_LUKS": true, 
            "rd_NO_MD": true, 
            "rhgb": true, 
            "ro": true, 
            "root": "/dev/mapper/VolGroup-lv_root"
        }, 
        "ansible_date_time": {
            "date": "2013-08-08", 
            "day": "08", 
            "epoch": "1375968652", 
            "hour": "13", 
            "iso8601": "2013-08-08T13:30:52Z", 
            "iso8601_micro": "2013-08-08T13:30:52.873665Z", 
            "minute": "30", 
            "month": "08", 
            "second": "52", 
            "time": "13:30:52", 
            "tz": "UTC", 
            "year": "2013"
        }, 
        "ansible_default_ipv4": {
            "address": "10.0.2.15", 
            "alias": "eth0", 
            "gateway": "10.0.2.2", 
            "interface": "eth0", 
            "macaddress": "08:00:27:c9:39:9e", 
            "mtu": 1500, 
            "netmask": "255.255.255.0", 
            "network": "10.0.2.0", 
            "type": "ether"
        }, 
        "ansible_default_ipv6": {}, 
        "ansible_devices": {
            "sda": {
                "holders": [], 
                "host": "SATA controller: Intel Corporation 82801HM/HEM (ICH8M/ICH8M-E) SATA Controller [AHCI mode] (rev 02)", 
                "model": "VBOX HARDDISK", 
                "partitions": {
                    "sda1": {
                        "sectors": "1024000", 
                        "sectorsize": 512, 
                        "size": "500.00 MB", 
                        "start": "2048"
                    }, 
                    "sda2": {
                        "sectors": "1047549952", 
                        "sectorsize": 512, 
                        "size": "499.51 GB", 
                        "start": "1026048"
                    }
                }, 
                "removable": "0", 
                "rotational": "1", 
                "scheduler_mode": "cfq", 
                "sectors": "1048576000", 
                "sectorsize": "512", 
                "size": "500.00 GB", 
                "support_discard": "0", 
                "vendor": "ATA"
            }, 
            "sr0": {
                "holders": [], 
                "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)", 
                "model": "CD-ROM", 
                "partitions": {}, 
                "removable": "1", 
                "rotational": "1", 
                "scheduler_mode": "cfq", 
                "sectors": "2097151", 
                "sectorsize": "512", 
                "size": "1024.00 MB", 
                "support_discard": "0", 
                "vendor": "VBOX"
            }, 
            "sr1": {
                "holders": [], 
                "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)", 
                "model": "CD-ROM", 
                "partitions": {}, 
                "removable": "1", 
                "rotational": "1", 
                "scheduler_mode": "cfq", 
                "sectors": "2097151", 
                "sectorsize": "512", 
                "size": "1024.00 MB", 
                "support_discard": "0", 
                "vendor": "VBOX"
            }
        }, 
        "ansible_distribution": "CentOS", 
        "ansible_distribution_release": "Final", 
        "ansible_distribution_version": "6.4", 
        "ansible_domain": "localdomain", 
        "ansible_eth0": {
            "active": true, 
            "device": "eth0", 
            "ipv4": {
                "address": "10.0.2.15", 
                "netmask": "255.255.255.0", 
                "network": "10.0.2.0"
            }, 
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fec9:399e", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "08:00:27:c9:39:9e", 
            "module": "e1000", 
            "mtu": 1500, 
            "type": "ether"
        }, 
        "ansible_eth1": {
            "active": true, 
            "device": "eth1", 
            "ipv4": {
                "address": "192.168.33.12", 
                "netmask": "255.255.255.0", 
                "network": "192.168.33.0"
            }, 
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fed5:dab6", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "08:00:27:d5:da:b6", 
            "module": "e1000", 
            "mtu": 1500, 
            "type": "ether"
        }, 
        "ansible_form_factor": "Other", 
        "ansible_fqdn": "localhost.localdomain", 
        "ansible_hostname": "localhost", 
        "ansible_interfaces": [
            "lo", 
            "eth1", 
            "eth0"
        ], 
        "ansible_kernel": "2.6.32-358.el6.x86_64", 
        "ansible_lo": {
            "active": true, 
            "device": "lo", 
            "ipv4": {
                "address": "127.0.0.1", 
                "netmask": "255.0.0.0", 
                "network": "127.0.0.0"
            }, 
            "ipv6": [
                {
                    "address": "::1", 
                    "prefix": "128", 
                    "scope": "host"
                }
            ], 
            "mtu": 16436, 
            "type": "loopback"
        }, 
        "ansible_machine": "x86_64", 
        "ansible_memfree_mb": 323, 
        "ansible_memtotal_mb": 458, 
        "ansible_mounts": [
            {
                "device": "/dev/mapper/VolGroup-lv_root", 
                "fstype": "ext4", 
                "mount": "/", 
                "options": "rw", 
                "size_available": 497491697664, 
                "size_total": 525282689024
            }, 
            {
                "device": "/dev/sda1", 
                "fstype": "ext4", 
                "mount": "/boot", 
                "options": "rw", 
                "size_available": 448802816, 
                "size_total": 507744256
            }, 
            {
                "device": "/vagrant", 
                "fstype": "vboxsf", 
                "mount": "/vagrant", 
                "options": "uid=501,gid=501,rw", 
                "size_available": 72607436800, 
                "size_total": 117461032960
            }
        ], 
        "ansible_os_family": "RedHat", 
        "ansible_pkg_mgr": "yum", 
        "ansible_processor": [
            "Intel(R) Core(TM) i3-3217U CPU @ 1.80GHz"
        ], 
        "ansible_processor_cores": "NA", 
        "ansible_processor_count": 1, 
        "ansible_product_name": "VirtualBox", 
        "ansible_product_serial": "NA", 
        "ansible_product_uuid": "NA", 
        "ansible_product_version": "1.2", 
        "ansible_python_version": "2.6.6", 
        "ansible_selinux": false, 
        "ansible_ssh_host_key_dsa_public": "...", 
        "ansible_ssh_host_key_rsa_public": "...", 
        "ansible_swapfree_mb": 2559, 
        "ansible_swaptotal_mb": 2559, 
        "ansible_system": "Linux", 
        "ansible_system_vendor": "innotek GmbH", 
        "ansible_user_id": "vagrant", 
        "ansible_userspace_architecture": "x86_64", 
        "ansible_userspace_bits": "64", 
        "ansible_virtualization_role": "guest", 
        "ansible_virtualization_type": "virtualbox", 
        "facter_architecture": "x86_64", 
        "facter_augeasversion": "0.9.0", 
        "facter_blockdevice_sda_model": "VBOX HARDDISK", 
        "facter_blockdevice_sda_size": 536870912000, 
        "facter_blockdevice_sda_vendor": "ATA", 
        "facter_blockdevice_sr0_model": "CD-ROM", 
        "facter_blockdevice_sr0_size": 1073741312, 
        "facter_blockdevice_sr0_vendor": "VBOX", 
        "facter_blockdevice_sr1_model": "CD-ROM", 
        "facter_blockdevice_sr1_size": 1073741312, 
        "facter_blockdevice_sr1_vendor": "VBOX", 
        "facter_blockdevices": "sda,sr0,sr1", 
        "facter_facterversion": "1.7.0", 
        "facter_filesystems": "ext4,iso9660", 
        "facter_hardwareisa": "x86_64", 
        "facter_hardwaremodel": "x86_64", 
        "facter_hostname": "localhost", 
        "facter_id": "vagrant", 
        "facter_interfaces": "eth0,eth1,lo", 
        "facter_ipaddress": "10.0.2.15", 
        "facter_ipaddress_eth0": "10.0.2.15", 
        "facter_ipaddress_eth1": "192.168.33.12", 
        "facter_ipaddress_lo": "127.0.0.1", 
        "facter_is_virtual": "true", 
        "facter_kernel": "Linux", 
        "facter_kernelmajversion": "2.6", 
        "facter_kernelrelease": "2.6.32-358.el6.x86_64", 
        "facter_kernelversion": "2.6.32", 
        "facter_macaddress": "08:00:27:C9:39:9E", 
        "facter_macaddress_eth0": "08:00:27:C9:39:9E", 
        "facter_macaddress_eth1": "08:00:27:D5:DA:B6", 
        "facter_memoryfree": "366.08 MB", 
        "facter_memoryfree_mb": "366.08", 
        "facter_memorysize": "458.64 MB", 
        "facter_memorysize_mb": "458.64", 
        "facter_memorytotal": "458.64 MB", 
        "facter_mtu_eth0": "1500", 
        "facter_mtu_eth1": "1500", 
        "facter_mtu_lo": "16436", 
        "facter_netmask": "255.255.255.0", 
        "facter_netmask_eth0": "255.255.255.0", 
        "facter_netmask_eth1": "255.255.255.0", 
        "facter_netmask_lo": "255.0.0.0", 
        "facter_network_eth0": "10.0.2.0", 
        "facter_network_eth1": "192.168.33.0", 
        "facter_network_lo": "127.0.0.0", 
        "facter_operatingsystem": "CentOS", 
        "facter_operatingsystemmajrelease": "6", 
        "facter_operatingsystemrelease": "6.4", 
        "facter_osfamily": "RedHat", 
        "facter_path": "/usr/local/bin:/bin:/usr/bin", 
        "facter_physicalprocessorcount": 1, 
        "facter_processor0": "Intel(R) Core(TM) i3-3217U CPU @ 1.80GHz", 
        "facter_processorcount": "1", 
        "facter_ps": "ps -ef", 
        "facter_puppetversion": "3.1.1", 
        "facter_rubysitedir": "/usr/lib/ruby/site_ruby/1.8", 
        "facter_rubyversion": "1.8.7", 
        "facter_selinux": "false", 
        "facter_sshdsakey": "...", 
        "facter_sshfp_dsa": "SSHFP 2 1 ...\nSSHFP 2 2 ...", 
        "facter_sshfp_rsa": "SSHFP 1 1 ...\nSSHFP 1 2 ...", 
        "facter_sshrsakey": "...", 
        "facter_swapfree": "2.50 GB", 
        "facter_swapfree_mb": "2559.99", 
        "facter_swapsize": "2.50 GB", 
        "facter_swapsize_mb": "2559.99", 
        "facter_timezone": "UTC", 
        "facter_uniqueid": "007f0100", 
        "facter_uptime": "1:09 hours", 
        "facter_uptime_days": 0, 
        "facter_uptime_hours": 1, 
        "facter_uptime_seconds": 4195, 
        "facter_virtual": "virtualbox"
    }, 
    "changed": false
}
このようにして収集したデータを task のオプションやテンプレートの変数に使うことができます。
  • IP Address は {{ansible_eth0.ipv4.address}} のようにして使えます
    tasks:
      - name: gathering data task example
        command: echo {{ansible_eth0.ipv4.address}}
    
  • {{ansible_processor_count * 5}} と計算結果を使うこともできます。CPUのコア数を元にプロセス数を設定したりする場合に使えます。
  • 次のようにして task 実行の条件判定にも使えます
    tasks:
      - name: "shutdown Debian flavored systems"
        command: /sbin/shutdown -t now
        when: ansible_os_family == "Debian"
    
そして、これらのデータが不要な場合は gather_facts: no とすることで収集しないことで playbook の実行時間を短縮できます。
- hosts: all
  sudo: yes
  gather_facts: no
  roles:
    - common

7. Best Plactices に沿った構成を真似る

Best Practices のディレクトリ構成にならって WordPress サーバーを構築する Playbook を作成します。 ここは長くなりそうだから詳細は別ページにしよう。

こんなディレクトリ構成で進めます。playbook branch のファイルで一応動作すると思います。随時改善していきます。 このファイルは https://github.com/yteraoka/ansible-tutorial/archive/playbook.zip からダウンロードできます。

$ tree
.
├── common.yml
├── group_vars
├── host_vars
├── roles
│   ├── common
│   │   ├── files
│   │   ├── handlers
│   │   │   └── main.yml
│   │   ├── tasks
│   │   │   ├── common-packages.yml
│   │   │   ├── epel.yml
│   │   │   ├── main.yml
│   │   │   ├── ntp.yml
│   │   │   └── sshd.yml
│   │   ├── templates
│   │   └── vars
│   └── wordpress
│       ├── files
│       │   └── wordpress-3.5.2-ja.zip
│       ├── handlers
│       │   └── main.yml
│       ├── tasks
│       │   ├── httpd.yml
│       │   ├── main.yml
│       │   ├── mysql.yml
│       │   ├── php.yml
│       │   └── wordpress.yml
│       ├── templates
│       │   ├── httpd.conf.j2
│       │   └── wp-config.php.j2
│       └── vars
│           └── main.yml
├── site.yml
├── test-servers
└── wordpress.yml

15 directories, 20 files
タスクの一覧を確認するには次のように --list-tasks オプションをつけて ansible-playboook コマンドを実行します。
$ ansible-playbook --list-tasks site.yml

playbook: site.yml

  play #1 (all): task count=7
    be sure epel repository is installed
    be sure common packages are installed
    configure sshd_config
    be sure ntpd is running and enabled

Please enter MySQL wordpress user password: 
  play #2 (wordpress): task count=19
    be sure httpd is installed
    create document root
    be sure httpd is configured
    remove httpd welcome.conf
    be sure httpd is running and enabled
    be sure mysql-server is installed
    be sure mysqldb is installed
    be sure mysqld is running and enabled
    Create database
    Create database user
    be sure php is installed
    be sure php-mysql is installed
    disable password authentication
    copy wordpress zip file
    unzip wordpress zip file
    configure wp-config.php

(随時更新しているため↑この内容は現在の repository のものとはずれてる場合があります)
実際の実行は次のコマンドになります。
$ ansible-playbook -i test-servers site.yml

7.1. 各ディレクトリ、ファイルの役割・意味

数種類のサービスが Web - App - DB 構成(それぞれは Role)で構築されており、それぞれに2ヶ所のロケーションにあるとする(Multi-AZみたいな)

group_vars/
グループ毎の変数を定義するのに使います、role ではありません、role 毎の変数は roles/{role_name}/vars/ を使います。例えばロケーション毎に異なる変数など。ファイル名はインベントリファイルで設定するグループ名です。インベントリファイルにグループ変数を書くことも可能です。
host_vars/
ホスト毎の変数を定義するのに使います。group_vars と同じくファイル名はインベントリファイルに設定したホスト名です。インベントリファイルの中でホスト名の右に並べて書くこともできます。
roles/
このディレクトリの下に role (役割) 毎のディレクトリを作成し、それぞれの role にさらに files/, handlers/, tasks/, templates/, vars/ というサブディレクトリを作成します。
roles/*/files/
当該 role の task で使うファイル関連モジュール (file, copy) が src として使うファイルの置き場所。任意のファイル名で置きます。
roles/*/handlers/
設定変更後にサービスの再起動をさせたりする場合に、notify という定義で処理を呼び出しますが、その呼び出されるハンドラをここで定義します。main.yml というファイルで作成しますが、include という定義で複数ファイルをそこから読み込ませることが可能です。
roles/*/tasks/
何かをインストールしたり、ユーザーを作成したりする task 定義のファイルをここに置きます。handlers と同じように mail.yml というファイルが起点となります。
roles/*/templates/
template モジュールで利用するテンプレートファイルを置きます。このモジュールでは Jinja2 (神社) というテンプレートエンジンが使われていて .j2 とうい拡張子を使います。
roles/*/vars/
role 毎に設定する変数を定義するファイルを置きます。handlers や tasks と同じく main.yml ファイルが起点となります。
sites.yml
ansible-playbook コマンドに渡す大元 (root) の playbook ファイルです。
test-servers
今回のインベントリファイルです。実際の運用では development, stage, production などというそれぞれの環境毎のファイルにするのが Best Practice っぽいです。
wordpress.yml
role 毎の対象グループ、対象ホストなどを定義します。今回は wordpress サーバーをセットアップする wordpress role とするため、このファイル名としてあります。site.yml で include されています。

7.2. リスト変数を使った処理

roles/common/tasks/common-packages.yml を見てみましょう。

- name: be sure common packages are installed
  yum: name={{item}} state=installed
  with_items:
    - ntp
    - bind-utils
    - unzip
  tags: common-packages
with_items にインストールされているべきパッケージ名を並べ、それぞれを yum モジュールで処理します。 tags を指定しておくと ansible-playbook の --tags オプションを使うことで指定の tag のついた task だけを実行可能。複数の task に同じ tag を付けられます。

roles/common/tasks/sshd.yml でも sshd_config の複数行の編集をまとめるために with_items を使っています。
- name: configure sshd_config
  lineinfile: dest=/etc/ssh/sshd_config owner=root group=root mode=0600 backup=yes {{item}}
  with_items:
    - regexp='^PasswordAuthentication' line='PasswordAuthentication no' insertafter='PasswordAuthentication no'
    - regexp='^PermitRootLogin' line='PermitRootLogin no' insertafter='#PermitRootLogin'
    - regexp='^GSSAPIAuthentication' line='GSSAPIAuthentication no' insertafter='#GSSAPIAuthentication'
  notify: restart sshd
  tags: sshd
それぞれ別の task にしてしまうと notify によて3度も sshd の restart が必要になってしまう...と思ったら、別々にしても notify は一度しか処理されませんでした。よって、こういう場合はわかりやすい書き方を選ぶのが良いでしょう。

lineinfile モジュールは設定ファイルの置換に便利です。insertafterbackref オプションは要チェックです。

7.3. MySQL 関連

パッケージのインストールだけでなく、DBの作成、ユーザーの作成も行えます。

# yum で mysql-server パッケージをインストール
- name: be sure mysql-server is installed
  yum: pkg=mysql-server state=installed
  tags: mysqld

# mysql_user で必要な python モジュールをインストールする
- name: be sure mysqldb is installed
  yum: pkg=MySQL-python state=installed
  tags: mysqld

# mysqld の起動、自動起動設定
- name: be sure mysqld is running and enabled
  service: name=mysqld state=running enabled=yes
  tags: mysqld

# wordpress 用 DB の作成
- name: Create database
  action: mysql_db db={{dbname}} state=present
  tags: mysqld

# wordpress 用 DB ユーザーの作成
- name: Create database user
  action: mysql_user name=${dbuser} password=${dbpassword} priv=${dbname}.*:ALL state=present
  tags: mysqld
dbname, dbuser 変数は roles/wordpress/vars/main.yml に書いてあり、dbpasswordwordpress.yml にて次のように vas_prompt 設定し、ansible-playbook 実行時に入力させている。
- hosts: wordpress
  sudo: yes
  roles:
    - wordpress
  vars_prompt:
    - name: "dbpassword"
      prompt: "Please enter MySQL wordpress user password"
      default: "wordpress"

8. serverspec でテストする

8.1. Ruby のインストール

CentOS 6 はいまだに 1.8.7 だから最新のバージョンを /opt/ruby-2.0.0 にインストール

まずは Ruby のコンパイルに必要なものをインストール

$ sudo yum -y install git gcc gcc-c++ make patch readline-devel zlib-devel libyaml-devel
ruby-build を git clone する
$ git clone -q git://github.com/sstephenson/ruby-build.git
2.0.0-p247 をインストール
$ cd ruby-build
$ sudo bin/ruby-build 2.0.0-p247 /opt/ruby-2.0.0
Tea Time...
Ruby や Perl のコンパイルには結構時間がかかりますねぇ。

8.2. ServerSpec のインストール

$ sudo /opt/ruby-2.0.0/bin/gem install serverspec
あ、bundler 使ったほうが良かったかな?

8.3. ServerSpec を実行してみる

まずは serverspec-init コマンドで土台を作ります。
$ /opt/ruby-2.0.0/bin/serverspec-init
Select a backend type:

  1) SSH
  2) Exec (local)

Select number: 1

Vagrant instance y/n: n
Input target host name: 192.168.33.12
 + spec/
 + spec/192.168.33.12/
 + spec/192.168.33.12/httpd_spec.rb
 + spec/spec_helper.rb
 + Rakefile
Vagrant 使ってるけど、Vagrant の Guest 同士での SSH だから Vagrant じゃないよと。
今回の設定では httpd.conf の ServerName は localhost となっているのでテストをちょっといじる
$ sed -i 's/192.168.33.12/localhost/' spec/192.168.33.12/httpd_spec.rb
それでは rake コマンドでテストを実行してみましょう
$ /opt/ruby-2.0.0/bin/rake spec
/opt/ruby-2.0.0/bin/ruby -S rspec spec/192.168.33.12/httpd_spec.rb
/opt/ruby-2.0.0/bin/ruby: No such file or directory -- rspec (LoadError)
rake aborted!
/opt/ruby-2.0.0/bin/ruby -S rspec spec/192.168.33.12/httpd_spec.rb failed

Tasks: TOP => spec
(See full trace by running task with --trace)
あらら、失敗。PATH の問題っぽい。
$ PATH=/opt/ruby-2.0.0/bin:$PATH
$ /opt/ruby-2.0.0/bin/rake spec
/opt/ruby-2.0.0/bin/ruby -S rspec spec/192.168.33.12/httpd_spec.rb
......

Finished in 0.62573 seconds
6 examples, 0 failures
成功しました。
spec/192.168.33.12/httpd_spec.rb に書いてあるテストが実行されました。
内容は次の通り
  • httpd パッケージがインストールされていること
  • httpd サービスが有効になっていること (chkconfig httpd on 状態)
  • httpd が起動していること
  • ポート 80 が LISTEN 状態となっていること
  • /etc/httpd/conf/httpd.conf ファイルが存在すること
  • httpd.conf に "ServerName localhost" が含まれること

8.4. mysqld のテストを作成

httpd_spec.rb を参考に mysqld のテストを作成しましょう。
これは JTF のパクリですね とまったく同じ課題ですね。

  • mysql-server パッケージがインストールされていること
  • mysqld デーモンが有効化されていること (chkconfig mysqld on されていること)
  • mysqld デーモンが起動していること
  • 3306 ポートが LISTEN していること
$ vi spec/192.168.33.12/mysqld_spec.rb
(例はこちら: https://gist.github.com/yteraoka/6156753)

8.5. WordPress のテストを作成

Chef 版との違いが出ましたよ。WordPress が日本語版だったので出力されるHTMLが日本語です。 http://localhost/wp-admin/install.php にアクセスすると出力されるメッセージ(HTML)に「5分でできる WordPress の有名なインストールプロセスへようこそ」が含まれることをチェックしましょう。

あぁぁ、ServerSpec の command 出力を日本語でマッチさせる方法がわからん...
誰か教えてくださいまし。
ただ、日本語文字列のコピペをミスっていただけでした...

$ vi spec/192.168.33.12/wordpress_spec.rb
(例はこちら: https://gist.github.com/yteraoka/6186278)

9. もっと知る

ansible-in-detail.html にモジュールの説明や細かいことを書いています。