Ansible Tutorial

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

GitHub Repository Ansible Tutorial Wiki

History
Ansible とは

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

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

あたりを参考に最新版の VirtualBox と Vagrant をインストールしてください。

$ mkdir ansible-tutorial
$ cd ansible-tutorial
$ vagrant init centos6 http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.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
$ scp -F ssh_config .vagrant/machines/node2/virtualbox/private_key node1:.ssh/id_rsa

vagrant ssh コマンドでログインできます

$ vagrant ssh node1
$ vagrant ssh node2

やり直したくなったら destroy して up すれば元の状態に戻ります

$ vagrant destroy node2
$ vagrant up node2

EPEL から yum でもインストール可能ですが、ここは pip で最新版をインストールします。CentOS 6 の Python は 2.6 なのでついでに 2.7 の最新を入れます (@tagomoris さんの xbuild を使わせてもらいます)

$ sudo yum install bzip2-devel sqlite-devel git patch gcc openssl-devel
$ cd /var/tmp
$ git clone https://github.com/tagomoris/xbuild.git
$ sudo xbuild/python-install 2.7.10 /opt/python-2.7
$ sudo /opt/python-2.7/bin/pip install ansible
$ echo 'PATH=/opt/python-2.7/bin:$PATH' >> ~/.bashrc

node1 から ping モジュールで疎通確認してみます

$ vagrant ssh node1

$ ansible 192.168.33.12 -m ping
ERROR: Unable to find an inventory file, specify one with -i ?

おっとエラーです。-i で inventory file を指定せよと言われています。Ansible はインベントリファイルに書かれたホストにしかアクセスしません。デフォルトのインベントリファイルが /etc/ansible/hosts です(ansible.cfgで定義されています)が、コマンドラインオプションの -i で指定できます。

ちなみに ansible.cfg は次の順で探します

  1. カレントディレクトリ
  2. 環境変数の ANSIBLE_CONFIG or ~/ansible.cfg
  3. /etc/ansible/ansible.cfg

ここではとりあえずカレントディレクトリに hosts ファイルを作成しましょう。

$ echo 192.168.33.12 > hosts

今度は -i でインベントリファイルを指定して ping モジュールを実行してみましょう

$ ansible -i hosts 192.168.33.12 -m ping

paramiko: The authenticity of host '192.168.33.12' can't be established. 
The ssh-rsa key fingerprint is 80c661a0ec2d1f68d5352986ad417f2c. 
Are you sure you want to continue connecting (yes/no)?
yes
192.168.33.12 | success >> {
    "changed": false, 
    "ping": "pong"
}

ok ですね (たしか、バージョン 1.3 から host key をチェックするようになりました)

次はリモートホストで任意のコマンドを実行してみましょう

$ ansible -i hosts 192.168.33.12 -a 'uname -r'
192.168.33.12 | success | rc=0 >>
2.6.32-431.el6.x86_64

ついでに yum モジュールでパッケージのインストールも試してみましょう

$ ansible -i hosts 192.168.33.12 -m yum -s -a name=telnet
192.168.33.12 | success >> {
    "changed": true, 
    "msg": "warning: rpmts_HdrFromFdno: Header V3 RSA/SHA1 Signature, key ID c105b9de: NOKEY\nImporting GPG key 0xC105B9DE:\n Userid : CentOS-6 Key (CentOS 6 Official Signing Key) <centos-6-key@centos.org>\n Package: centos-release-6-5.el6.centos.11.1.x86_64 (@anaconda-CentOS-201311272149.x86_64/6.5)\n From   : /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6\n", 
    "rc": 0, 
    "results": [
        "Loaded plugins: fastestmirror, security\nLoading mirror speeds from cached hostfile\n * base: www.ftp.ne.jp\n * extras: www.ftp.ne.jp\n * updates: www.ftp.ne.jp\nSetting up Install Process\nResolving Dependencies\n--< Running transaction check\n---< Package telnet.x86_64 1:0.17-47.el6_3.1 will be installed\n--< Finished Dependency Resolution\n\nDependencies Resolved\n\n================================================================================\n Package         Arch            Version                    Repository     Size\n================================================================================\nInstalling:\n telnet          x86_64          1:0.17-47.el6_3.1          base           58 k\n\nTransaction Summary\n================================================================================\nInstall       1 Package(s)\n\nTotal download size: 58 k\nInstalled size: 109 k\nDownloading Packages:\nRetrieving key from file:///etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-6\nRunning rpm_check_debug\nRunning Transaction Test\nTransaction Test Succeeded\nRunning Transaction\n\r  Installing : 1:telnet-0.17-47.el6_3.1.x86_64                              1/1 \n\r  Verifying  : 1:telnet-0.17-47.el6_3.1.x86_64                              1/1 \n\nInstalled:\n  telnet.x86_64 1:0.17-47.el6_3.1                                               \n\nComplete!\n"
    ]
}

