AdventCalendar
devops
serverspec
Ansible
Infrastracture_as_code
AnsibleDay 25

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

More than 1 year has passed since last update.

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

こちらはAnsible Advent Calendar 2016の25日目の記事です。

0.はじめに

本記事ではServerspecをAnsibleのhostvars変数の値を使ってansible-playbook から実行する方法を紹介します。

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

2.今回の方法

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

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

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

ことで実現しています。

image01.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インストール済

なお、サンプルのマシンはこちらを参考に今回作成しています。
また、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として保存する
    2. shellモジュールより、上記ファイルと、serverspecのロール名(spec_role)を引数にしてserverspecを実行する
    3. 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

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