Test KitchenではじめるChef入門


# 目的

Test Kitchenをつかって[Chef](https://www.chef.io/chef/)へ入門します。

Chefを使うとサーバの設定を自動化することができます。

Test KitchenはChefで利用するクックブックの統合テストを行うことができます。

Chefに入門する環境として初めからTest Kitchenを使うのが良いと感じたのでこの文書を作成しました。

関連: [LT駆動開発10でTest KitchenではじめるChef入門という話をした。 - そんなこと覚えてない](http://blog.eiel.info/blog/2015/01/10/chef-abc-on-test-kitchen/)


# 環境構築

## Chef DKのインストール

Chef DKはChefのクックブック開発する上で便利なものがまとめられています。
DKはDevelopment Kitの略です。
今回利用するTest KitchenはChef DKに含まれています。

Chef DKは https://downloads.chef.io/chef-dk/ よりダウンロードできます。
brewcaskを使用しているのであれば

```
$ brew cask install chefdk
```

でインストールすることが可能です。

```動作確認
$ chef -v
Chef Development Kit Version: 0.3.5
$ which chef
/usr/bin/chef

$ kitchen -v
Test Kitchen version 1.2.1
$ which kitchen
/usr/bin/kitchen
```

補足ですが、`/usr`にインストールされて気持ち悪いかもしれませんが、実体は`/opt/chefdk`にインストールされます。

```
$ ls -ll /usr/bin/chef
lrwxr-xr-x  1 root  wheel  20  1  3 14:22 /usr/bin/chef -> /opt/chefdk/bin/chef
```

## vagrantのインストール

Test Kitchenのdriverはデフォルトで[Vagrant](https://www.vagrantup.com/)を利用します。
つまり、普通に使うとVagrantも使うことになります。

Vagrantのダウンロード
https://www.vagrantup.com/downloads.html

brewcaskを使用しているのであれば

```
$ brew cask install vagrant
```
でもインストールすることが可能です。

```動作確認
$ vagrant -v
Vagrant 1.7.2
```

# プロジェクトの作成

Chefではクックブックという単位で開発していきます。
Chef DKに添付されている`chef`コマンドの`generate`サブコマンドを使用すると雛形が作成できます。
hello-chefというクックブックを作成してそこで作業してみましょう。

```
$ chef generate cookbook hello-chef
$ cd hello-chef
```

hello-chefというクックブックができたので、このディレクトリで作業を進めていきます。

# Test Kitichenの設定

Testk Kitchnの設定は`.kitchen.yml`に書きます。
generateコマンドで自動的に生成されています。

少し変更して、今回は下記のようにしました。

```.kitchen.yml
---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.10

suites:
  - name: default
    run_list: recipe[hello-chef::default]
    attributes:
```

platformのcentosを削って、ubuntuの14.10を指定しました。

# インスタンスの作成

Test Kitchenのインスタンスを作成してみましょう。
chef-clientを実行するための仮想マシンが生成されます。

Chef DKに開発を支援するchefコマンドがあるため、紛らわしくならないように、Chefを実行することを実際にサーバの設定をしてくれるコマンドのchef-clientと呼んでいきます。

インスタンスの作成に`kitchen verify`コマンドを使用してみます。
このコマンドの実行は長いので、じっくり待つことになります。通信環境によりますが、15分かかりました。

```
$ kitchen verify
```

このコマンドを実行すると

1. インスタンスを作成(setup)
2. chef-clientを実行(converge)
3. テストコードの実行(verify)

をしてくれます。
もしインスタンスを作成済みでchef-clientを実行であれば、テストコードの実行のみが実行されます。

また、作成したインスタンスはlistサブコマンドで確認できます。

```
$ kitchen list
Instance             Driver   Provisioner  Last Action
default-ubuntu-1410  Vagrant  ChefZero     Verified
```

Last Actionでどこまで実行されているのか確認することができます。
作成するだけであれば`kitchen setup`コマンドがあります。
今回はなるべく全体像が把握できるように`verify`コマンドを利用しました。

# 作成したインスタンスにログインする

loginサブコマンドで作成したインスタンスにログインできます。
ログインしてchef-clientがインストールされているのかどうか確認してみます。

```
$ kitchen login default-ubuntu-1410
default-ubuntu-1410 $ /opt/chef/bin/chef-client -v
Chef: 12.0.3
```

Chefは`/opt/chef`にインストールされています。

kitchen loginして実行するコマンドの戦闘には`default-ubuntu-1410 $`と記述しておきます。

# chef-applyで遊んでみる

chef-clientは引数などが複雑なので、はじめはchef-applyを使ってChefを体験していきます。

まずはファイルを作成してみましょう。

chef-applyをつかってhello.txtを作成するには以下のコマンドを実行します。

```実行例
default-ubuntu-1410 $ /opt/chef/bin/chef-apply -e 'file "hello.txt" do content "Hello, World\n" end'
```

chefは`/opt/chef`にインストールされていて、環境変数PATHが設定されていないので、フルパスでコマンドを実行しています。

```実行結果
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[hello.txt] action create
    - create new file hello.txt
    - update content in file hello.txt from none to 77df26
    --- hello.txt       2015-01-03 02:34:39.707849381 +0000
    +++ ./.hello.txt20150103-2182-1a8z2qa       2015-01-03 02:34:39.707849381 +0000
    @@ -1 +1,2 @@
    +Hello, World
```

Hello.txtが作成できています。中身を確認してみます。

```
$ cat hello.txt
hello,world
```

# レシピファイルの作成


ワンライナーでファイルを作成しましたが、見通しが悪いのでファイルに書いてみます。

ローカルマシンの`recipe/hello.rb`に書いてみましょう。
このhello.rbをレシピファイルと呼びます。

```hello.rb
file 'hello.txt' do
  content "Hello, World\n"
end
```

`recipe/hello.rb`を仮想マシンにアップロードするには`kitchen convege`コマンドを利用します。

```
$ kitchen converge
```

本来はchef-clientを実行するコマンドですが、クックブックの同期が行なわれるのを利用しています。
このコマンドを実行すると`/tmp/kitchen/cookbooks/hello-chef/recipes`にファイルが転送されています。実行してみましょう。

```
default-ubuntu-1410 $ /opt/chef/bin/chef-apply /tmp/kitchen/cookbooks/hello-chef/recipes/hello.rb
```

```
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[hello.txt] action create (up to date)
```

長いですが、パスを省略してかけば`chef-apply hello.rb`をしているだけです。

変更がないと、面白みがないのでファイルの中身を変更してみましょう。


```recipes/hello.rb
file 'hello.txt' do
  content "Hello, Chef\n"
end
```

contentに指定した文字列がファイルの中身になります。
さて、レシピファイルを転送しましょう。

```
$ kitchen converge
```

レシピファイルを実行します。

```
default-ubuntu-1410 $ /opt/chef/bin/chef-apply /tmp/kitchen/cookbooks/hello-chef/recipes/hello.rb
```

```実行結果
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[hello.txt] action create
    - update content in file hello.txt from 8663ba to c634c3
    --- hello.txt       2015-01-03 06:59:34.007560036 +0000
    +++ ./.hello.txt20150103-5116-1bfkldm       2015-01-03 07:07:02.527169431 +0000
    @@ -1,2 +1,2 @@
    -Hello, World
    +Hello, Chef
```

実行できました。中身が`Hello, Chef`に変更されています。

ところで、hello.rbはhello-chefクックブックのレシピなります。
このレシピを指すのに`recipe[hello-chef::hello]`と表現することができます。
省略して`hello-chef::hello`レシピと呼ぶことがあります。

# ランリストの設定

`converge`サブコマンドを実行した際に実行するレシピを設定することができます。
この設定は`.kitche.yml`に設定します。
「実行するレシピの一覧」をランリストと呼びます。

`.kitchen.yml`に`run_list`という項目があります。


```.kitchen.ymlより抜粋
    run_list:
      - recipe[hello-chef::default]
```

`hello-chef::default`が設定されています。
`hello-chef::hello`も加えてみます。

```.kitchen.yml変更後
    run_list:
      - recipe[hello-chef::default]
      - recipe[hello-chef::hello]
```

変化がわかるように`hello.rb`も書き換えておきましょう。

```recipes/hello.rb
file 'hello.txt' do
  content "Hello, Cookbook\n"
end
```

変更したら`converge`サブコマンドを実行してみます。

```
$ kitchen converge
```

hello.txtを確認してみます。

```
default-ubuntu-1410 $ cat hello.txt
Hello, Chef
```

変化していません。
ktichen convergeコマンドを使用した際には`/`ディレクトリが基準になるので作成したファイルは`/hello.txt`になります。

```実行例
default-ubuntu-1410 $ cat /hello.txt
Hello, Cookbook
```

仮想マシン上でレシピを実行したい場合は、下記コマンドを実行します。

```
$ sudo chef-client -z -j /tmp/kitchen/dna.json -c /tmp/kitchen/client.rb
```

何が起きているのか詳細を知りたい場合は、各ファイルの中身やオプションの意味を調べてみてください。

蛇足ですが、Chefの入門が難しく感じるのは、初めて挑戦する時に指定するオプションが多いのが原因な気がします。


# テストコードを書いてみる

Test Kitchenは統合テストを行うツールなので、テストコードを記述するとテストを実行することができます。
折角なので書いてみましょう。

今回はServerspecを使います。

`test/integration/default/serverspec`ディレクトリにhello_spec.rbを作成してみます。
ファイルを作成する位置に意味があるので、同じファイル名にすることをおすすめします。

```
$ mkdir -p test/integration/default/serverspec
```

```test/integration/default/serverspec/hello_spec.rb
require 'serverspec'

set :backend, :exec

describe 'Hello Chef' do
  describe file('/hello.txt') do
    it { should contain('Cookbook') }
  end
end
```

`/hello.txt`の中に「Cookbook」という文字列が含まれているか確認するテストを書きました。

Serverspecをここまで利用していなかったので、仮想マシンがServerspecを使えるように設定されていません。
`setup`サブコマンドを実行すると必要なものがインストールできます。

```
$ kitchen setup
```

`verify`サブコマンドを実行した際に一度`setup`サブコマンドは自動で実行されていますが、その際はServerspecが不要だったためインストールされていませんでした。明示的に`kitchen setup`コマンドを呼ぶことで再度セットアップすることができます。

テストを実行するには`verify`サブコマンドを実行します。

```
$ kitchen verify
```

```実行結果
Hello Chef
  File "/hello.txt"
           should be file

       Finished in 0.09 seconds (files took 0.27225 seconds to load)
       1 example, 0 failures
       Finished verifying <default-ubuntu-1410> (0m1.99s).
```

# Attributesを使う

レシピファイルの中で変数を使いたいことがあります。
変数のように値を差し替えるような機能があるとレシピを変更せずに作成するファイルの内容を調整することができるようになります。
この機能がAttributesです。

Test Kittchenでは`.kitchen.yml`ファイルでattributeを設定しておくことができます。`message`という値を設定してみます。

```.kitchen.yml抜粋
    attributes:
      message: "World!!"
```

レシピファイルの中でmessageの値を利用するには`node.message`になります。
実際に使ってみます。

```recipes/hello.rb
file 'hello.txt' do
  content "Hello, #{node.message}\n"
end
```

レシピを実行します。

```
$ kitchen converge
```

テストを実行してみます。

```
$ kitchen verify
```

```実行結果(抜粋)
Hello Chef
  File "/hello.txt"
    should contain "Cookbook" (FAILED - 1)

Failures:

  1) Hello Chef File "/hello.txt" should contain "Cookbook"
     Failure/Error: it { should contain('Cookbook') }
       expected File "/hello.txt" to contain "Cookbook"
       /bin/sh -c grep\ -qs\ --\ Cookbook\ /hello.txt\ \|\|\ grep\ -qFs\ --\ Coo
kbook\ /hello.txt

     # /tmp/busser/suites/serverspec/hello_spec.rb:7:in `block (3 levels) in <to
p (required)>'

Finished in 0.10098 seconds (files took 0.28348 seconds to load)
1 example, 1 failure
```


`/hello.txt`の中身が変更されてテストが失敗します。
ファイルに「Cookbook」という文字がなくなったためです。
hello_spec.rb`を修正しておきます。

```test/integration/default/serverspec/hello_spec.rb
require 'serverspec'

set :backend, :exec

describe 'Hello Chef' do
  describe file('/hello.txt') do
    it { should contain('World') }
  end
end
```

```
$ kitchen verify
```

なれてきたらテストファーストでやってみると良いと思います。

# 通しでテストする

仮想マシンをいろいろ操作した上でテストを実行しています。
仮想マシンを起動してレシピを実行するだけで本当にテストが成功するのでしょうか？
`test`サブコマンドを利用すると一旦すべて破棄して仮想マシンをつくるとこらか確認してくれます。

```
$ kitchen test
```

終了したら仮想マシンが削除されてしまいます。
残したい場合は`--destroy=never`オブションを使います。

より詳細は、[test-kitchenのつかいかた - Qiita](http://qiita.com/sawanoboly/items/9f560bd63ad0712b17ba)を参照してください。

# 補足


## インスタンスが停止しているとき

「マシンを再起動しちゃってインスタンスが止まちゃってるよ！」なんてことがあると思います。
`.kitchen/kitchen-vagrant/default-ubuntu-1410/`にVagrantfileがあるので、

```
$ cd .kitchen/kitchen-vagrant/default-ubuntu-1410/
$ vagrant up
```

とすれば大丈夫です。

すっきりさせたい場合は`kitchen destroy`してしまう荒技もあります。もちろんsetupからやりなおすことになります。

# まとめ

Chefに挑戦する際にTest Kitchenを使うと仮想マシンのセットアップをしてくれるのでChef入門しやすくなるのではないかと思って書いてみました。
しかも、Test Kitchenを使ってChefを始めるとテストコードを書く環境も整っています。

レシピに書いた`file`などのサーバに変化を与える命令はResoucesと言います。
その他の命令を確認したい場合は[Resources and Providers Reference — Chef Single-page Topics](http://docs.chef.io/chef/resources.html)を見てください。

Chefを使っていく上で知っておいたほうが良いことは他にもありますが、まずは最初の敷居を下げることができたら良いと思います。

作成したクックブックを実際のサーバにknife zeroで適用するところも書きたいですが長くなりそうなので持ち越します。


# 参考文献

* [Learn Chef](https://learn.chef.io/)
* [Ruby - chef-solo,chef-shellより更にミニマムなchef-apply - Qiita](http://qiita.com/sawanoboly/items/770fe24058fab5ab613d)
* [Introduction - KitchenCI](http://kitchen.ci/docs/getting-started/)
* [Serverspec - Resource Types](http://serverspec.org/resource_types.html)
* [Resources and Providers Reference — Chef Single-page Topics](http://docs.chef.io/chef/resources.html)