モジュールのドキュメントが ansible-doc コマンドで確認できます。これ地味に便利です。バージョン 1.4 から PAGER を使って表示されるようになりました

$ ansible-doc yum

それではいよいよ Playbook を書いてみましょう

今度はインベントリファイルでグループを定義しておきます

$ cat <<_EOD_ > hosts
[test-servers]
192.168.33.12
_EOD_

次に playbook ファイル (YAML) を作成します

$ cat <<_EOD_ > simple-playbook.yml
---
- hosts: test-servers
  become: yes
  tasks:
    - name: be sure httpd is installed
      yum: name=httpd state=installed

    - name: be sure httpd is running and enabled
      service: name=httpd state=started enabled=yes
_EOD_

この playbook の内容は次の通り

hosts: test-servers
対象のホストまたはグループを指定する (glob [*] も使えます)
カンマ区切りでも、YAML のリスト指定でも大丈夫です
become: yes
リモートホストで sudo (default) を使って実行する
デフォルトでは root としての実行ですが、別途 become_user を指定することで別のユーザーとして実行することも可能です
tasks:
実行する処理を定義します
name は必須ではありません、yum, service がモジュール名で、それに続くのが各モジュールのオプションです

playbook の syntax check を行なってみます。--syntax-check オプションを使います

$ ansible-playbook -i hosts simple-playbook.yml --syntax-check

playbook: simple-playbook.yml

次に task の一覧を確認してみましょう。--list-tasks オプションを使います (その他のオプション一覧はこちら)

$ ansible-playbook -i hosts simple-playbook.yml --list-tasks

playbook: simple-playbook.yml

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

エラーがなければこのような出力です。エラーがあると赤字で ERROR ... と表示されます

次は dry-run です、--check オプションを指定することで変更は行わないが、実際に実行するとこうなるという出力がされます
(1.2 の時はエラーになってたけど改善されてますね)

$ 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] ********************************** 
ok: [192.168.33.12]

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

ではいよいよ実行してみましょう

$ 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  2896) 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

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

