Test Kitchen + ServerspecでChefのAttributesを参照する


この記事は[Sansan Advent Calendar 2015](http://qiita.com/advent-calendar/2015/sansan)の14日目です。

何書こうかなと考えたのですが、新鮮なネタを持ちあわせておらず、今年のAdvent Calenderで一切話題が無かったChefについて書きます。

Test Kitchen + Serverspec(busser-serverspec) でChefレシピのテストを書いているのですが、ChefのAttributesによってテストケースが変わる場合にとても困っていました。

例えば、ユーザを作成するレシピとそのテストを書きたいとします。
※ パスワードは今回設定しないとします。

```users/attriutes/default.rb
default["users"]["username"] = "foo"
default["users"]["home_dir"] = "/home/foo"
```

```users/recipe/default.rb
user "#{node['user']['username']}" do
  home     "#{node['users']['home_dir']}"
  supports :manage_home => true
  action   :create
end
```

```users_spec.rb
require 'spec_helper'

describe user("foo") do
  it { should exist }
  it { should have_home_directory "/home/foo" }
end

describe file("/home/foo") do
  it { should be_directory }
  it { should be_owned_by "foo" }
  it { should be_grouped_into "foo" }
end
```

Serverspecだと例のように直接ユーザ名とホームディレクトリを指定してテストを書くことになりますが、もし、サーバ(Node)やRoles,Environmentsで、`override_attributes`等でAttributesが変わったり上書きされている場合だとそのままではテストが通りません。

調べてみると、`.kitchen.yml`で設定すれば良いという記事をよく見かけますが、多くのレシピを作っている場合、すべてのAttributesを`.kitchen.yml`に書くのはとても大変です。また、Attributesを変更する度に`.kitchen.yml`も変更しなければならないため、変更漏れがあるとテストが落ちて面倒です。

そこでやっと本題に入りますが、ServerspecからChefのAttributesを参照することでテストを柔軟に書けるようにしたいと思います。

## ディレクトリ構成

```
chef-repo
├── .kitchen.yml
├── site-cookbooks
├── environments
├── nodes
├── roles
├── data_bags
└── test
    └── integration
        ├── default
        │   └── serverspec
        │       └── default_spec.rb
        ├── helpers
        │   └── serverspec
        │       └── spec_helper.rb
        └── shared
            └── users
                └── default.rb
```

`test`ディレクトリがメインなのでそれ以外は端折っています。
`test/integration/shared`ディレクトリ以下には各Cookbookに書いたレシピに対応したテストを置きます。
`test/integration/default`は後述するテストスイート名です。

## 各種設定ファイル

```.kitchen.yml
driver:
  name: docker_cli

transport:
  name: docker_cli

provisioner:
  name: chef_zero
  data_path: test/integration/shared

platforms:
  - name: centos-7.1
    driver_config:
      username: root
      image: centos:centos7
      privileged: true
      command: /sbin/init

suites:
  - name: default
    run_list:
      - recipe[users::default]
    attributes:
```

このファイルで重要な設定は2つです。プロバイダの`name`と`data_path`です。

プロバイダはChef-Zeroにします(Chef-Soloではない)。
Chef-Zeroというのは雑な説明をすると、ローカルで動かす簡易的なChef Serverです(合ってる？)。
後述しますが、Chef-Zeroにすることが今回のキモとなります。
また、`data_path`は作成したServerspecファイルのパスを指定します。

ドライバは何でも良いですが、[Docker_CLI](https://github.com/marcy-terui/kitchen-docker_cli)にしました。普通のDockerドライバでも良いのですが、SSHポート開ける必要がないのでこちらの方が楽かなと思ってこちらにしました。

```helper/serverspec/spec_helper.rb
require 'serverspec'
require 'json'

# ※1
base_spec_dir = Pathname.new(File.expand_path('../../../../kitchen/data', __FILE__))
Dir[base_spec_dir.join('./**/*.rb')].sort.each{ |f| require f }

set :backend, :exec

# ※2
attributes = {}
node_dir = File.expand_path('../../../../kitchen/nodes',  __FILE__)
Dir.glob(File.join(node_dir, '*.json')) do |json_file|
  node = JSON.parse(File.read(json_file))
  attributes = node["default"]
  %W{normal override}.each do |priority|
    if node[priority].nil? then
      next
    end
    node[priority].each do |key, attr|
      if ! attributes[key].nil? then
        if attr.kind_of?(Array) then
          attributes.merge!({ key => attr })
        else
          attributes[key].merge!(attr)
        end
      else
        attributes.merge!({ key => attr })
      end
    end
  end
  # ※3
  attributes["roles"] = node["automatic"]["roles"]
  attributes["chef_environment"] = node["chef_environment"]
  next
end
set_property attributes
```

このヘルパーファイルでは後半(※2)が重要です。
Test Kitchenを実行することで、Dockerコンテナ上で以下のようなファイルが置かれます。

```bash
# Chefレシピなどのファイル郡
/tmp/kitchen/
├── site-cookbooks
│   └── users
│       ├── attributes
│       │   └── default.rb
│       ├── metadata.rb
│       ├── README.md
│       └── recipes
│           └── default.rb
├── data
│   └── users
│       └── default.rb
├── environments
├── nodes
│   └── default-centos-71.json
└── roles

# Serverspec(busser-serverspec)のファイル郡
/tmp/verifier/
├── bin
├── gems
└── suites
    └── serverspec
        ├── default_spec.rb
        └── spec_helper.rb
```

Chef-Zeroプロバイダで実行することで、`/tmp/kitchen/nodes/<node>.json`にChef実行したノード情報が入っているので、`set_property`として読み込ませることでServerspecから参照できるようにしています。`override_attributes`については同じ属性があった場合は上書きし、同じ属性がない場合はプロパティに追加しています。　また、RoleとEnvironmentもプロパティに入れることもできます(※3)。
ちなみに、`.kitchen.yml`の`data_path`で指定したファイル郡は`/tmp/kitchen/`以下に置かれますので、ヘルパーファイルの前半(※1)で読み込ませています。

あとはテストは以下のように修正して`$ kitchen test`を実行すればテストが通るはずです。

```test/integration/default/default.rb
require 'spec_helper'

describe 'default' do
  include_examples "users::default"
end
```

```test/integration/shared/users/default.rb
shared_examples 'users::default' do

  describe user("#{property['users']['username']}") do
    it { should exist }
    it { should have_home_directory "#{property['users']['home_dir']}" }
  end

  describe file("#{property['users']['home_dir']}") do
    it { should be_directory }
    it { should be_owned_by "#{property['users']['username']}" }
    it { should be_grouped_into "#{property['users']['username']}" }
  end

end
```

## 最後に

あまり綺麗な形ではないかもしれませんが、Serverspec(+Test Kitchen)からChefのAttributesを参照できるようにしました。
もっと上手い方法があればぜひコメントしていただければ幸いです。

本来はTest Kitchen(busser-serverspec)が機能として追加してくれれば一番いいんですけどね･･･。