VPC 上に構築した Elastic Beanstalk の EC2 インスタンスを踏み台サーバ経由で serverspec するまでの流れ、のメモ。
だいたい以下のような構成だと当てはまるはず。
- Elastic Beanstalk 使ってる
- VPC 上で使ってる
- 踏み台使って EC2 インタンスにアクセスしてる
- 雑多な操作には fabric 使ってる
- ステージングと本番の2環境ある
導入
serverspec インストール
Gemfile 作成
source 'https://rubygems.org'
gem 'serverspec'
gem 'rake'
インストール
mkdir -p vendor/bundle
bundle install --path vendor/bundle
初期化
初期化コマンドを実行して雛形を作成する。
backend は SSH 接続を選択、ホスト名は適当な値を入力。
bundle exec serverspec-init
Select OS type:
1) UN*X
2) Windows
Select number: 1
Select a backend type:
1) SSH
2) Exec (local)
Select number: 1
Vagrant instance y/n: n
Input target host name: example.com
+ spec/
+ spec/example.com/httpd_spec.rb
+ spec/spec_helper.rb
+ Rakefile
テストを書く
デフォルトのhttpd_spec.rb
をリネームして nginx_spec.rb
にしてみる。
require 'spec_helper'
describe package('nginx') do
it { should be_installed }
end
describe service('nginx') do
it { should be_running }
end
describe port(80) do
it { should be_listening }
end
describe file('/etc/nginx/conf.d/webserver.conf') do
it { should be_file }
end
やりたいことの書き方は公式のここを見るとわかりやすい。
その他の spec を書くのに以下のリソースタイプを使ったりした。
be_mode
ファイルモードを確認する
ex. nginx のログディレクトリに実行権限があるか(fluentd用)
describe file('/var/log/nginx') do
it { should be_mode 755 }
end
match
プロセスが指定のパラメータで起動しているか
ex. newrelic が起動しているか
describe process("nrsysmond") do
its(:args) { should match /-c \/etc\/newrelic\/nrsysmond.cfg -p / }
end
踏み台設定
serverspec 的に ssh config の設定を使ったほうが楽そうなので、踏み台サーバ(*-bastion)から多段SSHで接続できるように定義しておく。
対象サーバの HostName は serverspec 側で動的に割り当てるようにするので、ここでは設定しない。
(AutoScaling 走ってるので書いてもしょうがないし)
.ssh/config
Host foo-staging-bastion
HostName 54.xxx.yyy.zzz
IdentityFile ~/.ssh/foo-staging.pem
User foouser
Host foo-staging
IdentityFile ~/.ssh/foo-staging.pem
User ec2-user
ProxyCommand ssh foo_step -W %h:%p
Host foo-staging-bastion
HostName 54.zzz.yyy.xxxx
IdentityFile ~/.ssh/foo-production.pem
User foouser
Host foo-production
IdentityFile ~/.ssh/foo-production.pem
User ec2-user
ProxyCommand ssh foo_step -W %h:%p
sererspec 設定
デフォルトの設定ではホスト毎にディレクトリを掘って spec を書かなければいけないので、role 毎に spec を書けるように変更する。role とホストの定義は json ファイルから読み込むようにする。
基本的には公式 Tips を少しアレンジしただけ。
初期化コマンドで生成した spec/example.com
を spec/base
に変更して、 Rakefile
と spec/spec_helper.rb
の内容を書き換える。
対象のロールを増やしたい場合は base
ディレクトリと同列に webapp
とか db
とかディレクトリ追加して、その中に spec 書いていけばよい。
spec/env/servers.json
にはホストと role を定義する。
ディレクト構成は以下な感じ。
.
├── Rakefile
└── spec
├── base
│ ├── fluentd_spec.rb
│ ├── newrelic_spec.rb
│ ├── nginx_spec.rb
│ └── uwsgi_spec.rb
├── env
│ └── servers.json
└── spec_helper.rb
各種ファイルを以下のように変更する。
spec/spec_helper.rb
require 'serverspec'
require 'pathname'
require 'net/ssh'
include SpecInfra::Helper::Ssh
include SpecInfra::Helper::DetectOS
RSpec.configure do |c|
c.request_pty = true
if ENV['ASK_SUDO_PASSWORD']
require 'highline/import'
c.sudo_password = ask("Enter sudo password: ") { |q| q.echo = false }
else
c.sudo_password = ENV['SUDO_PASSWORD']
end
c.before :all do
block = self.class.metadata[:example_group_block]
if RUBY_VERSION.start_with?('1.8')
file = block.to_s.match(/.*@(.*):[0-9]+>/)[1]
else
file = block.source_location.first
end
c.ssh.close if c.ssh
c.host = ENV['TARGET_HOST']
hostname = ENV['TARGET_HOSTNAME']
options = Net::SSH::Config.for(c.host)
user = options[:user] || Etc.getlogin
c.ssh = Net::SSH.start(hostname, user, options)
c.os = backend.check_os
end
end
公式 Tips のままだと何度やっても死ぬので以下を追記してる。
c.request_ppy = true
ref: serverspecがsudoのところで動かなくて悩んだ件(解決) #serverspec - きょうもぼへぼへちゃんがゆく
Rakefile
require 'rake'
require 'rspec/core/rake_task'
require 'json'
servers = JSON.parse(File.read('spec/env/servers.json'))
desc "Run serverspec to all servers"
task :spec => 'serverspec:all'
class ServerspecTask < RSpec::Core::RakeTask
attr_accessor :target_host, :target_hostname
def spec_command
cmd = super
"env TARGET_HOST=#{target_host} TARGET_HOSTNAME=#{target_hostname} #{cmd}"
end
end
namespace :serverspec do
task :all => servers.map {|s| 'serverspec:' + s['name'] }
servers.each do |server|
desc "Run serverspec to #{server['name']} (#{server['hostname']})"
ServerspecTask.new(server['name'].to_sym) do |t|
t.target_host = server['host']
t.target_hostname = server['hostname']
t.pattern = 'spec/{' + server['roles'].join(',') + '}/*_spec.rb'
end
end
end
spec/env/servers.json
[
{
"host": "foo-staging",
"hostname": "10.0.2.206",
"name": "foo-staging-10.0.2.206",
"roles": [
"base"
]
},
{
"host": "foo-production",
"hostname": "10.0.3.225",
"name": "foo-production-10.0.3.225",
"roles": [
"base"
]
},
{
"host": "foo-production",
"hostname": "10.0.1.205",
"name": "foo-production-10.0.1.205",
"roles": [
"base"
]
}
]
host に .ssh/config に設定した Host, hostname に private ip を定義する。
テスト実行
bundle exec rake spec
trace オプションを付けるとデバッグしやすい
bundle exec rake spec -t
定義ファイルの自動生成
毎回変わる EC2 インスタンスをいちいち手書きするのもアレなので、fabric で servers.json を自動生成できるようにしておく。
# -*- coding: utf-8 -*-
import json
import boto
import boto.beanstalk
import boto.ec2
from fabric.api import task, puts
# Elastic Beanstalk の Applicaton 名
EB_APP_NAME = 'example-app'
# Elastic Beanstalk の リージョン名
EB_REGION = 'ap-northeast-1'
def _get_eb_conn():
""" Beanstalk の conn オブジェクトを取得する
"""
region = [r for r in boto.beanstalk.regions() if r.name == EB_REGION][0]
return boto.connect_beanstalk(region=region)
def _get_ec2_conn():
""" EC2 の conn オブジェクトを取得する
"""
region = [r for r in boto.ec2.regions() if r.name == EB_REGION][0]
return boto.connect_ec2(region=region)
def _get_eb_environments():
""" Application 内の Environment 一覧を返す
"""
conn = _get_eb_conn()
res = conn.describe_environments(application_name=EB_APP_NAME)
return res['DescribeEnvironmentsResponse']['DescribeEnvironmentsResult']['Environments']
@task
def generate_spec_servers():
""" Application 内の EC2 で severs.json を生成する
"""
puts('Start generate spec/env/servers.json for serverspec')
envs = _get_eb_environments()
eb_conn = _get_eb_conn()
ec2_conn = _get_ec2_conn()
servers = []
for e in envs:
env_name = e['EnvironmentName']
if 'production' in env_name:
host = 'foo-production'
elif 'staging' in env_name:
host = 'foo-staging'
res = eb_conn.describe_environment_resources(environment_id=e['EnvironmentId'])
instances = res['DescribeEnvironmentResourcesResponse']['DescribeEnvironmentResourcesResult']['EnvironmentResources']['Instances']
for ins in ec2_conn.get_only_instances(instance_ids=[i['Id'] for i in instances]):
servers.append({
'name': '{}-{}'.format(env_name, ins.private_ip_address),
'host': host,
'hostname': ins.private_ip_address,
'roles': ['base']
})
with open('spec/env/hosts2.json', 'w') as f:
f.write(json.dumps(servers, indent=2))
puts('Completed')
以下で spec/env/servers.json
が生成される
fab generate_spec_servers