ec2のipを動的に取得し、全てのインスタンスにServerspecを実行する

# やってること

- ec2のipを動的に取得
- 取得したipに対し、Serverspecを実行するtaskを動的に定義
- 上記のtaskを全て実行するtaskを定義
- awsのaccess key情報が環境変数で指定されてない場合、ec2の情報を取得しない

# 前提及び筆者の環境

- aws-sdkはv1を使用
- `vagrant`というホストに対して実行するspecが予め存在している

```console
$ ls -la spec

-rw-r--r--  1 nekogeruge staff 640  2 16 14:34 spec_helper.rb
drwxr-xr-x  4 nekogeruge staff 136  1 13 17:42 vagrant
```
- ec2に実行するspecは`vagrant`と同じものを使用する
- Serverspecを実行したいインスタンスには、踏み台サーバを介した多段sshを使用して接続しており、下記のような`.ssh/config`が書いてある

```
Host bastion
  HostName 99.999.99.999
  Port 22
  User ec2-user
  IdentityFile ~/.ssh/bastion.pem
  TCPKeepAlive yes
  PasswordAuthentication no
Host 10.0.1.*
  Port 22
  User ec2-user
  IdentityFile ~/.ssh/internal.pem
  ProxyCommand ssh bastion -W %h:%p
```

# Rakefile

```rb
require 'rake'
require 'rspec/core/rake_task'
require 'aws-sdk-v1'

#
# AWSのaccess key情報が環境変数に含まれている時のみ
# ec2のipを取得する
#
if ENV['AWS_ACCESS_KEY_ID'] && ENV['AWS_SECRET_ACCESS_KEY']
  AWS.config(
    {
      access_key_id: ENV['AWS_ACCESS_KEY_ID'],
      secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
      region: 'ap-northeast-1'
    }
  )

  ec2_hosts = AWS.ec2.instances.select { |i| i.status == :running }.map(&:private_ip_address)
end

task :spec    => 'spec:all'
task :default => :spec

namespace :spec do
  targets = []

  Dir.glob('./spec/*').each do |dir|
    next unless File.directory?(dir)
    targets << File.basename(dir)
  end

  task :all     => targets
  task :default => :all

  #
  # 取得したec2のipのtaskを作成
  #
  if ec2_hosts
    ec2_hosts.each do |host|
      desc "Run serverspec tests to ec2 {host}"
      RSpec::Core::RakeTask.new(host.to_sym) do |t|
        ENV['TARGET_HOST'] = host
        t.pattern = "spec/vagrant/*_spec.rb"
      end
    end

    #
    # 上で作成したtaskを全て実行する
    # spec:ec2というtaskを作成
    #
    desc "Run serverspec tests to all ec2 host"
    task :ec2 => ec2_hosts.map { |host| "spec:#{host}" }
  end

  targets.each do |target|
    desc "Run serverspec tests to #{target}"
    RSpec::Core::RakeTask.new(target.to_sym) do |t|
      ENV['TARGET_HOST'] = target
      t.pattern = "spec/#{target}/*_spec.rb"
    end
  end
end
```

# 実行

- aws関連の環境変数無し

```console
$ rake -T

rake spec:vagrant  # Run serverspec tests to vagrant
```

- 有り
  - [envchain](https://github.com/sorah/envchain)使用
    - 例として`aws_staging`というnamespaceを使用しています

```console
$ envchain aws_staging rake -T

rake spec:10.0.1.100    # Run serverspec tests to ec2 10.0.1.100
rake spec:10.0.1.101    # Run serverspec tests to ec2 10.0.1.101
rake spec:10.0.1.102    # Run serverspec tests to ec2 10.0.1.102
rake spec:ec2           # Run serverspec tests to all ec2 host
rake spec:vagrant       # Run serverspec tests to vagrant
```

- `rake spec:ec2`を実行すると、`10.0.1.100`、`10.0.1.100`、`10.0.1.102`に対してServerspecを実行するtaskが順番に実行される

```console
$ envchain aws_staging rake spec:ec2

...
```

# 問題点と解決策

実行してみると、specの結果にipが表示されない。
`spec_helper.rb`に下記を追記することにより、結果にホスト名及びipが表示されるようになる

```spec_helper.rb
puts "\nHost: #{ENV['TARGET_HOST']}"
```

```console
$ envchain aws_staging rake spec:ec2

Host: 10.0.1.100
...

Host: 10.0.1.101
...

Host: 10.0.1.102
...
```

# 参考

- [IPが変わりゆくAWS EC2インスタンスにServerspec流す場合について本気出して考えてみた](http://ksss9.hatenablog.com/entry/2015/02/14/203935)
- [How to share Serverspec tests among hosts](http://serverspec.org/advanced_tips.html)