Serverspecでサーバの構成をテストする 導入と個人的知見

## はじめに

最近肩こりが辛いんですが、肩こりのラスボスみたいな感じの痛みで夏の終わりを感じています。

今回はServerspecについて書きます。

Serverspecはサーバの状態をテストするツールです。
ServerspecはRuby(Rails)で有名なテストフレームワークであるRSpecと
サーバを抽象化するライブラリであるserverinfraを合わせたツールです。

## Good

* Puppetのようにサーバが「あるべき姿」をコード化することによってサーバの状態を定義することができる(仕様書のようになる)
* プログラミングの手法であるTDD(テスト駆動開発)的にサーバの構築ができる
* 定義した際の状態が不変であれば、障害時普及の手助けになる

他にも使い方によっては様々な利点がありそうです。

## 環境

* windows 7 64bit
* ruby 2.2.1p85 (2015-02-26 revision 49769) [i386-mingw32]
* serverspec (2.21.0)
* rspec (3.3.0)
* リモートのサーバ: CentOS 7.1

※ windowsのコンソールは cygwin + nyaos + ckwmod です

## インストール

早速 serverspecをインストールします
※ Rubyのインストールは割愛します。

gemライブラリで一発です。

```
$ gem install serverspec
```

もろもろの依存するライブラリが合わせてインストールされます。
これで終了です。

## 早速使用する

今回はリモートのCentOS 7サーバでapacheが動いているかを確認します。これはデフォルトのサンプルで入っている設定です。
まずは 下記コマンドを叩きます。
※任意のディレクトリを作成しその中での作業をおすすめします。
※プログラムが無いと言われる場合は環境変数のパスを確認して下さい。

```
$ serverspec-init

# 以下対話的な入力

Select OS type:

  1) UN*X
  2) Windows

# と表示されます。今回は UN*Xなので 1 を入力

Select number: 1

Select a backend type:

  1) SSH
  2) Exec (local)

# リモート接続のため 1 を選択

Select number: 1

# vagrantかどうか聞かれるので今回は n を選択

Vagrant instance y/n: n

#テストするサーバのホスト名を入力

Input target host name: www.example.com
```

ここまで入力を行うと初期設定用に下記のファイルが作成されます

 + spec/
 + spec/www.example.com/
 + spec/www.example.com/sample_spec.rb
 + spec/spec_helper.rb
 + Rakefile
 + .rspec

### ファイルの説明

* spec/www.example.com/sample_spec.rb
    * 実際にテストする内容を記載するファイル
    * 同ディレクトリに *_spec.rb というファイル名で別ファイルを作成すればそちらもテストされます
* spec/spec_helper.rb
    * テストに関する全般的な設定ファイルです。
* Rakefile
    * テスト実行のためのタスク定義ファイルです
* .rspec
    * RSpecの挙動を設定するファイルです

### 実行

```
$ rake spec:www.example.com # ドメインは各自のものに読み替えてください
```

もしも

```
Please set sudo password to Specinfra.configuration.sudo_password.
```

と出て実行できない場合は下記のコマンドを実行してください。

```
$ rake spec:www.example.com ASK_SUDO_PASSWORD=1
```

ASK_SUDO_PASSWORD=1 は sudoのパスワードを対話的に入力しその後実行します。

うまく行けば下記のような感じになると思います。

```
$ rake spec:jazz.workapart.org ASK_SUDO_PASSWORD=1
Enter sudo password:

Package "httpd"
  should be installed

Service "httpd"
  should be enabled
  should be running

Port "80"
  should be listening

Finished in 1.48 seconds (files took 6.75 seconds to load)
4 examples, 0 failures
``` 

うまくいきました。

## 他

### 鍵認証あたりでうまく行かない場合

~/.ssh/config に設定をする必要があるかもしれません。
下記記事の SSH認証のところが参考になります。

<a href="http://hidemium.hatenablog.com/entry/2014/05/04/040051">http://hidemium.hatenablog.com/entry/2014/05/04/040051</a>

### テストに便利なリソースタイプ

puppetの用にテスト項目を設定する上で便利なリソースタイプの一覧が有ります。
下記を組み合わせていくと簡単な事は直感的に結構書けそうな感じです。

<a href="http://serverspec.org/resource_types.html">http://serverspec.org/resource_types.html</a>

# 個人的知見

個人で使いたい用途があったため、躓いたところやserverspecと直接関係ない部分でも
さまよった証を残します。

## 要件

概ねですが、テストを作成する前に以下の要件を満たすようにできたらよいなと考えていました。

* レンタルサーバサービスでのテスト
* 区画作成後に出来た区画が正常に作成されているかをテストする
* 全体に対して同様のテストを行う
* 区画情報の他にプロセスの確認なども行う
* 本番サーバとステージサーバのテストをタスクで実行し分ける

概ねこれらに関しては、Serverspecにてテストが可能でしたが
一点だけ問題があり全体テストでの採用を見送りました（後述）

## 要件に合わせた対応と細かいtips

### 単純な確認

### サービスが起動しているか

service のリソースを使うことで対応可能です。

```
describe service('nginx-1.8.0') do
  it { should be_running }
end
```

### fileの存在確認

Directoryも同様ですが下記の記述で確認が可能です。

```
Fileの場合
describe file("/var/path/filename") do
  it { should be_file }                    # ファイルかどうか
  it { should be_owned_by name }           # ファイルのオーナーがnameと同じか
  it { should be_grouped_into groupname }  # ファイルのグループがgroupnameと同じか
  it { should be_mode 600}                 # ファイルのmodeが 600 か
end

Directoryの場合
describe file("/var/path/directory/") do
  it { should be_directory }           # directoryかどうか
  it { should be_owned_by 'root'}      # オーナーがrootか
  it { should be_grouped_into 'root'}  # グループがrootか

  # 中でif文を書くことも出来ます
  if dir1 == 'conf'
    it { should be_mode 700}
  else
    it { should be_mode 755}
  end
end
```

