ChefDKハンズオン with すごい広島の資料


以下は[ChefDKハンズオン with すごい広島](https://hiroshimarb.doorkeeper.jp/events/20979)用の資料です。

# 準備

本ハンズオンはTest Kitchenのdriverにdocker_cliを利用します。
準備物としてダウンロードをするものが多いので事前に準備をしておくとよりスムーズに進みます。

## MacOSX

* ChefDK 0.4.0
* Virtualbox
* Boot2docker 1.5.0

[Homebrew Cask](http://caskroom.io/)をインストールしていれば簡単です。

```
$ brew cask update
$ brew cask install chefdk virtualbox boot2docker
```

* https://downloads.chef.io/chef-dk/mac/#/
* https://docs.docker.com/installation/mac/
* https://www.virtualbox.org/wiki/Downloads

## Windows

サポートできない恐れがあります。
仮想環境でDockerの動作するLinux環境を用意しておくと安全です。

* ChefDK 0.4.0
* Virtualbox
* Boot2docker 1.5.0

各ファイルは下記よりダウンロードしてください。

* https://downloads.chef.io/chef-dk/windows/#/
* https://docs.docker.com/installation/windows/
* https://www.virtualbox.org/wiki/Downloads

## Linux

* ChefDK 0.4.0
* docker 1.5.0

多様なのでUbuntuのものを代わりに貼っておきます。

* https://downloads.chef.io/chef-dk/ubuntu/#/
* https://docs.docker.com/installation/ubuntulinux/

Debianで試した時の環境設定メモもあります。

* http://qiita.com/eielh/items/53025a09effec9bf5d62

## その他インストールしておくと良いかもしれないもの

* vagrant
* git

Test Kitchenのデフォルトのdriverはvagrantなので、vagrant使用したい場合はvagrantをインストールしてください。最後にvagrantのプロビジョンを行う例もあります。
gitはなくても良いですが、各時点でコミットしておくと変化がよくわかって便利です。

## 共通

今回はubunutu-14.04を使っています。
事前にイメージをダウンロードしておくとスムーズに参加できます。

Test Kitchenでは複数のOSを同時にテストすることもできます。
好みに応じてダウンロードしてください。

```
$ docker pull ubuntu:14.04
$ docker pull centos:6.6
```

# ChefDKに付いているもの

ChefDKはChef Development Kitの略で、RubyとChefとCookbookの開発に使えるサブツールで構成されています。

* https://downloads.chef.io/chef-dk/

今回はVersion0.4.0を想定しています。

添付されているツールは以下の通りです。

* chef
* chefコマンド ChefDKのためのコマンド。ジェネレータの実行やChefDKに添付されているRubyを操作したりするのに使う。
* Test Kitchien サーバの統合テストのためのツール。Chef以外でも使える
* ChefSpec Chefのレシピを単体テストするためのツール
* Foodcritic Chefのレシピの書き方をチェックしてくれる
* Berkshelf cookbookの依存関係を解決してくれるツール。bundlerみたいなもの。

# 目標

今回の目標は以下の通りです。

* ChefDKに添付されているツールを一通り使う
* Webサーバを立てる

特にTest KitchenはChef以外の構成管理ツールでも使えて有用です。

# ChefDK添付のRubyを使う

ChefDKにはRubyが添付されています。
このRubyを利用するようにシェルを設定します。

```
$ eval "$(chef shell-init `basename $SHELL`)"
```

Powershellの場合は以下を使用します。

```
$ chef shell-init powershell | Invoke-Expression
```

rubyコマンドがchefdkに添付されているものになっているはずです。

```
$ which ruby
/opt/chefdk/embedded/bin/ruby
```

## chefコマンドについて

chefコマンドはChefDKに添付されているコマンドです。
Chef本体に添付されているものではありません。
引数なしでchefコマンドを実行するとヘルプを見ることができます。

ChefDKに添付されているRubyに対して操作ができたり、雛形を生成するgeneraterを実行できたりします。


# クックブックの作成

ChefではCookbookという単位で開発していきます。Rubygemsで言うgemがcookbookにあたります。
今回はmywebというクックブックを作成します。

```
$ chef generate cookbook myweb
$ cd myweb
```

chefコマンドにはgenerateサブコマンドがあり、cookbookの雛形を生成できます。
cookbookだけでなくrecipeやtemplateなども雛形が作成できます。

cookbookを生成すると下記のようなディレクトリ構成になります。

```
.
├── .gitignore
├── .kitchen.yml
├── Berksfile
├── README.md
├── chefignore
├── metadata.rb
├── recipes
│   └── default.rb
├── spec
│   ├── spec_helper.rb
│   └── unit
│       └── recipes
│           └── default_spec.rb
└── test
    └── integration
        └── default
            └── serverspec
                ├── default_spec.rb
                └── spec_helper.rb
```

metadata.rbにはcookbookのメタ情報を記述します。

chefignoreにはcookbookを配布する際に含めないファイルを記述します。

.kitchen.ymlはTest Kitchenの設定ファイルです。

他にもレシピファイル、インテグレーションテストのファイルやユニットテストファイルが生成されています。

git initされている状態なのでコミットをしておきましょう。

```
$ git add .
$ git commit -m 'initial commit'
```

# Dockerの動作確認

今回はDockerの中でChefを実行します。
Vagrantを使うと時間がかかってしまうので、今回はDockerを使うことにしました。

では、Dockerが使えることを確認しておきましょう。

```
$ docker ps
CONTAINER ID        IMAGE               COMMAND             CREATED         STATUS              PORTS               NAMES
```

正常に動作していればこのような出力になります。

boot2dockerを使用していて、動作していないのであれば、boot2dockerのstatusを確認しておきましょう。

```
$ boot2docker status
running
```

boot2dockerが動作しているのに失敗する場合は環境変数の設定が漏れている恐れがあります。

```
$ $(boot2docker shellinit)
```

で、そのシェルだけは一時的に環境変数を設定できます。

# Test Kitchenの設定

折角なので、テスト駆動開発風に進めていきます。
まず、統合テストをしましょう。

統合テストをするにはTest Kitchenを利用します。
Test Kitchenがchefを実行するマシンを作成し、ServerSpecなどで記述したテストを実行してくれます。

Test Kitchenの設定は`.kitchen.yml`に記述します。
`.kitchen.yml`を少し書き換えます。

```.kitchen.yml
---
driver:
  name: docker_cli
  require_chef_omnibus: false
  run_command:
    - curl -L https://www.chef.io/chef/install.sh | bash
  publish:
    - 8100:80
#  no_cache: true

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04
#  - name: centos-6.6

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

今回はdriverにはdocker_cliを使います。
driverをインストールする必要があります。

```
$ bundle init
$ echo "gem 'kitchen-docker_cli'" >> Gemfile
$ bundle install
```

driverにはデフォルトでvagrantが使われます。
AWSのec2やGoogle Cloud Platformのgceなども使うこともできます。
production環境になるべく近い環境を使うのがオススメです。

問題がないようであればコミットしておきます。

```
$ git add .
$ git commit -m 'setup .kitchen.yml'
```

`.kitchen.yml`の中身について補足します。

`require_chef_omnibus`はchefを自動でインストールするかどうかです。
`false`にして`run_command`で手動でインストールしているため`false`にしています。
run_commandはインスタンスを作成する際の追加コマンドを記述できます。

今回このようにしているのはDockerのキャッシュが効いて、一回のサイクルが短かく済むからです。

また`no_cache`を`true` にするとdockerのキャッシュを使わないようにもできます。
最初から作りなおしたいときに利用してください。

`publish`は、今回webサーバを立てるので、80番ポートを8100でアクセスできるようにしています。

Test Kitchenを使うと同時に複数のOSのテストができます。
今回はひとつだけにしています。
同時にやってみたい場合はcentosのコメントアウトを外してみてください。

provisionerにはchef-zeroが指定されています。
他のprovisonerを使用することもできます。
SaltStackやAnsibleも使用することができるそうです。

# Test Kitchenを使う

Test Kitchenを使用してみましょう。

テストを実行するには下記のtestサブコマンドを使用します。
実行に時間がかかる場合もあります。

```
$ kitchen test default-ubuntu-1404
```

第２引数の`default-ubuntu-1404`はインスタンス名です。
突然登場しましたが、インスタンス名を確認するにはlistサブコマンドを使用します。

```
$ kitchen list
Instance             Driver     Provisioner  Last Action
default-ubuntu-1404  DockerCli  ChefZero     <Not Created>
```

test-kitchenでは「platformsの数」*「suitesの数」だけ同時にテストを行うことができますが、インスタンス名を指定することで、特定のインスタンスだけテストを実行できます。
省略した場合はすべてのインスタンスでテストを実行できます。

インスタンス名はplaftformとsuiteを組み合わせて生成されます。

また、正確には第２引数がマッチするものだけをテストします。

例えば以下のように複数のplatformがあるとします。

```
$ kitchen list
Instance             Driver     Provisioner  Last Action
default-ubuntu-1404  DockerCli  ChefZero     <Not Created>
default-centos-66    DockerCli  ChefZero     <Not Created>
```

ubuntuのみテストしたい場合は

```
$ kitchen test ubuntu
```

とします。何も指定しない場合は両方実行されます。

Test Kitchenのもっと詳しい使い方は

* [test-kitchenのつかいかた](http://qiita.com/sawanoboly/items/9f560bd63ad0712b17ba)

などを読んでみてください。

特に[テスト](http://qiita.com/sawanoboly/items/9f560bd63ad0712b17ba#%E3%83%86%E3%82%B9%E3%83%88)の章は大切です。

# Serverspec

それではServerspecをかいていきましょう。

期待することはポート80でListenしていることですね。
一応、nginxが走っていること、自動で起動することも確認しておきます。

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

describe 'myweb::default' do
  describe service('nginx') do
    it { should be_enabled }
    it { should be_running }
  end

  describe port('80') do
    it { should be_listening.with('tcp') }
  end
end
```

テストの実行は`kitchen test`です。

このテストは失敗します。

出力は長いですがよくみると下記のような出力があると思います。

```
Finished in 0.27516 seconds (files took 0.39046 seconds to load)
3 examples, 3 failures
```

このテストを成功するためにはなにをすればいいでしょうか?

test-kitchenは失敗するとインスタンスが作成されたままで残っています。
`kitchen login`でサーバにログインできます。

テストを通るようにしてみましょう。

```
$ kitchen login
remote$ apt-get -y install nginx
remote$ service nginx start
remote$ exit
```

この状態で再度テストのみを実行してみます。

```
$ kitchen verify
```

成功したでしょうか。

`kitchen test`はインスタンスをつくりなおしてテストを実行しますが、`kitchen verify`はインスタンスはそのままでテストの実行だけできます。

この状態で再度`kitchen test`を実行するとテストは失敗します。
インスタンスが削除されて、nginxのインストールがなかったことになるからです。

# ChefSpec

今度は単体テストフレームワークのChefSpecに挑戦してみましょう。
さきほど手動で行った作業は

* nginxのインストール
* nginxの起動

でした。
ChefSpec化してみましょう。

```spec/unit/recipes/default_spec.rb
require 'spec_helper'

describe 'myweb::default' do
  let(:chef_run) { ChefSpec::SoloRunner.converge(described_recipe) }

  it { expect(chef_run).to install_package('nginx') }
  it { expect(chef_run).to start_service('nginx') }
end
```

`it`の行に注目してください。

* install_package('nginx')
* start_service('nginx')

で、作業していることを確認できます。

このテストの実行にはrspecコマンドを使います。

```
$ rspec
```

ChefSpecの使い方を学ぶにはexampleを見るのがよいです。

* https://github.com/sethvargo/chefspec/tree/master/examples

ChefSpecの良いところはtest-kitchenの実行に比べて早いことです。
chefを実行するよりも早いと思うので文法チェックなどに使えます。
無理して書く必要はないですが、メタプログラミングしたり、リファクタリングする際にあると有効な場合もあります。

# レシピの作成

今度はChefSpecが成功するように実際にレシピを書いていきます。

```recipes/default.rb
package 'nginx'

service 'nginx' do
  action [:start]
end
```

Chefではレシピにリソースを記述して、サーバの設定をします。
packageやserviceはリソースを作成する命令です。
作成するリソースのプロパティを設定する場合は`do`と`end`で囲んだ部分で行います。

`packkage 'nginx'`では`package[nginx]`リソースを作成して、
`service 'nginx'`では`service[nginx]`リソースを`action`が`:start`になるように設定しています。

レシピを作成したら`rspec`を再度実行してみましょう。
テストに成功するはずです。

# test-kitchenでのchefの実行

`kitchen test`が失敗している状態なので、インスタンスが残っています。
そのままのインスタンスを使ってchefを実行したい場合は`kitchen converge`が使えます。

うまくいったらそのままServerspecの実行をしてみましょう。
`kitchen verify`で実行します。

boot2dockerを使用している場合はまずIPを確認します。

```
$ boot2docker ip
192.168.59.103
```

http://192.168.59.103:8100 にアクセスするとウェブサーバが動いていることが確認できると思います。8100なのは`.kitchen.yml`のpublishで設定をしたからです。

![スクリーンショット 2015-02-23 19.30.17.png](https://qiita-image-store.s3.amazonaws.com/0/5840/610693cc-cda3-2599-7441-399467a5198a.png "スクリーンショット 2015-02-23 19.30.17.png")


インスタンスを作りなおしてもうまくいくのか確認してみましょう。
`kitchen test -d never`を使います。

`-d never`を使用するとテストに成功してもインスタンスを残すことができます。
さて、ウェブサーバーが動いているのを確認してみましょう。

うまくいったらコミットしておきましょう。

```
$ git add .
$ git commit -m 'install nginx'
```

# Chefのテンプレートを使う

index.htmlを自分好みのものに変えてみたいと思います。
index.htmlの作成にはテンプレート機能を使ってみます。

テンプレートはcookbookに用意したファイルのテンプレートに変数を埋めてファイルを作成する機能です。

テンプレートの雛形を作成するのにchefコマンドが使えます。

```
$ chef generate template index.html
```

そうすると、`templates/default/index.html.erb`が作成されます。

自由にファイルを作成しましょう。
折角なので変数を参照してみます。

```templates/default/index.html.erb
Hello <%= node.name %>
```

node.nameにはTest Kitchenの

作成したテンプレートのリソースを作る必要があります。
レシピに内容を追加しましょう。

```recipes/default.rb
template '/usr/share/nginx/html/index.html' do
  owner 'www-data'
  group 'www-data'
  mode 644
end
```

Test Kitchenを実行して反映してみましょう。
`kitchen converge`をつかうのが良いでしょう。


実行に成功したら http://192.168.59.103:8100 にアクセスしてみましょう。
作成したhtmlが表示されるでしょうか。

![スクリーンショット 2015-02-23 20.24.38.png](https://qiita-image-store.s3.amazonaws.com/0/5840/3fdc265e-10a5-593b-0b94-4b801b5c3d4b.png "スクリーンショット 2015-02-23 20.24.38.png")


されてませんね。

## Foodcritic

Foodcriticを使うとrecipeの中の良くない書き方をみつけて教えてくれます。

```
$ foodcritic .
FC006: Mode should be quoted or fully specified when setting file permissions: ./recipes/default.rb:13
```

`recipes/default.rb`の13行目に良くない書き方があるようです。
先頭にある`FC006`が問題の種類で、 http://acrmp.github.io/foodcritic/#FC006 を見ると詳しい解説が書いてあります。

modeは文字列で `"644"`とかくか頭に00をつけて`00644`で書けとあります。

```recipes/default.rb
template '/usr/share/nginx/html/index.html' do
  owner 'www-data'
  group 'www-data'
  mode '644'
end
```

と、なおして`kitchen converge`として再度確認してみましょう。


![スクリーンショット 2015-02-23 20.41.59.png](https://qiita-image-store.s3.amazonaws.com/0/5840/aa6bb0bd-940f-8b1a-e57a-da461230d2e8.png "スクリーンショット 2015-02-23 20.41.59.png")

うまくうごいたら、コミットしておきましょう。

```
$ git add .
$ git commit -m 'create default index.html'
```

## node

テンプレートの中で`<%= node.name %>`としました。
nodeの中にはohaiというツールがサーバ情報を取得して保存されています。
`/tmp/kitchen/nodes/`に保存されています。

中身をみてみましょう。

```
$ kitchen login
remote $ cat /tmp/kitchen/nodes/default-ubuntu-1404.json
```

OSの種類、CPUの情報、実行したrun_listの情報、Chefの情報…様々な情報が記録されていることがわかると思います。これの情報はテンプレートやレシピの中でも利用できます。

閲覧したら exit しましょう。

```
remote $ exit
```

# Berkshelf

Berkshelfは他のクックブックを取得するのに使われます。
nginxのコミュニティクックブックを利用してみましょう。

mywebクックブックがnginxクックブックに依存していることをmetadata.rbに記述して上げます。

```metadata.rb
name             'myweb'
maintainer       'The Authors'
maintainer_email 'you@example.com'
license          'all_rights'
description      'Installs/Configures myweb'
long_description 'Installs/Configures myweb'
version          '0.1.0'
depends          'nginx'
```

追加したのは最後の`depends 'nginx'`のみです。

Berkshelfは`Berksfile`に利用するクックブックを記述しますが、
`metadata`と記述してあるため、このmetadata.rbの情報を利用してくれます。

コミュニティクックブックを使えばnginxの設定はいろいろ楽ができます。
ohaiプラグインがはいっていたり、attributesを与えるだけで設定を変更できたりします。

`recipe/default.rb`を書き換えてみます。

packgeリソースとserviceリソースを削除して、nginxクックブックのdefalutレシピを使うように変更してみます。

```recipe/default.rb
include_recipe 'nginx::default'

template '/usr/share/nginx/html/index.html' do
  owner 'www-data'
  group 'www-data'
  mode '644'
end
```

http://192.168.59.103:8100 にアクセスすると403になります。

![スクリーンショット 2015-02-23 20.58.30.png](https://qiita-image-store.s3.amazonaws.com/0/5840/89a106fd-eb70-b142-0e1e-b1d55115cd29.png "スクリーンショット 2015-02-23 20.58.30.png")


コミュニティクックブックによってドキュメントルートが代わってしまっているようです。
attributesを使うことでドキュメントルートを変更することができます。
.kitchen.ymlで変更してみます。


```.kitchen.yml
---
driver:
  name: docker_cli
  require_chef_omnibus: false
  run_command:
    - curl -L https://www.chef.io/chef/install.sh | bash
  publish:
    - 8100:80
#  no_cache: true

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.04
#  - name: centos-6.6

suites:
  - name: default
    run_list:
      - recipe[myweb::default]
    attributes:
      nginx:
        default_root: /usr/share/nginx/html
```

以下が追加されています。

```
      nginx:
        default_root: /usr/share/nginx/html
```

`kitchen converge`を実行して、
http://192.168.59.103:8100 にアクセスするとindex.htmlが表示されるでしょうか。

![スクリーンショット 2015-02-23 20.41.59.png](https://qiita-image-store.s3.amazonaws.com/0/5840/2898f364-9f01-c4ef-f1b6-1bc280d0d938.png "スクリーンショット 2015-02-23 20.41.59.png")

こちらでも稀に見れないことがあったので、もしダメだったら `kitchen test -d never`を試してみてください。

nginxクックブックではどんなattributesが使えるのかなどは下記を参照してください。
必要があればソースコードも見ると勉強になります。

* https://supermarket.chef.io/cookbooks/nginx


そういえば、include_recipeを使うとChefSpecが動いてくれないので、つらいです。
include_recipeは使い方を考えたほうが良いかもしれないとおもってたりします。

# 宿題

## つくったcookbookでvagrantのプロビジョンをしてみよう

例

Vagrantfileを作ります。

```Vagrantfile
# -*- mode: ruby -*-
# vi: set ft=ruby :

Vagrant.configure(2) do |config|
  config.vm.box = "opscode-ubuntu-14.04"
  config.vm.box_url = "http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_ubuntu-14.04_chef-provisionerless.box"
  config.vm.provision "shell", inline: <<-SHELL
    curl -L https://www.chef.io/chef/install.sh | bash
  SHELL
  config.vm.provision "chef_zero" do |chef|
    chef.cookbooks_path = "cookbooks"
    chef.add_recipe "myweb"
    chef.json = {
      "nginx" => {
        "default_root" => "/usr/share/nginx/"
      }
    }
  end
end
```

次にBerksfileをつくります。

```Berksfile
source "https://supermarket.chef.io"

cookbook 'myweb', git: '../myweb' # mywebのディレクトリを指定する
```

```
$ berks
$ berks vendor cookbooks
$ vagrant up
```

attributesをcookbook側で指定しちゃう手もあるにはありますが、もにゅもにゅ。(よりよい方法を調査中)

# ここからの発展

* packerでディスクイメージを作成する
* CIで実行する
* どこかのサーバで使用する

どこかのサーバを使用する場合はknife-zeroやknife-soloといった選択肢もありますし、
cookbookをアップロードしてchef-zeroやchfe-soloのオプションでrun-listやattributesを指定できます。

# 説明してないこと

* chef install
* chef update
* chef push
* chef verify
* chef gem
* chef exec

# まとめ

ChefをつかっているかどうかはさておきTest Kitchenはとても便利です。

* 最初から実行しなおしたい 
  * kitchen test -d never
* Chef(プロビジョナ)をもう一度実行したい
  * kitchen converge
* Serverspecを実行したい
  * kitchen verify
* インスタンスを削除したい
  * kitchen destroy
* インスタンスにログインしたい
  * kitchen login

とりえあず、上記ぐらいを最初は覚えておけば良いと思います。

# 参考文献

* [ChefDKのドキュメント](https://docs.chef.io/#chef-dk-title)
* [GitHub chef/chef-dk](https://github.com/chef/chef-dk)