ServerspecをAnsibleのhostvars変数の値を使ってansible-playbook から実行する方法

# Serverspec run by ansible-playbook and it use hostvars variables of Ansible.

こちらは[Ansible Advent Calendar 2016](http://qiita.com/advent-calendar/2016/ansible)の25日目の記事です。

## 0.はじめに
本記事ではServerspecをAnsibleの[hostvars変数の値](http://qiita.com/tbuchi888/items/ddb7f9d51b578d5ecfa2)を使ってansible-playbook から実行する方法を紹介します。

+ こちらは以前書いた記事を元にアップデートしたものです。
+ コード一式は https://github.com/tbuchi888/serverspec-run-by-ansible-playbook.git へ上げています。すぐに試したい場合は以下手順を参照してください。

```　
#Step1
git clone https://github.com/tbuchi888/serverspec-run-by-ansible-playbook.git
cd serverspec-run-by-ansible-playbook

#Step2
#インベントリファイル（hosts.yml）やspec/ロール名/配下等を自環境へ合わせて修正。

#Step3
ansible-plyabook -i hosts.yml spec.yml
```

その他、参考までにAnsible公式サイトが推奨するテスト方法については以下をご確認ください。
http://docs.ansible.com/ansible/test_strategies.html

## 1.インベントリホストへのServerspecの実行
Ansibleのインベントリホストに対してServerspecを実施する場合
以下のような方法があると思います。

|No.|方法|特徴|
|:-:|:-:|:-:|
|1|Ansibleのcommandやshellモジュールから、`rake spec`や`rake spec:hogerole`などServerspecを直接呼び出す方法|AnsibleとServerspecで別々にinventoryやhostvarsを管理する必要がある。|
|2 |[ansible_spec](https://github.com/volanja/ansible_spec)などAnsibleとServerspecのパーサーを使う方法|ansible_specはGemでインストール後に、指定の形式（Inventoryファイルやロールなど、ベストプラクティスディレクトリ構成に近い）をとれば、Ansibleのファイルを利用して、容易にAnsibleのロール単位でテスト実行が可能で非常に便利です。ただし、Ansible側でロール構成が必要なことやインベントリファイル内のVariablesが未だ対応していない等の制約もあります。|
|3|[kitchen-ansible](https://github.com/neillturner/kitchen-ansible)などを利用する|こちらは未検証です。DockerやVagrantなどと連携して、テスト用のコンテナやVMを構築するイメージでしょうか。|

## 2.今回の方法
今回は、上述のNo.1に近い方法で、Ansibleの`template`モジュールやServerspecのTipsなど
以下基本機能を利用して「ServerspecをAnsibleのhostvars変数の値を使ってansible-playbookから実行する方法」を検討しました。

+ Ansible
  + [FAQ: In a template, get all the IPs of all machines in a group](https://support.ansible.com/hc/en-us/articles/201957817-In-a-template-get-all-the-IPs-of-all-machines-in-a-group)
+ Serverspec 
  + [advanced tips: How to use host specific properties](http://serverspec.org/advanced_tips.html)

具体的には、力技ですが ansible-playbook を実行時に

+ 各ターゲットホストのhostvarsの情報をAnsibleのtemplateモジュールを利用してYAML形式のファイルとしてdumpします。
+ 続けてServerspec呼出し時に先ほどdumpしたファイルのパスと
テストしたいロール名(`spec_role`)を渡してあげる

ことで実現しています。

![image01.png](https://qiita-image-store.s3.amazonaws.com/0/117986/ac3c31c1-e3c1-f421-71a2-bfa0509e6529.png)

### 3.環境
AnsibleとServerspec実行 HOSTマシン

- OSX Yosemite
  - Ansible: 2.3.0 (devel 6c3de3c299) last updated 2016/12/22 
  - Serverspec: 2.37.2

参考：サンプルのインベントリホスト（ターゲットホスト）マシン

 - Windows2012R2 * 1台
   - IISインストール済
 - CentOS6.8 * 2台
   - apacheインストール済 

なお、サンプルのマシンは[こちら](http://qiita.com/tbuchi888/items/c60025d68c8b49dbf6d1)を参考に今回作成しています。
また、CentOSについてはOSXユーザーのSSH公開鍵をvagrantユーザーにコピーしておきます。 

例）

```
ssh-copy-id vagrant@192.168.33.41
ssh-copy-id vagrant@192.168.33.41
```

### 4.事前準備
Directory構成は以下のようになります。

```
├── README.md
├── Rakefile                # Sererspec：カスタマイズしたもの
├── hosts.yml               # Ansible：  サンプルのインベントリファイル
├── spec
│   ├── AAA                 # Sererspec：サンプルのロール名
│   │   └── sample_spec.rb  # Sererspec：サンプルのspecファイル
│   ├── BBB                 # Sererspec：サンプルのロール名
│   │   └── sample_spec.rb  # Sererspec：サンプルのspecファイル
│   └── spec_helper.rb      # Sererspec：カスタマイズしたもの
├── spec.yml                # Ansible：  カスタマイズしたPlaybook
├── spec_vars               # Ansible：  playbook実行時に作成
│   ├── hostvars_centos6-httpd01.yml 
│   ├── hostvars_centos6-httpd02.yml
│   ├── hostvars_win2012-iis01.yml
└── templates               
    └── dump_variables.j2　　　　　　# Ansible：  hostvarsをダンプするjinjya2テンプレート

```

#### 4.1.Ansible側の準備

 - インベントリホスト（実行対象ホスト）毎のhostvarsをYAML形式でダンプするjinjya2のtemplateファイルを`templates/dump_variables.j2`として作成します。


``` dump_variables.j2
{{ hostvars[inventory_hostname] | to_yaml }}
```

- Ansible実行ホストマシンのlocalで以下を実施するplaybookを`spec.yml`として作成します。
  1. templateモジュールを利用して、インベントリホストのhostvarsの情報をYAML形式でカレントディレクトリ上の`spec_vars/[inventory_hostname].yml`として保存する
  1. shellモジュールより、上記ファイルと、serverspecのロール名（`spec_role`）を引数にしてserverspecを実行する
  1. debugモジュールでserverspec実行結果を表示する 

``` spec.yml
---
- hosts: all
  gather_facts: no
  vars:
    spec_vars_dir:  "{{playbook_dir}}/spec_vars"
    host_vars_path: "{{spec_vars_dir}}/hostvars_{{inventory_hostname}}.yml"
  tasks:
   - name: create "{{spec_vars_dir}}" directory
     file:
       path: "{{spec_vars_dir}}"
       state: directory 
     delegate_to: localhost
   - name: "dump_variables hostvars to yml"
     template:
       src: templates/dump_variables.j2
       dest: "{{host_vars_path}}"
     delegate_to: localhost
   - name: rake serverspec with hostvars
     shell: HOST_VARS_PATH={{host_vars_path}} rake serverspec:{{spec_role}}
     register: raw_result
     delegate_to: localhost
   - name: stdout of serverspec
     debug: var=raw_result.stdout_lines
```


- 参考：以下はサンプルのAnsibleのインベントリファイルとなります。
自環境に合わせて適宜変更してください。
なお、`spec_role`に合致するserverspecを実施します。
`spec_role`を定義しない場合はserverspecのテスト対象外となります。

``` hosts.yml
[win]
win2012-iis01 spec_role=AAA ansible_host=192.168.33.51

[centos]
centos6-httpd01 spec_role=AAA ansible_host=192.168.33.41
centos6-httpd02 spec_role=BBB ansible_host=192.168.33.42
myansible spec_role=BBB ansible_host=localhost

[win:vars]
ansible_connection=winrm
ansible_port=5985
ansible_user=vagrant
ansible_password=vagrant
web_service_name="W3SVC"

[centos:vars]
ansible_user=vagrant
ansible_private_key_file=~/.ssh/id_rsa
web_service_name=httpd
```

#### 4.2.Serverspec側の準備

- `Rakefile`を以下の通りカスタマイズします。
  1. hostvarsのYAML形式ファイルのパスを指定して、変数として取込ます。
  2. `spec_role`名をロールとして指定します。

Rakefile

``` Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'yaml'

hostvars = YAML.load_file(ENV['HOST_VARS_PATH'])

namespace :serverspec do
  desc "Run serverspec for #{hostvars["spec_role"]}"
  RSpec::Core::RakeTask.new(hostvars["spec_role"].to_sym) do |t|
    t.pattern = "spec/#{hostvars["spec_role"]}/*_spec.rb"
  end
end
```

- `spec_helper.rb`を以下通りカスタマイズします。
  1. hostvarsのYAML形式ファイルのパスを指定して、変数として取込ます。
  2. マルチOS（windows/un*x）、マルチコネクション(ssh/local/winrm)対応も実施します。

``` spec_helper.rb
require 'serverspec'
require 'net/ssh'
require 'winrm'
require 'yaml'

hostvars = YAML.load_file(ENV['HOST_VARS_PATH'])
set_property hostvars

if hostvars["ansible_connection"] == 'winrm'
#
# OS type: Windows / Connetion type: winrm
#
  set :backend, :winrm
  set :os, :family => 'windows'

  host = hostvars["ansible_ssh_host"]  || hostvars["ansible_host"]      || hostvars["inventory_hostname"]
  user = hostvars["ansible_ssh_user"]  || hostvars["ansible_user"]  
  port = hostvars["ansible_ssh_port"]  || hostvars["ansible_port"]    
  pass = hostvars["ansible_ssh_pass"]  || hostvars["ansible_password"]   

  endpoint = "http://#{host}:#{port}/wsman"

  winrm = ::WinRM::WinRMWebService.new(endpoint, :ssl, :user => user, :pass => pass, :basic_auth_only => true)
  winrm.set_timeout 300 # 5 minutes max timeout for any operation
  Specinfra.configuration.winrm = winrm

elsif hostvars["ansible_connection"] == 'local' || hostvars["ansible_ssh_host"] == 'localhost' || hostvars["ansible_host"] == 'localhost' || hostvars["inventory_hostname"] == 'localhost'
#
# OS type: UN*X / Connction type: local exec
#
 set :backend, :exec
else
#
# OS type: UN*X / Connction type: ssh
#
  set :backend, :ssh

  set :sudo_password, hostvars["ansible_ssh_pass"] || hostvars["ansible_password"]

  host = hostvars["ansible_ssh_host"] || hostvars["ansible_host"] || hostvars["inventory_hostname"]

  options = Net::SSH::Config.for(host)

  options[:user] ||= hostvars["ansible_ssh_user"]             || hostvars["ansible_user"]
  options[:port] ||= hostvars["ansible_ssh_port"]             || hostvars["ansible_port"]
  options[:keys] ||= hostvars["ansible_ssh_private_key_file"] || hostvars["ansible_private_key_file"]

  set :host,        options[:host_name] || host
  set :ssh_options, options

  # Disable sudo
  # set :disable_sudo, true

  # Set environment variables
  # set :env, :LANG => 'C', :LC_MESSAGES => 'C'

  # Set PATH
  # set :path, '/sbin:/usr/local/sbin:$PATH'
end
```

-  テスト用のspecファイルを準備します。
こちらは自環境に合わせて適宜変更してください。

以下サンプルのServersepcのロールとspecファイル

+ ROLE:AAA
  + spec/AAA/sample_spec.rb
  + Ansibleのhostvarsに設定した変数`web_service_name`のサービス状態を確認するサンプル

``` sample_spec.rb
require 'spec_helper'

describe service( property["web_service_name"] ) do
  it { should be_enabled }
  it { should be_running }
end
```

+ ROLE:BBB
  + spec/BBB/sample_spec.rb
  + Ansibleのインベントリファイルの`inventory_hostname`をホスト名として含んでいるか確認するサンプル

``` sample_spec.rb
require 'spec_helper'

describe command('hostname') do
  its(:stdout) { should contain( "#{property['inventory_hostname']}" ) }
end
```

### 5.参考：実行結果
以下サンプル環境でのplaybook実行結果です。
WindowsサーバとCentOSサーバの各々のロールに対してServerspecが実行されます。

```
$ ansible-playbook -i hosts.yml spec.yml

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

TASK [create spec_vars directory] *******************************************************************************************************************************
changed: [win2012-iis01 -> localhost]
ok: [centos6-httpd02 -> localhost]
ok: [centos6-httpd01 -> localhost]

TASK [dump_variables hostvars to yml] ***************************************************************************************************************************
changed: [centos6-httpd02 -> localhost]
changed: [centos6-httpd01 -> localhost]
changed: [win2012-iis01 -> localhost]

TASK [rake serverspec with hostvars] ****************************************************************************************************************************
changed: [centos6-httpd02 -> localhost]
changed: [centos6-httpd01 -> localhost]
changed: [win2012-iis01 -> localhost]

TASK [stdout of serverspec] *************************************************************************************************************************************
ok: [centos6-httpd02] => {
    "raw_result.stdout_lines": [
        "/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby -I/Library/Ruby/Gems/2.0.0/gems/rspec-support-3.5.0/lib:/Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/lib /Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/exe/rspec --pattern spec/BBB/\\*_spec.rb", 
        "", 
        "Command \"hostname\"", 
        "  stdout", 
        "    should contain \"centos6-httpd02\"", 
        "", 
        "Finished in 5.33 seconds (files took 2.5 seconds to load)", 
        "1 example, 0 failures"
    ]
}
ok: [centos6-httpd01] => {
    "raw_result.stdout_lines": [
        "/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby -I/Library/Ruby/Gems/2.0.0/gems/rspec-support-3.5.0/lib:/Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/lib /Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/exe/rspec --pattern spec/AAA/\\*_spec.rb", 
        "", 
        "Service \"httpd\"", 
        "  should be enabled", 
        "  should be running", 
        "", 
        "Finished in 5.62 seconds (files took 2.68 seconds to load)", 
        "2 examples, 0 failures"
    ]
}
ok: [win2012-iis01] => {
    "raw_result.stdout_lines": [
        "/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby -I/Library/Ruby/Gems/2.0.0/gems/rspec-support-3.5.0/lib:/Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/lib /Library/Ruby/Gems/2.0.0/gems/rspec-core-3.5.4/exe/rspec --pattern spec/AAA/\\*_spec.rb", 
        "", 
        "Service \"W3SVC\"", 
        " WARN  WinRM::WinRMWebService : WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_powershell_script instead", 
        "  should be enabled", 
        " WARN  WinRM::WinRMWebService : WinRM::WinRMWebService#run_powershell_script is deprecated. Use WinRM::CommandExecutor#run_powershell_script instead", 
        "  should be running", 
        "", 
        "Finished in 9.7 seconds (files took 2.48 seconds to load)", 
        "2 examples, 0 failures"
    ]
}

PLAY RECAP ******************************************************************************************************************************************************
centos6-httpd01            : ok=4    changed=2    unreachable=0    failed=0   
centos6-httpd02            : ok=4    changed=2    unreachable=0    failed=0   
win2012-iis01              : ok=4    changed=3    unreachable=0    failed=0 
```

## 6.その他
WindowsサーバへServerspecする際のリソースタイプや設定など以下が参考になりました。
https://github.com/mizzy/serverspec/blob/master/WINDOWS_SUPPORT.md