$ 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:fe4d:b37d", 
            "fe80::a00:27ff:fed5:b2ee"
        ], 
        "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-12-16", 
            "day": "16", 
            "epoch": "1387201344", 
            "hour": "13", 
            "iso8601": "2013-12-16T13:42:24Z", 
            "iso8601_micro": "2013-12-16T13:42:24.143102Z", 
            "minute": "42", 
            "month": "12", 
            "second": "24", 
            "time": "13:42:24", 
            "tz": "UTC", 
            "tz_offset": "+0000", 
            "year": "2013"
        }, 
        "ansible_default_ipv4": {
            "address": "10.0.2.15", 
            "alias": "eth0", 
            "gateway": "10.0.2.2", 
            "interface": "eth0", 
            "macaddress": "08:00:27:4d:b3:7d", 
            "mtu": 1500, 
            "netmask": "255.255.255.0", 
            "network": "10.0.2.0", 
            "type": "ether"
        }, 
        "ansible_default_ipv6": {}, 
        "ansible_devices": {
            "sda": {
                "holders": [], 
                "host": "IDE interface: Intel Corporation 82371AB/EB/MB PIIX4 IDE (rev 01)", 
                "model": "VBOX HARDDISK", 
                "partitions": {
                    "sda1": {
                        "sectors": "1024000", 
                        "sectorsize": 512, 
                        "size": "500.00 MB", 
                        "start": "2048"
                    }, 
                    "sda2": {
                        "sectors": "82860032", 
                        "sectorsize": 512, 
                        "size": "39.51 GB", 
                        "start": "1026048"
                    }
                }, 
                "removable": "0", 
                "rotational": "1", 
                "scheduler_mode": "cfq", 
                "sectors": "83886080", 
                "sectorsize": "512", 
                "size": "40.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"
            }
        }, 
        "ansible_distribution": "CentOS", 
        "ansible_distribution_release": "Final", 
        "ansible_distribution_version": "6.5", 
        "ansible_domain": "localdomain", 
        "ansible_env": {
            "CVS_RSH": "ssh", 
            "G_BROKEN_FILENAMES": "1", 
            "HOME": "/home/vagrant", 
            "LANG": "C", 
            "LESSOPEN": "|/usr/bin/lesspipe.sh %s", 
            "LOGNAME": "vagrant", 
            "MAIL": "/var/mail/vagrant", 
            "PATH": "/usr/local/bin:/bin:/usr/bin", 
            "PWD": "/home/vagrant", 
            "SELINUX_LEVEL_REQUESTED": "", 
            "SELINUX_ROLE_REQUESTED": "", 
            "SELINUX_USE_CURRENT_RANGE": "", 
            "SHELL": "/bin/bash", 
            "SHLVL": "2", 
            "SSH_CLIENT": "192.168.33.11 58880 22", 
            "SSH_CONNECTION": "192.168.33.11 58880 192.168.33.12 22", 
            "USER": "vagrant", 
            "_": "/usr/bin/python"
        }, 
        "ansible_eth0": {
            "active": true, 
            "device": "eth0", 
            "ipv4": {
                "address": "10.0.2.15", 
                "netmask": "255.255.255.0", 
                "network": "10.0.2.0"
            }, 
            "ipv4_secondaries": [], 
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fe4d:b37d", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "08:00:27:4d:b3:7d", 
            "module": "e1000", 
            "mtu": 1500, 
            "promisc": false, 
            "type": "ether"
        }, 
        "ansible_eth1": {
            "active": true, 
            "device": "eth1", 
            "ipv4": {
                "address": "192.168.33.12", 
                "netmask": "255.255.255.0", 
                "network": "192.168.33.0"
            }, 
            "ipv4_secondaries": [], 
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fed5:b2ee", 
                    "prefix": "64", 
                    "scope": "link"
                }
            ], 
            "macaddress": "08:00:27:d5:b2:ee", 
            "module": "e1000", 
            "mtu": 1500, 
            "promisc": false, 
            "type": "ether"
        }, 
        "ansible_form_factor": "Other", 
        "ansible_fqdn": "localhost.localdomain", 
        "ansible_hostname": "localhost", 
        "ansible_interfaces": [
            "lo", 
            "eth1", 
            "eth0"
        ], 
        "ansible_kernel": "2.6.32-431.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"
            }, 
            "ipv4_secondaries": [], 
            "ipv6": [
                {
                    "address": "::1", 
                    "prefix": "128", 
                    "scope": "host"
                }
            ], 
            "mtu": 16436, 
            "promisc": false, 
            "type": "loopback"
        }, 
        "ansible_machine": "x86_64", 
        "ansible_memfree_mb": 169, 
        "ansible_memtotal_mb": 458, 
        "ansible_mounts": [
            {
                "device": "/dev/mapper/VolGroup-lv_root", 
                "fstype": "ext4", 
                "mount": "/", 
                "options": "rw", 
                "size_available": 37423112192, 
                "size_total": 40797364224
            }, 
            {
                "device": "/dev/sda1", 
                "fstype": "ext4", 
                "mount": "/boot", 
                "options": "rw", 
                "size_available": 446151680, 
                "size_total": 507744256
            }, 
            {
                "device": "/vagrant", 
                "fstype": "vboxsf", 
                "mount": "/vagrant", 
                "options": "uid=900,gid=999,rw", 
                "size_available": 22555742208, 
                "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": 1, 
        "ansible_processor_count": 1, 
        "ansible_processor_threads_per_core": 1, 
        "ansible_processor_vcpus": 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": "AAAAB3NzaC1kc3MAAACBAOQ2z5AKK04+mx8RJoSB8axADs5md7igiVmva7EhmibLkB35vpKVICI78y6l9jt6yq16+PFsEfOCEVXifXblfz122vu+pIeccEan52q5vn+W4xfu7svNDoKKg6VXgAezMCHWk15u9rQ2S5PY49VSut/SaVa2bYarNOpjY88hQv/NAAAAFQDjFghslSnBJfqJiRDgkVW7gR9P/QAAAIBXo7OpZh7kBgqHIbHFY2gSbtr6UUa/n5BmHBAbuJQpcv6EgeW0LJkE/6JjFgqJLMEjgd6JKdotz6p8397S7xXzwJBn/EONWR4g9NSwIgjcK4/6UALpkxcz73lSXhYXvGIMC+GYlZOTNm1ieuy1Oi/K0S4DBJhLNtlXE4Pm32gDCQAAAIB7C97Tt5DPEDl5IekSuZDfV9D33uj5BCZivINM40JxdVzG79RQyUAstiVvRWmvai02C0ff9T2VLbihHidHeaA/cTmAIXOlEda/vE/qrmmCVoalUOmEyOLDR6UXE/OKriKLtlpHzos5RJrfPc9xpme4DBdwuiXkwMyi/lY6164jfA==", 
        "ansible_ssh_host_key_rsa_public": "AAAAB3NzaC1yc2EAAAABIwAAAQEA1MfoV6b96gaG82/Iee8abtYEpyGEuaMKovWBoIKyEiDH4A/iiICRHtyhc/IYFQixM6GQI/j1kuVC4moDUCVMXqqwI1aJcKroNexVsRnoKToLXW/NGZ90bpd7m+YVd+LomaXUAFWq64j3Eo3+hXkf4k8iSQrz06jCLcfllDuf8JuRto1wIi7GJCqxCjgEEelmhCSvwrLbuFM2FD4i34tWj7+PraPNm/kkCqooev0mKPArXSZ65WCsIuZ2VV4F0FQnU6iOqYwphEsiWPvaQZfEwzzD3Tr4RkWDPu7i4VQN++KmzsPIjYp514KMydJy7/3Msqee3xtexkv/l0lRyihpRQ==", 
        "ansible_swapfree_mb": 927, 
        "ansible_swaptotal_mb": 927, 
        "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"
    }, 
    "changed": false
}

