VagrantのProvisionerとしてAnsibleを使う（Windows with CygwinをControl Machineとして）

現時点（1.8.4）では、AnsibleはControl MachineとしてWindowsをサポートしていないが、ブログ[Running Vagrant with Ansible Provisioning on Windows](http://www.azavea.com/blogs/labs/2014/10/running-vagrant-with-ansible-provisioning-on-windows/)等を参考にセットアップしてみたので手順をまとめておく。

前半は純粋にWindows(Cygwin)でAnsibleを使う設定になっていてVagrantとは無関係に使用できる。

なお、末尾の回避策に記載しているが未解決の問題（秘密鍵のパーミションが緩過ぎると言われて蹴られてしまう）があり、一部強引にAnsibleのソースコードを書き換えて回避している。

#環境
* Windows 8.1 Pro
* Cygwin
* Vagrant 1.7.2
* Ansible 1.8.4

#Cygwinの環境を整える
Cygwinの下記のパッケージを追加する（通常のCygwin Setupを使用）。

* python
* python-paramiko
* python-crypto
* python-setuptools
* gcc-core
* gcc-g++
* make
* wget
* openssh
* libyaml-devel

#pipをインストールする
Ansibleをインストールするためにpip（Pythonのパッケージ管理システム）をインストールする（本作業時点では、CygwinのPythonは、2.7.8であったためpipはデフォルトでは付属していないため）。

```lang:
$ python /usr/lib/python2.7/site-packages/easy_install.py pip
...
...
Installed /usr/lib/python2.7/site-packages/pip-6.0.8-py2.7.egg
Processing dependencies for pip
Finished processing dependencies for pip
```

#Ansibleをインストールする
```lang:
$ pip install ansible
Collecting ansible
  Downloading ansible-1.9.0.1.tar.gz (916kB)
...
...
Successfully installed PyYAML-3.11 ansible-1.9.0.1 ecdsa-0.13 jinja2-2.7.3 markupsafe-0.23
```

なお、
` Command "python setup.py egg_info" failed with error code 1`
が出てインストールできないときはsetuptoolsをupgradeします。
`$ pip install --upgrade setuptools`
#Ansible単体の動作確認の準備
ここで、VagrantのProvisionerとしてのAnsibleを動作させる前にAnsible単体の動作確認をする。
##Managed Nodeを用意する
Managed Node（Ansibleで操作される側のサーバ）として、適当な仮想マシンをvagrant upしておく等して、IP Addressを控えておく（以下では仮に192.168.30.11と192.168.30.12の2ノードとする）。

##inventoryファイルを用意する
```lang:
mkdir /etc/ansible
chmod 755 /etc/ansible
```
```lang:/etc/ansible/hosts
192.168.30.11
192.168.30.12
```

##sshのセットアップ
###鍵ペアを作成する。
```lang:
$ ssh-keygen
Generating public/private rsa key pair.
Enter file in which to save the key (/home/XXXX/.ssh/id_rsa):
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /home/XXXX/.ssh/id_rsa.
Your public key has been saved in /home/XXXX/.ssh/id_rsa.pub.
The key fingerprint is:
```

###Managed Nodeにsshで使用するホスト名を付ける（オプション）

```lang:~/.ssh/config
Host node01
  HostName 192.168.30.11
Host node02
  HostName 192.168.30.12
```
この設定をした場合は inventoryファイルは下記でも良い。

```lang:/etc/ansible/hosts
node01
node02
```


###公開鍵をManaged Nodeに配布する
```lang:
$ ssh-copy-id vagrant@node01`
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
vagrant@192.168.30.11's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'vagrant@node01'"
and check to make sure that only the key(s) you wanted were added.

$ ssh-copy-id vagrant@node02
/usr/bin/ssh-copy-id: INFO: attempting to log in with the new key(s), to filter out any that are already installed
/usr/bin/ssh-copy-id: INFO: 1 key(s) remain to be installed -- if you are prompted now it is to install the new keys
vagrant@192.168.30.11's password:

Number of key(s) added: 1

Now try logging into the machine, with:   "ssh 'vagrant@node02'"
and check to make sure that only the key(s) you wanted were added.
```

ここで、@の前の"vagrant"は、node01,node02へログインするユーザー名。

###ControlMasterの設定
ansibleでは、sshのcontrol masterとの通信に問題があるらしく下記の修正が必要だった。

```lang:/etc/ansible/ansible.cfg
[ssh_connection]
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
control_path = /tmp
```

ちなみに、この設定をしないと

```lang:
fatal: [node01] => SSH Error: Failed to connect to new control master
    while connecting to 192.168.30.11:22
```
となる。

###sshの認証エージェントを起動する
パスフレーズの入力を省略するため認証エージェントを使う。

```lang:
$ eval `ssh-agent`
Agent pid 9148
```
（ssh-agentは、バッククオートで囲む）

ちなみに、認証エージェントを使わないとパスフレーズを何度も入力しなければならないだけでなく、複数ノードへのssh接続要求を平行して処理するためパスフレーズの入力要求が入り乱れうまく入力できない。その場合は、ansible-playbook起動時に `-f 1` を付けて一度に1ノードづつ処理するようにすればパスフレーズの入力が可能となる。

###認証エージェントに秘密鍵を記憶させる
```lang:
$ ssh-add .ssh/id_rsa
Enter passphrase for .ssh/id_rsa:
Identity added: .ssh/id_rsa (.ssh/id_rsa)
```

#Ansible単体の動作確認
##Ad-hocコマンド
Ad-hocコマンドを実行してAnsibleが単体で動作することを確認する。
2つのManaged Node両方から /etc/redhat-releaseを取ってくるAd-hocコマンドを実行してみる。

```lang:
$ ansible all -a "cat /etc/redhat-release" -u vagrant
192.168.30.12 | success | rc=0 >>
CentOS Linux release 7.1.1503 (Core)

192.168.30.11 | success | rc=0 >>
CentOS Linux release 7.1.1503 (Core)
```

無事成功。
##Playbook
簡単なPlaybookの動作も確認しておく。
2つのノード両方に"kuroneko"というユーザーを登録するPlaybookを用意する。

```lang:
$ cat <<EOF > add_user.yml
> - hosts: all
>   sudo: yes
>   remote_user: vagrant
>   vars:
>     username: kuroneko
>   tasks:
>     - name: add user
>       user: name={{ username }} group=wheel shell=/usr/bin/bash
> EOF
```

ansible-playbookを実行してみる。

```lang:
$ ansible-playbook add_user.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [192.168.30.11]
ok: [192.168.30.12]

TASK: [add user] **************************************************************
changed: [192.168.30.11]
changed: [192.168.30.12]

PLAY RECAP ********************************************************************
192.168.30.11              : ok=2    changed=1    unreachable=0    failed=0
192.168.30.12              : ok=2    changed=1    unreachable=0    failed=0
```

無事成功。念のため、kuronekoが作成されていることを確認。

```lang:
$ ansible all -a "grep kuroneko /etc/passwd" -u vagrant
192.168.30.12 | success | rc=0 >>
kuroneko:x:1002:10::/home/kuroneko:/usr/bin/bash

192.168.30.11 | success | rc=0 >>
kuroneko:x:1002:10::/home/kuroneko:/usr/bin/bash
```

ユーザー"kuroneko"が確かに追加されていて、playbookが正常に実行されたことが確認できた。

#ansible-playbookをvagrantから呼び出せるように設定
##ansible-playbook.batファイルを用意する
Vagrantからansible-playbookをvagrantから呼び出すためには

* Windows PATHの中に ansible-playerを配置する
* ansible-playerがCygwinのPaythonを使用できる


ことが必要。
そこで、下記のbatファイルをC:\HashiCorp\Vagrant\binに配置。

```lang:ansible-playbook.bat
@echo off

set CYGWIN=C:\cygwin64

set SH=%CYGWIN%\bin\bash.exe

"%SH%" -c "/bin/ansible-playbook %*"
```

ここで、`CYGWIN=C:\cygwin64`は、Cygwinをインストールしたディレクトリです。実環境に合わせてください。また、batファイルを配置するディレクトリは、`C:\HashiCorp\Vagrant\bin`である必要はありません。WindowsのPATHが通っている場所に配置します。

ansible-playbook.batの動作確認をしておきます。

```lang:ansible-playbook.bat動作確認
$ ansible-playbook.bat add_user.yml

PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [node01]
ok: [node02]

TASK: [add user] **************************************************************
ok: [node01]
ok: [node02]

PLAY RECAP ********************************************************************
node01                     : ok=2    changed=0    unreachable=0    failed=0
node02                     : ok=2    changed=0    unreachable=0    failed=0
```

batファイルからansible-playbookが呼び出されて、無事add_user.ymlが実行されています。

#VagrantのProvisionerとしてのAnsibleの動作確認
##Vasgrantfileの用意
2つのCentOS 7 (1503)の仮想サーバを作成して、ApacheをprovisioningするVagrantfileを用意。

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

  if Vagrant.has_plugin?("vagrant-cachier")
    config.cache.scope = :box
  end
  if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.auto_update = false
  end
  config.vm.define "web02" do |web|
    web.vm.box = "centos7-1503-01-min"
    web.vm.hostname = "web02"
    web.vm.network "private_network", ip: "192.168.40.12"
    web.vm.provision "shell",
      inline: "nmcli connection reload;systemctl restart network.service"
    web.vm.provision "ansible" do |ansible|
      ansible.playbook = "ensure-apache-latest.yml"
    end
  end
  config.vm.define "web01" do |web|
    web.vm.box = "centos7-1503-01-min"
    web.vm.hostname = "web01"
    web.vm.network "private_network", ip: "192.168.40.11"
    web.vm.provision "shell",
      inline: "nmcli connection reload;systemctl restart network.service"
    web.vm.provision "ansible" do |ansible|
      ansible.playbook = "ensure-apache-latest.yml"
    end
  end

end
```

呼び出されているPlaybookは以下のとおり。

```lang:ensure-apache-latest.yml
- hosts: all
  sudo: yes
  remote_user: vagrant
  tasks:
    - name: ensure apache is at the latest version
      yum: pkg=httpd state=latest
      notify:
      - stop firewalld
      - restart httpd
  handlers:
    - name: stop firewalld
      service: name=firewalld state=stopped enabled=no
    - name: restart httpd
      service: name=httpd state=restarted enabled=yes
```

##動作確認（失敗）
上記のVagrantfileで、vagrant upすると下記のエラーメッセージが表示され、Playbookを起動できない。

```lang:ansibleのエラーメッセージ
fatal: [web01] => private_key_file (D:/Vagrant/projects/ansible-windows/.vagrant/machines/web01/virtualbox/private_key) is group-readable or world-readable and thus insecure - you will probably get an SSH failure
```

秘密鍵ファイルのパーミションが寛容過ぎるということのようだが、chmod 600したり setfaclでaclを厳しくしても解決できなかった。
本記事の上の方で ~/.ssh/id_rsaを使って、Playbookの動作確認をしているので、このファイルと同じパーミションを設定したり getfacl ~/.ssh/id_rsa | setfacl -f - private_key 等を試してみたがなぜか解決できなかった。stat private_key等してみてもおかしなところはないように見える。CygwinとWindowsのパーミション関係で何か見逃していそうだがわからない。そこで、次の回避策を強行した。

#回避策

上のエラーメッセージは、`/usr/lib/python2.7/site-packages/ansible/runner/connection.py`から出力されている。

```lang:connection.py抜粋
if st is not None and st.st_mode & (stat.S_IRGRP | stat.S_IROTH):
  raise AnsibleError("private_key_file (%s) is group-readable or world-readable and thus insecure - "
    "you will probably get an SSH failure"
    % (private_key_file,))
```

試験的にこの部分を#でコメントアウトして、vagrant destroy;vagrant upしたところ無事、provisioningすることができた。


```lang:実行ログの抜粋
$ vagrant up
Bringing machine 'web02' up with 'virtualbox' provider...
Bringing machine 'web01' up with 'virtualbox' provider...
...
...
...
==> web02: Running provisioner: ansible...
...
...
...
PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [web02]

TASK: [ensure apache is at the latest version] ********************************
changed: [web02]

NOTIFIED: [stop firewalld] ****************************************************
changed: [web02]

NOTIFIED: [restart httpd] *****************************************************
changed: [web02]

PLAY RECAP ********************************************************************
web02                      : ok=4    changed=3    unreachable=0    failed=0
==> web02: Configuring cache buckets...
==> web01: Importing base box 'centos7-1503-01-min'...
...
...
...
==> web01: Running provisioner: ansible...
...
...
...
PLAY [all] ********************************************************************

GATHERING FACTS ***************************************************************
ok: [web01]

TASK: [ensure apache is at the latest version] ********************************
changed: [web01]

NOTIFIED: [stop firewalld] ****************************************************
changed: [web01]

NOTIFIED: [restart httpd] *****************************************************
changed: [web01]

PLAY RECAP ********************************************************************
web01                      : ok=4    changed=3    unreachable=0    failed=0
==> web01: Configuring cache buckets...
```
#余談
上のVagrantfileのansible playbookの呼び出し方だと、仮想サーバ作って、プロビジョンして、仮想サーバ作って、プロビジョンしてをただひたすらシーケンシャルに実行するだけでつまらない。VagrantのドキュメントのAnsibleのTIPS AND TRICSに"ANSIBLE PARALLEL EXECUTION"という記載がある。これをまねて下記のVagrantfileで、provisioningの並列実行をやってみた。


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

  if Vagrant.has_plugin?("vagrant-cachier")
    config.cache.scope = :box
  end
  if Vagrant.has_plugin?("vagrant-vbguest")
    config.vbguest.auto_update = false
  end
  config.ssh.insert_key = false
  config.vm.define "web02" do |web|
    web.vm.box = "centos7-1503-01-min"
    web.vm.hostname = "web02"
    web.vm.network "private_network", ip: "192.168.40.12"
    web.vm.provision "shell",
      inline: "nmcli connection reload;systemctl restart network.service"
  end
  config.vm.define "web01" do |web|
    web.vm.box = "centos7-1503-01-min"
    web.vm.hostname = "web01"
    web.vm.network "private_network", ip: "192.168.40.11"
    web.vm.provision "shell",
      inline: "nmcli connection reload;systemctl restart network.service"
    web.vm.provision "ansible" do |ansible|
      ansible.playbook = "ensure-apache-latest.yml"
      ansible.limit = 'all'
    end
  end

end
```

すべての仮想サーバへひとつの秘密鍵でsshするため`config.ssh.insert_key = false`を忘れると1台を除いてprovisioningに失敗するので注意。

後でlegacy insecure keyを入れ替えなければならないが、使えそうだ。