IPが変わりゆくAWS EC2インスタンスにServerspec流す場合について本気出して考えてみた

Serverspec

Serverspec

Serverspec本を読んだので実践してみた。

EC2インスタンスはPublic IPが変わる可能性がある。 新しいインスタンスを増やしたり減らしたりもする。 サービスは複数あり、サーバーも複数だ。

そんな状況でServerspecでテストしていきたい場合、何ができるのだろうか。

ド素人ながら本気出して考えてみた*1

ユーザーの問題

Serverspecをが流すのか。

Serverspec用のユーザーを用意する

Serverspec用ユーザーにServerspec用鍵を持たせてServerspec(くどい)すればいいんじゃね?

  • 色んなサービス色んなサーバー全部に入れるユーザーと鍵ができることになる
  • しかもsudoしないとテストできない項目もあるのでできればsudo権限が必要
  • その鍵が盗まれれば全サーバーの死。リスキーな状態になる。
  • パスワードつけたりが意外とめんどくさい。。。
  • 結局ec2-userと同等の権限を持った全サーバーにログイン可能な神ユーザーが誕生。

といろいろ問題がある。

本気出した結論:

ec2-userでいいんじゃね。Serverspec本にもec2-userで紹介されているし。

秘密鍵問題

ec2-userでいいとして鍵はどうするのか。

Serverspec用マスターキーをもつ => 却下

ssh_optionにすべての鍵を列挙しておく => ssh的に5つまでしかダメ

本気出した結論

インスタンス作った時に指定した最初の鍵をつかう。名前はAPIから取る。

EC2のdescribe_instances APIからインスタンスの情報を取り、key_nameから鍵の名前を取得可能。秘密鍵名もだいたい "#{key_name}.pem"なので鍵置き場を固定すればいくつでも対応できるし、ばらばらでいいし、既に鍵は存在する。

IPどうやってとるの問題

基本describe_instances APIで取ればよし。

踏み台

踏み台サーバーから入らないといけない場合はProxyCommand相当を個々のサーバーごとに設定する必要がある。

本気出した結論

いわゆるName(タグ)の命名規則をちゃんと揃え、命名規則からProxyCommandを組み立てられるようにしておく。Nameの命名規則がととのっているとCapistoranoとかでも便利。

ssh_config問題

ssh_configに依存すると多人数で開発するとき「これ書いといてな」としか言えないしめっちゃめんどくさい。

本気出した結論

ssh_configを一切使わない。Serverspecのホスト指定もIPアドレスで行う。

specをホストごとに書いてられへんで問題

spec/app01.example.com/nginx_spec.rb

なんてやっていると当然流動的なサーバー群に対し無力。

本気出した結論

EC2のタグを使う。

"Roles"タグを用意し、','区切りで複数のロールを割り当てられるようにする。*2

さらにロール毎のspecディレクトリをきってロールに対するspecを書く。

specを走らせるときはdescribe_instances APIで取得したRolesタグからそのままディレクトリを指定して走らせるようにしておく

まとめ

まとめるとRakefileはこんな感じになる。

hosts = (aws-sdkを使ったすごいコード)

desc "Run serverspec to all instances"
task :spec    => hosts.map{|h| "spec:#{h.public_ip_address}"}
task :default => :spec
namespace :spec do
  hosts.each do |host|
    desc "Run serverspec to #{host.name}"
    RSpec::Core::RakeTask.new(host.public_ip_address) do |t|
      ENV['TARGET_HOST'] = "#{host.name}:#{host.public_ip_address}" # specが失敗した時に表示されて便利
      ENV['TARGET_IP'] = host.public_ip_address
      ENV['TARGET_KEY'] = File.expand_path("~/.ssh/aws/#{host.key_name}.pem")
      t.pattern = "spec/{base,#{host.roles.join(',')}}/**/*_spec.rb"
    end
  end
end
require 'serverspec'

puts "\nHost: #{ENV['TARGET_HOST']}"

set :backend, :ssh
set :request_pty, true
set :env, :LANG => 'en_US.UTF-8'
set :host, ENV['TARGET_IP']
set :ssh_options, :user => 'ec2-user', :keys => [ENV['TARGET_KEY']]

まーなんか、このへんが妥協点な気がする。

*1:この程度が僕の本気なんです……。

*2:web,app,db,...