このようにして収集したデータを task のオプションやテンプレートの変数に使うことができます

そして、これらのデータが不要な場合は gather_facts: no とすることで収集しないことで playbook の実行時間を短縮できます

- hosts: all
  become: yes
  gather_facts: no
  roles:
    - common

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

こんなディレクトリ構成で進めます。playbook branch のファイルで一応動作すると思います。随時改善していきます。 このファイルは GitHub https://github.com/yteraoka/ansible-tutorial.git にあります

$ git clone https://github.com/yteraoka/ansible-tutorial.git
$ cd ansible-tutorial
$ git checkout playbook
$ tree -F
.
|-- common.yml
|-- group_vars/
|-- host_vars/
|-- roles/
|   |-- common/
|   |   |-- defaults/
|   |   |-- files/
|   |   |-- handlers/
|   |   |   `-- main.yml
|   |   |-- meta/
|   |   |-- tasks/
|   |   |   |-- common-packages.yml
|   |   |   |-- epel.yml
|   |   |   |-- main.yml
|   |   |   |-- ntp.yml
|   |   |   `-- sshd.yml
|   |   |-- templates/
|   |   `-- vars/
|   `-- wordpress/
|       |-- defaults/
|       |-- files/
|       |-- handlers/
|       |   `-- main.yml
|       |-- meta/
|       |-- 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

19 directories, 19 files

タスクの一覧を確認するには次のように --list-tasks オプションをつけて ansible-playboook コマンドを実行します

$ ansible-playbook --list-tasks -i test-servers site.yml

playbook: site.yml

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

Please enter MySQL wordpress user password [wordpress]: 
  play #2 (wordpress):
    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 mysqld is running and enabled
    Create database
    Create database user
    be sure php is installed
    set timezone in php.ini
    download wordpress package
    unzip wordpress zip file
    generate secret keys
    read secret keys
    configure wp-config.php

MySQL ユーザーのパスワードは playbook に書かず、実行時に入力するようにしてあるため、Please enter MySQL wordpress user password: と、プロンプトが表示されます。ブランクで Enter を押すと playbook に書いてあるデフォルトのパスワードが使われます

実際の実行は次のコマンドですが

$ ansible-playbook -i test-servers site.yml

実行前に各ディレクトリ、ファイルの役割を確認しましょう

数種類のサービスが 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/, defaults/, meta/ というサブディレクトリを作成します (その role で使うもののみ)
roles/*/files/
当該 role の task で使うファイル関連モジュール (copy) が src として使うファイルの置き場所。任意のファイル名で置きます
roles/*/handlers/
設定変更後にサービスの再起動をさせたりする場合に、notify という定義で処理を呼び出しますが、その呼び出されるハンドラをここで定義します。main.yml というファイルで作成しますが、include という定義で複数ファイルをそこから読み込ませることが可能です
roles/*/tasks/
何かをインストールしたり、ユーザーを作成したりする task 定義のファイルをここに置きます。handlers と同じように main.yml というファイルが起点となります
roles/*/templates/
template モジュールで利用するテンプレートファイルを置きます。このモジュールでは Jinja2 (神社) というテンプレートエンジンが使われていて .j2 という拡張子を使います
roles/*/vars/
role 毎に設定する変数を定義するファイルを置きます。handlers や tasks と同じく main.yml ファイルが起点となります
roles/*/defaults/
当該 role で使う変数の default 値を設定します。他で設定されていなければここで設定した値が使われます。main.yml に書きます。この変数が一番低い優先度です 「Ansible の変数の優先順
roles/*/meta/
role の依存設定を書きます。A という role が B という role に依存しているなら roles/A/meta/main.yml に次のように書きます
---
dependencies:
  - role: B
    foo: "bar"
site.yml
ansible-playbook コマンドに渡す大元 (root) の playbook ファイルです
test-servers
今回のインベントリファイルです。実際の運用では development, stage, production などというそれぞれの環境毎のファイルにするのが Best Practice っぽいです
wordpress.yml
role 毎の対象グループ、対象ホストなどを定義します。今回は wordpress サーバーをセットアップする wordpress role とするため、このファイル名としてあります。site.yml で include されています

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 / -t オプションを使うことで指定の 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
    regexp="{{ item.regexp }}"
    line="{{ item.line }}"
    insertafter="{{ item.insertafter }}"
  with_items:
    - regexp: '^PasswordAuthentication'
      line: 'PasswordAuthentication no'
      insertafter: '#PasswordAuthentication'
    - 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 は一度しか処理されませんでした。notify はその role の最後に実行されるようです。よって、こういう場合はわかりやすい書き方を選ぶのが良いでしょう。ただし、task が増えるとその分 SSH の回数が増えます。

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

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

---
# yum で mysql-server と mysql_* モジュールで必要な MySQL-python パッケージをインストール
- name: be sure mysql-server is installed
  yum: name={{ item }} state=installed
  with_items:
    - mysql-server
    - MySQL-python
  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
  mysql_db: db={{ dbname }} state=present encoding=utf8
  tags: mysqld

# wordpress 用 DB ユーザーの作成
- name: Create database user
  mysql_user: >
    name={{ dbuser }}
    password="{{ dbpassword }}"
    priv={{ dbname }}.*:ALL
    state=present
  tags: mysqld

dbname, dbuser 変数は roles/wordpress/vars/main.yml に書いてあり、dbpasswordwordpress.yml にて次のように vars_prompt 設定し、ansible-playbook 実行時に入力させています

- hosts: wordpress
  become: yes
  roles:
    - wordpress
  vars_prompt:
    - name: "dbpassword"
      prompt: "Please enter MySQL wordpress user password"
      default: "wordpress"

それでは playbook を実際に実行してみましょう

$ ansible-playbook -i test-servers site.yml

うまくいったでしょうか?

今実行した playbook がうまく動作したことを確認するために ServerSpec を利用してチェックするようにしてみましょう

ServerSpec は Ruby 製ですので、まずは Ruby のインストールですが、 CentOS 6 はいまだに 1.8.7 と古いので最新のバージョンを ruby-build を使って /opt/ruby-2.0.0 にインストールします

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

$ sudo rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
$ sudo yum -y install git gcc gcc-c++ make patch readline-devel zlib-devel libyaml-devel openssl-devel

ruby-build を git clone する

$ git clone https://github.com/sstephenson/ruby-build.git

最新の Ruby (2.0.0-p353) をインストール

$ cd ruby-build
$ sudo bin/ruby-build 2.0.0-p353 /opt/ruby-2.0.0

Ruby や Perl のコンパイルには結構時間がかかりますねぇ。お茶でも飲んで待ちましょう

Ruby のインストールが終わったら ServerSpec を gem でインストールします

$ sudo /opt/ruby-2.0.0/bin/gem install serverspec --no-ri --no-rdoc

あ、bundler 使ったほうが良かったかな?

まずは serverspec-init コマンドで土台を作ります

$ mkdir ~/serverspec
$ cd ~/serverspec
$ /opt/ruby-2.0.0/bin/serverspec-init
Select OS type:

  1) UN*X
  2) Windows

Select number: 1

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 コマンドでテストを実行してみましょう。今回インストールした ruby のパスが PATH に入ってないので先に入れておく必要があります

$ PATH=/opt/ruby-2.0.0/bin:$PATH
$ rake spec
/opt/ruby-2.0.0/bin/ruby -S rspec spec/192.168.33.12/httpd_spec.rb
......

Finished in 0.99275 seconds
6 examples, 0 failures

成功しました

spec/192.168.33.12/httpd_spec.rb に書いてあるテストが実行されました。
内容は次の通り

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

$ vi spec/192.168.33.12/mysqld_spec.rb

(例はこちら: https://gist.github.com/yteraoka/6156753)

作成したら再度 rake spec コマンドを実行します

$ rake spec
/opt/ruby-2.0.0/bin/ruby -S rspec spec/192.168.33.12/httpd_spec.rb spec/192.168.33.12/mysqld_spec.rb
..........

Finished in 1.24 seconds
10 examples, 0 failures

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

$ vi spec/192.168.33.12/wordpress_spec.rb

(例はこちら: https://gist.github.com/yteraoka/6186278)

Ansible Note にモジュールや細かいことを書いていきます。(Ansible in detail はなかなか更新できないので Wiki に移しました)

この tutorial では role として common, wordpress という分け方をしましたが、その後、Ansible AWX のインストーラーなどを見ると nginx, mysql などを role にするのが良さそうです。その上で site.yml を次のようにするのが良いようです

---
- hosts: all
  become: yes
  roles:
    - { role: sshd }
    - { role: ntpd }
    - { role: epel }

- hosts: webservers
  become: yes
  roles:
    - { role: nginx, name: 'value' }
    - ...

- hosts: dbservers
  become: yes
  roles:
    - { role: mysql, name: 'value' }
    - ...

「role A は role B に依存しているよ」なんていう書き方もできるようです。 Ansibleのroleを使いこなす [Qiita]

ここで紹介したベストプラクティス構成で多種多様なサーバーを一つにまとめるのは非常に困難です。共有したい role だけを外だしにするのが筆者の今のお勧めです。「Ansible オレ>オレベストプラクティス - Qiita [キータ]」お試しあれ。