Vagrantで複数の仮想マシンとansible_localを利用して構築する（ついでにansibleのhostsファイルも適用させる）

# やること
- DBサーバとWEBサーバを別々のVMで起動させる
- windows環境でansibleを実行できるようにする（ansible_local）
- inventory配下のhostsファイルをyamlで書く
- ansibleのinventory/hostsファイルを読み込んでローカル環境の設定を適用させる
- playbookはもちろんBestPracticesを踏襲

# 環境
- Host OS: Windows10 Pro
- Guest OS: Centos6.7 x86_64
- Ansible 2.5.2
- Vagrant 1.9.8
- VirtualBox 5.1.22

# 動機
Vagrantで複数のVMを立ち上げてansibleを導入しようとした時のことです。windows環境化でansibleを実行するには色々な方法がありますが、vagrantの[ansible_local](https://www.vagrantup.com/docs/provisioning/ansible_local.html)が使えそうな雰囲気だったので実装してみましたが、公式の方法がしっくりこなかったので独自に実装してみた話です。


## Ansible Parallel Execution
公式で並列処理時の[方法](https://www.vagrantup.com/docs/provisioning/ansible.html#ansible-parallel-execution)があったのでこれを参考に書きました。

```ruby:Vagrantfile
N = 3
(1..N).each do |machine_id|
  config.vm.define "machine#{machine_id}" do |machine|
    machine.vm.hostname = "machine#{machine_id}"
    machine.vm.network "private_network", ip: "192.168.77.#{20+machine_id}"

    # Only execute once the Ansible provisioner,
    # when all the machines are up and ready.
    if machine_id == N
      machine.vm.provision :ansible do |ansible|
        # Disable default limit to connect to all the machines
        ansible.limit = "all"
        ansible.playbook = "playbook.yml"
      end
    end
  end
end
```
``s/:ansible/:ansible_local/g``に書き換えて起動！
最後に立ち上げたVMにしかplaybookが作動せず、、、原因が良く分からない。
そもそも、ホストOSで呼び出す方法を真似たので良くなかったのかも。

## Ansible Parallel Execution from a Guest
``ansible_local``はGuest側でansibleを起動するのでこっちの[方法](https://www.vagrantup.com/docs/provisioning/ansible_local.html#ansible-parallel-execution-from-a-guest)を参照するべきでした。
ansible専用のVM（controller）を作ろうと書いてあります。

```ruby:Vagrantfile
Vagrant.configure("2") do |config|

  config.vm.box = "centos6.7x86_64"

  config.vm.define "web" do |machine|
    machine.vm.network "private_network", ip: "172.17.177.21"
  end

  config.vm.define "db" do |machine|
    machine.vm.network "private_network", ip: "172.17.177.22"
  end

  config.vm.define 'controller' do |machine|
    machine.vm.network "private_network", ip: "172.17.177.11"

    machine.vm.provision :ansible_local do |ansible|
      ansible.playbook       = "site.yml"
      ansible.verbose        = true
      ansible.install        = true
      ansible.limit          = "all" # or only "nodes" group, etc.
      ansible.inventory_path = "inventories/local/hosts"
    end
  end
end
```
「うーん、でもansible用のVMまでわざわざ作る必要あるのか？」と思い、もう少しシンプルな作りを目指してみました。

# 成果物
```text:playbook_from_AlternativeDirectoryLayout
|-- Vagrantfile
|-- ansible.cfg
`-- playbook
    |-- inventories
    |   |-- production
    |   |   `-- hosts.yml
    |   |-- staging
    |   |   `-- hosts.yml
    |   |-- develop
    |   |   `-- hosts.yml
    |   `-- local
    |       `-- hosts.yml
    |-- roles
    |   |-- common
    |   |-- httpd
    |   `-- postgresql
    |-- site.yml
    |-- dbservers.yml
    `-- webservers.yml
```

```bash:Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

require 'yaml'

# All Vagrant configuration is done below. The "2" in Vagrant.configure
# configures the configuration version (we support older styles for
# backwards compatibility). Please don't change it unless you know what
# you're doing.

VAGRANTFILE_API_VERSION = "2"
VAGRANTFILE_DIR = File.dirname(__FILE__)
ANSIBLE_INVENTORY_PATH = 'playbook/inventories/local/hosts.yml'

begin

conf = YAML.load_file(File.join(VAGRANTFILE_DIR, ANSIBLE_INVENTORY_PATH))
rescue Errno::ENOENT => ex
  STDERR.puts <<-EOT
    File does not exist. "#{ANSIBLE_INVENTORY_PATH}"
  EOT
  exit;
end

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|

  config.vm.box = "centos6.7x86_64"

  conf['all']['hosts'].each do |host, value|
    config.vm.define host do |machine|
      machine.vm.hostname = value['hostname']
      machine.vm.network "private_network", ip: value['ip']

      # virtualbox
      machine.vm.provider "virtualbox" do |vb|
        vb.name = value['hostname']
      end

      # synced_folder
      if value.has_key? 'synced_folder' then
        value['synced_folder'].each do |folder|
          machine.vm.synced_folder File.join(VAGRANTFILE_DIR, folder['host']), folder['guest']
        end
      end

      # ansible
      machine.vm.provision :ansible_local do |ansible|
        ansible.playbook = "playbook/#{host}.yml"
        ansible.inventory_path = ANSIBLE_INVENTORY_PATH
        ansible.limit = 'all'
      end
    end
  end
end
```

```yaml:playbook/inventories/local/hosts.yml
---
# file: playbook/inventories/local/hosts.yml

all:
  hosts:
    webservers:
      hostname: carolsendai.co.jp
      ip: 192.168.39.90
      synced_folder:
        - { guest: /var/LOG, host: ./LOG }
        - { guest: /usr/local/lib, host: ./lib }
    dbservers:
      hostname: db.carolsendai.co.jp
      ip: 192.168.39.91
```
```yaml:playbook/webservers.yml
---
# file: playbook/webservers.yml

- hosts: webservers
  connection: local
  become: yes
  roles:
    - common
    - httpd
```
```yaml:playbook/dbservers.yml
---
# file: playbook/dbservers.yml

- hosts: dbservers
  connection: local
  become: yes
  roles:
    - common
    - postgresql
```
# 解説
## hosts.yml
```ruby:Vagrantfile
require 'yaml'
ANSIBLE_INVENTORY_PATH = 'playbook/inventories/local/hosts.yml'
conf = YAML.load_file(File.join(VAGRANTFILE_DIR, ANSIBLE_INVENTORY_PATH))
```
inventoryの[hostsがyaml形式で書ける](https://docs.ansible.com/ansible/2.5/plugins/inventory/yaml.html)(ansible 2.1以上)ようになったことでロードが容易になりました。設定も色々記述できて自由度が上がったと思います。

## multi-machine
```ruby:Vagrantfile
VAGRANTFILE_DIR = File.dirname(__FILE__)
ANSIBLE_INVENTORY_PATH = 'playbook/inventories/local/hosts.yml'

conf['all']['hosts'].each do |host, value|
    config.vm.define host do |machine|
      machine.vm.hostname = value['hostname']
      machine.vm.network "private_network", ip: value['ip']

      # virtualbox
      machine.vm.provider "virtualbox" do |vb|
        vb.name = value['hostname']
      end

      # synced_folder
      if value.has_key? 'synced_folder' then
        value['synced_folder'].each do |folder|
          machine.vm.synced_folder File.join(VAGRANTFILE_DIR, folder['host']), folder['guest']
        end
      end

      # ansible
      machine.vm.provision :ansible_local do |ansible|
        ansible.playbook = "playbook/#{host}.yml"
        ansible.inventory_path = ANSIBLE_INVENTORY_PATH
        ansible.limit = 'all'
      end
    end
```
公式のような``N = 3``など記述せず、``hosts.yml``の配列分のVMを増やすこともできます。（そこまでする必要があるのかは置いといて）
``site.yml``が読み込まれていないのがちょっと残念な作り。