LoginSignup
13
13

More than 5 years have passed since last update.

serverspec で Elastic Beanstalk をテストする

Posted at

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

やりたいことの書き方は公式のここを見るとわかりやすい。

serverspec - Resource Types

その他の 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 を少しアレンジしただけ。

serverspec - Advanced Tips

初期化コマンドで生成した spec/example.comspec/base に変更して、 Rakefilespec/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

参考

13
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
13