同じ内容のテストを行う場合は配列にして回すと記述を減らすことが出来ます。

```
files = [file1, file2, file3]
files.each do |file|
  describe file("/var/path/filename/#{file}") do
    it { should be_file }                    
    it { should be_owned_by name }           
    it { should be_grouped_into groupname }  
    it { should be_mode 600}                 
  end
end
```

その他基本的なリソースは公式サイトに紹介されているのでそちらを参照されるととてもよいです。他には 私が多用したのが file directory service interface command でした。

### 悩ましかった部分

#### ldapの値を取り出して確認

ldapsearch コマンドをcommandリソースに与えてgrepとawkしました

```
情報が足りないですが簡単な例 ※環境に合わせて変更してください。
ldapsearch_command = "ldapsearch -x -h 127.0.0.1 -b "認証に必要な情報" -w #{password}"

describe command("#{ldapsearch_command} \"uid=idname\"  |grep 'ftpUID:'|awk '{print $抜き取る場所}'") do
  its(:exit_status) {should match eq 0} 
  its(:stdout) {should match 検証する文字列}
end

```

mysqlもpostgresもほぼ同様にコマンドを投げて帰ってきた値を切り抜いてSTDOUTで比較していますが、postgresだけはmysqlのように認証をエコーしてログインということが出来なかったので一時的に環境変数にpostgresのログイン情報を設定して情報を得てserverspecが実行し終わった後にshellを抜けているような挙動にみえたので、そのタイミングで環境変数を消すというふうに設定しました。

### spec_helper.rbの変更

#### YAMLから設定を読み込む

一つの区画ではなく多数の区画を確認する必要があったので、テストのソースではないところで情報を管理しておく必要が有りました。
また、テストは同じ設定のため同じソースを違う設定で回す必要も有りました。
そのため読み込んだ設定を

```
configs.each do |config|
  describe xxx do
  end
  describe xxx do
  end
  .
  .
  .
end
```

のように回していく必要があります。

そしてそもそもserverspecのテストはリモートのサーバにて実行されているので、specファイルの中に記載をしてしまうとローカルのymlファイルを読めないため、spec実行前に変数とかにして渡し実行する必要が有ります。
これは、spec_helperの中に set_propertyを記述することで変数等を持ち込んで実行が可能です。

```
properties = YAML.load_file(File.join('.', 'filepath', "configs.yml"))
set_property properties
```

specの方では property という名前で値を取り出すことが出来ます。

#### sudoパスワードの記述を強制

sudoがテスト項目で必須なため
if文で分岐しているあたりを下記のようにして実行時に必ず聞かれるように設定しました。

```
require 'highline/import'
set :sudo_password, ask("Enter sudo password: ") { |q| q.echo = false }
```

#### 一時的にsudoでは実行しないことがある場合

spec_helperに以下を記載します。

```
RSpec.configure do |c|
  c.around :each, :sudo => false do |example|
    set :disable_sudo, true
    example.run
    set :disable_sudo, false
  end
end
```

specファイルには以下のように :sudo => false を記載することで、sudoではない権限で実行します。

```
describe command(実行コマンド), :sudo => false do
  its(:exit_status) {should match eq 0} 
end
```

### 同内容のテストをタスクで実行し分ける

通常serverspec-initを実行するとhost名のディレクトリができそのディレクトリの名前を接続先として設定して実行します。通常であればその方法で問題ないのですが、windows環境なためシンボリックリンクで実行もまた微妙で以下のようにディレクトリとrakeタスクを変更しました。

ディレクトリ
<pre>
/
│  .gitignore
│  .rspec
│  Gemfile
│  Gemfile.lock
│  Rakefile
│
└─spec
    │  spec_helper.rb
    │
    ├─lib
    │      sample1_spec.rb
    │      sample2_spec.rb
    │      sample3_spec.rb
    │
    └─yml
            host1.example.com.yml
            host2.example.com.yml
</pre>


それに対するrakeタスクが以下です。

```
require 'rake'
require 'rspec/core/rake_task'

#### Hosting Servers
hosts = [
  'host1.example.com',
  'host2.example.com',
]

namespace :spec do
  task hosts.map {|h| 'spec:' + h.split('.')[0] }
  hosts.each do |host|
    short_name = host.split('.')[0]

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

### 全体区画テストの見送り(解決せず)

今回作成したメインのspecファイルは

* 280行 と 33describeがある
* 1ホストあたり約100区画がこのテストを通る
* 1区画で実行すると30秒程度かかる
* 場合によって頻繁にテストが走る可能性がある。

というものになっており、本来のサーバの構成をテストするというものよりも少々細かく数が多いものとなったため、おそらく推測で30分程度テストがかかる可能性がありました。
設定をまとめてリモートから取得してローカルで比較するというものになっていないのでこの時間がかかるものと推測されます。sshやコマンドが何回も繰り返されるぶん無駄が発生していると考えているのですが、今回はそこを対応するまでの時間がなかったため単体に対して行う方向で考えています。

## 最後に

全体テストこそ見送ったものの、serverspecに関しては個人的に以下の良さがあると思いました。

* rubyの知識が少なくてもテストが書きやすい
* 実行結果がrspecのおかげで把握しやすい
* 簡単なものであればresourceを駆使することで簡単に作れる
* サーバの構成をテストできる => そして安心できる

まだサーバの構築の際にTDD的利用は私自身行っていませんが、
サーバの構築に新しい価値観がもたらされるとてもよいツールだと感じました。