serverspecで並列処理をするためのメモ



## 背景
### Why?
インフラの構築作業を自働化したい
### How?
ミドルウェアのレイヤーを対象とし、Chefなどのツールを利用して実装する
### What?
自動構築した環境が意図した通りの設定になっているのか、繰り返し確認したい


## 作業環境
| software | version |
|:--|:--|
| Mac OS X | 10.8.5 |
| zsh | 4.3.11 (i386-apple-darwin12.0) |
| git | 1.8.4.3 |
| Homebrew | 0.9.5 |
| anyenv | - |
| rbenv | 0.4.0-67-g3300587 |
| ruby | 2.0.0-p247 |
| bundler | 1.3.5 |

#### -- 補足 --
* git は Homebrew によってインストールされてます
* rbenv は anyenv によってインストールされてます
* ruby は rbenv によってインストールされてます

## 前提
基本的には、 [serverspec の並列処理](http://mizzy.org/blog/2013/06/22/1/) で @gosukenator さんが書かれているように parallel_tests を使えば良いと思います

ただし、自分のように [serverspec でホスト固有の属性値を扱う方法](http://mizzy.org/blog/2013/05/12/2/) のように各ホストの属性値をYAMLで取得してRakeから実行する場合には、 parallel_tests では上手く行かなかったので、それを試行錯誤してできた結果がこのメモになります


## 1. ホストごとの属性値を定義する

```conf/attributes.yml
host1:
  :service:
    - 'os'
    - 'user'
    - 'ntp'
    - 'ssh'
    - 'apache22'
  :os_base_type:            'FreeBSD'
  :os_base_version:         'xxx.xxx-RELEASE-xxx'
  :os_base_architecture:    'amd64'
host2:
  :service:
    - 'os'
    - 'user'
    - 'ntp'
    - 'ssh'
    - 'postgresql'
  :os_base_type:            'FreeBSD'
  :os_base_version:         'xxx.xxx-RELEASE-xxx'
  :os_base_architecture:    'amd64'
host3:
  :service:
以下略
```


## 2. Rakeタスクを定義する

```Rakefile
#  encoding: utf-8
ATTRIBUTES_FILE = 'conf/attributes.yml'
RSPEC_OPTIONS = '-c -fs'

PROCESSES_NUMBER = 3
THREAD_NUMBER = 12

require 'rake'
require 'rspec/core/rake_task'
require 'yaml'
require 'parallel'

#  Rake task
#  rake all
desc "Parallel Runs Targets => registered all-hosts"
task :all => 'serverspec:parallel_all_hosts'

#  rake host\[hostname\]
desc "Parallel Runs Targets => registered host all-services"
task :host, "hostname"
task :host do |x, args| Rake::Task['serverspec:parallel_all_services'].invoke(args.hostname) end

#  rake parallel
#desc "Serial Runs Targets => registered all-hosts"
task :serial_all => 'serverspec:all_hosts'

#  Load 'host list & host service list' data
attributes = YAML.load_file(ATTRIBUTES_FILE)

namespace :serverspec do
  task :all_hosts => attributes.keys.map {|host| 'serverspec:' + host.split('.')[0] }
    attributes.keys.each do |host|
      if File.exist?("spec/#{host}/") then
        desc "Serial Runs Targets   => #{host}"
        RSpec::Core::RakeTask.new(host.split('.')[0].to_sym) do |t|
          ENV['TARGET_HOST'] = host
          t.rspec_opts = RSPEC_OPTIONS
          t.pattern = "spec/#{host}/{" + attributes[host][:service].join(',') + '}/*_spec.rb'
        end
      end
    end
  attributes.keys.map {|host|
    attributes[host][:service].each do |service|
      task :service => ('serverspec:' + host.split('.')[0] + '_' + service.split('.')[0]).to_sym
      if File.exist?("spec/#{host}/#{service}/") then
        desc "Serial Runs Targets   => #{host}: #{service}"
        RSpec::Core::RakeTask.new((host.split('.')[0] + '_' + service.split('.')[0]).to_sym) do |t|
          ENV['TARGET_HOST'] = host
          t.rspec_opts = RSPEC_OPTIONS
          t.pattern = "spec/#{host}/#{service}/*_spec.rb"
        end
      end
    end
  }
  task :parallel_all_hosts do
    Parallel.each(attributes.keys, in_processes: PROCESSES_NUMBER) do |host|
      Parallel.each(attributes[host][:service], in_threads: THREAD_NUMBER) do |service|
        if File.exist?("spec/#{host}/#{service}/") then
          require 'open3'
          command = 'rake serverspec:' + host.split('.')[0] + '_' + service.split('.')[0]
          o, e, s = Open3.capture3(command)
          puts o #  stdout
          puts e #  stderr
          # puts s #  status
        end
      end
    end
  end
  task :parallel_all_services, "hostname"
  task :parallel_all_services do |x, args|
    host = args.hostname
    Parallel.each(attributes[host][:service], in_threads: THREAD_NUMBER) do |service|
      if File.exist?("spec/#{host}/#{service}/") then
        require 'open3'
        command = 'rake serverspec:' + host.split('.')[0] + '_' + service.split('.')[0]
        o, e, s = Open3.capture3(command)
        puts o #  stdout
        puts e #  stderr
        # puts s #  status
      end
    end
  end
end

```

#### -- 課題 --
* 他の人のコードを参考に、初めて書いたので細かいことは良くわかってないｗ
* 同じことを繰り返し書いているので、直し方は知らないけどスッキリするように修正したい
* parallel で並列処理するための値は、外部から引数で上書き指定ができるようにしたい

とか課題が色々とあるインフラエンジニアの書いたひどいコードですが、自分がやりたかった目的は達成できたので今のところはこれで許容してます


とりあえず、この状態で

rake -T を実行すると

```
rake all                          # Parallel Runs Targets => registered all-hosts
rake host[hostname]               # Parallel Runs Targets => registered host all-services
rake serverspec:host1             # Serial Runs Targets   => host1
rake serverspec:host1_apache22    # Serial Runs Targets   => host1: apache22
rake serverspec:host1_ntp         # Serial Runs Targets   => host1: ntp
rake serverspec:host1_os          # Serial Runs Targets   => host1: os
rake serverspec:host1_ssh         # Serial Runs Targets   => host1: ssh
rake serverspec:host1_user        # Serial Runs Targets   => host1: user
rake serverspec:host2             # Serial Runs Targets   => host2
rake serverspec:host2_ntp         # Serial Runs Targets   => host2: ntp
rake serverspec:host2_os          # Serial Runs Targets   => host2: os
rake serverspec:host2_postgresql  # Serial Runs Targets   => host2: postgresql
rake serverspec:host2_ssh         # Serial Runs Targets   => host2: ssh
rake serverspec:host2_user        # Serial Runs Targets   => host2: user
rake serverspec:host3             # Serial Runs Targets   => host3
以下略
```

・全ホストを対象に実行
・指定ホストを対象に実行
・指定ホストの指定サービスを対象に実行

と各種タスクが用意されている状態にできます


## 3. 全ホストを対象にしたテストを実行した場合

Rakefile の

```
task :all_hosts => attributes.keys.map {|host| 'serverspec:' + host.split('.')[0] }
```

のところで、実際に行われている処理は

```
task :all_hosts => [serverspec:host1,serverspec:host2,serverspec:host3]
```

上記のように事前タスクを実行するタスクとして定義されています


あとは、並列処理を実行するためのタスクを作成して、

```
task :parallel_all_hosts do
```

parallel で各ホストを in_processes で並列実行し、

```
Parallel.each(attributes.keys, in_processes: PROCESSES_NUMBER) do |host|
```

さらに、それぞれのホスト内で定義されているサービスを in_threads で並列処理しています

```
Parallel.each(attributes[host][:service], in_threads: THREAD_NUMBER) do |service|
```


## 4. rakeタスク実行時に内部処理していること

実際に内部でやってる処理自体は、

* 事前タスクを定義した、全ホスト実行用のタスクを定義する
 * 上記タスクの処理過程で、事前タスクと定義されていた個別ホスト実行用のタスクが定義される

* 個別ホストの指定したサービスのみを実行できるタスクを定義している

* 全ホストを対象に並列実行するタスクを定義
 * 上記タスクから、 rake コマンドが並列で実行される
 * その際に、実行したテストコードの結果表示が混ざらないように、 open3 を利用して標準出力とエラー出力を各 rake コマンド単位で画面出力するようにしています

くらいなので、YAMLを利用しない場合は、ディレクトリのリストから map を作成して並列処理するとかやれば良いのではないかと思います