Chefハンズオン#2 Chef Cookbookのリソースと冪等性

===========================================

[前回 : Chefの環境構築とサンプルレシピの作成・実行](http://qiita.com/mather314/items/9ae1b0ccb5bde271dd38)

前回はサンプルクックブックのレシピを作りました。今回はその続きです。

とりあえず、新しいクックブックで始めましょう。

```
$ knife cookbook create resource-sample -o site-cookbooks
```

## リソース(Resource)とは

レシピ(recipes/以下のRubyスクリプト)はリソースを記述することで構成されます。

前回の実行結果にあるように、default.rb では3つのリソースを定義しており、種類としては `directory` と `file` の2つです。

```ruby:site-cookbooks/sample/recipe/default.rb
directory "/tmp/hoge" do
  # 状態・アクション・属性の定義
end

file "/tmp/hoge/ipaddress.txt" do
  # 状態・アクション・属性の定義
end
```

これ以外にも様々なリソースが予め定義されています。

以下のサイトをリファレンスとして参照してください。
http://docs.getchef.com/chef/resources.html

リソースはクックブックの中で定義することができますが、まずは既存のリソースを使ってみましょう。

代表的なリソースとサンプル
---------------------

代表的なリソースをサンプルとして書いてみます。
`site-cookbooks/resource-sample/recipes/default.rb` に追記しながら試してみてください。

### user, group : Unixシステムのユーザ管理

サーバ内のユーザとグループを定義します。

```ruby:site-cookbooks/resource-sample/recipes/default.rb
#
# someone グループを id=1234 で作成
#
group "someone" do
  action :create
  gid 1234
end

#
# someone ユーザを id=1234 で someone グループに所属させて作成
# ホームディレクトリ /home/someone に作成する
# シェルは /bin/bash 
#
user "someone" do
  action :create
  password "$1$y0mNscmC$LDkzLErL355w0T5mhVJ3C1" # openssl passwd -1 hogehoge
  uid 1234
  gid "someone"
  home "/home/someone"
  shell "/bin/bash"
  supports { :manage_home => true }
end

#
# app ユーザをシステムユーザとして作成
#
user "app" do
  uid 2345
  system true
  shell "/bin/false"
end
```

ユーザに関する属性値を記述できます。

ユーザのパスワードはハッシュ化された値で登録します。`openssl`コマンドがあれば `openssl passwd -1` で生成できます。

既に存在するユーザであっても、パスワードの変更や起動シェルなど属性の違いがあれば修正されます。

### file, remote_file, cookbook_file

ファイルを管理するリソースです。

- `file`: サーバ内の指定されたパスのファイルの内容、パーミッション、ユーザ等を管理する
- `remote_file`: URLから指定されたパスにファイルをダウンロードする
- `cookbook_file`: cookbookの`files/`ディレクトリ以下に予め用意されたファイルを指定されたパスに置く

共通することは、ファイルのチェックサムを比較して差分がなければ何もしないという点です。自動的に冪等性が担保されます。



```ruby:site-cookbooks/resource-sample/recipe/default.rb
#
# file: サーバ内のファイルを定義する
#
file "/tmp/platform.txt" do 
  content "platform: #{node[:platform]}"
  mode 0644 # 注: 0644は8進数表記。644だと違う意味になってしまう。文字列で"644"の方がミスしにくい。
  owner "root"
end

# 不必要なファイルが存在しない状態を定義する。削除するというタスクではないことに注意。
file "/tmp/unused.txt" do
  action :delete
end

#
# remote_file: インターネット上のファイルをダウンロードして保存する
#
remote_file "/tmp/resource.html" do
  source "http://docs.getchef.com/chef/resources.html"
  mode "644"
  owner "someone"
end

#
# cookbook_file: クックブック内のfiles/からファイルをコピーする
#
cookbook_file "/tmp/cookbook_file_sample.sh" do
  mode "0755"
end
```

`cookbook_file` で配置するファイルは`files/default/`以下に同名（ここでは `cookbook_file_sample.sh`）で配置します。

```text:site-cookbooks/resource-sample/files/default/cookbook_file_sample.sh
#!/bin/sh

echo "Copied by cookbook_file."
```

別名でも良いですが、その場合は属性で指定が必要です。詳しくはリファレンスを見てください。

### template

`cookbook_file` リソースと似ているが、テンプレートファイルを差し替えてから配置できます。

```ruby:site-cookbooks/resource-sample/recipe/default.rb
template "/tmp/template-sample.txt" do
  mode "644"
  owner "someone"
end
```

テンプレートファイルは`templates/default/`以下に配置します。ファイル名は差し替えるファイル名に`.erb`をつけたもの（ここでは`template-sample.txt.erb`）です。
差し替えはerbで行われます。詳細については[こちら](http://www2a.biglobe.ne.jp/~seki/ruby/erb.html)を参照してください。

```site-cookbooks/resource-sample/templates/default/template-sample.txt.erb
platform: <%= node[:platform] %>

<% if 1 == 0 %>
!?
<% else %>
OK
<% end %>
```

### directory

読んで字のごとくディレクトリの管理です。
階層の深いディレクトリを作る場合などは、`recursive`をつけます。ただし、中間のディレクトリ(以下の例では`/tmp/too`, `/tmp/too/deep`)にはownerやmodeが適用されないので注意してください。

```ruby:site-cookbooks/resource-sample/recipe/default.rb
directory "/tmp/too/deep/dir" do
  action :create
  recursive true
  owner "someone"
  mode 0755
end
```

### link

シンボリックリンクを管理します。

```ruby:site-cookbooks/resource-sample/recipe/default.rb

#
# シンボリックリンク(/tmp/ip.txt)を作成
#
link "/tmp/ip.txt" do
  to "/tmp/hoge/ipaddress.txt"
end

#
# シンボリックリンクを削除
#
link "/tmp/unknown_link" do
  action :delete
end
```

toでリンク先を指定します。同じ位置に別ファイルへのリンクが合った場合は、定義したリンク先に書き換えます。

不要なリンクを削除することも出来ます。存在しなければ何もしません。

### package

OSの持つパッケージ管理システムを利用してパッケージを管理します。

```ruby:site-cookbooks/resource-sample/recipe/default.rb
package "vim" do
  action :install
end

package "exim4" do
  action :purge
end
```

パッケージ管理ツール別に、`apt_package`, `yum_package` などもありますが、chefの実行時に自動的に判定するので、基本的には `package` で充分でしょう。

プラットフォームによりパッケージ名が異なる場合がありますが、その場合は`case`文で分岐しましょう。
http://docs.getchef.com/essentials_cookbook_recipes.html#use-case-statement

### service

`/etc/init.d/*` のサービスの起動状態を管理します。

```ruby
#
# ntpサービスを起動時有効化(enable)して、開始(start)する
#
service "ntp" do
  action [:enable, :start]
end
```


注意すべき点として、 **この記述の場合、サービスが（起動していない場合に）起動するタイミングはChefの処理の最後** になります。

リソースに対する変更タイミングについては後ほど記述します。

### execute

汎用コマンドの実行を可能にするリソースです。
このリソースを利用すると任意のコマンドを実行可能ですが、特に条件を指定しない場合、何度も処理を実行していしまいます。
不用意に実行しないため、 **変更の必要がないと判断する基準を必ず定義してください。**

```ruby
#
# /path/to/source に移動し、 ./configure を someone ユーザで実行
# Makefile が既に生成されていれば実行しない.
#
ex = execute "configure hoge" do
  action :nothing
  command "./configure --prefix=/usr/local/hoge"
  user "someone"
  cwd "/path/to/source"
  not_if "test -e /path/to/source/Makefile"
end

resouces("execute[configure hoge]")
```

### script(bashなど)

汎用コマンドの中でも、特にスクリプトファイルで複雑な処理を実行したい場合に使用します。
複数行のスクリプトが書きたい場合は、ヒアドキュメントを使うと便利です。
`execute` の場合と同じく、条件がなければ何度も実行してしまいます。

```
#
# スクリプト実行
#
bash "test script" do
  code <<-EOS
    # script here
    touch evidence
  EOS
  user "someone"
  cwd "/path/to/execute"
  environment { "PATH" => "/usr/local/pgsql/bin" }
  not_if "test -e /path/to/execute/evidence"
end
```

## 実行タイミングについて

Chefレシピの実行順序は大まかには以下のとおりです。

- クックブックの評価（レシピの定義、属性値を読み込んで一覧化する）
- クックブックの実行（リソースを評価して実行する）
- ディレイリソースの実行(service startなど)

### Rubyスクリプトの評価とリソースの評価

Rubyスクリプトの実行自体はクックブックの評価に当たるため、以下の様なコードではnkfのインストールがスクリプト評価時に行われず、まだインストールされていない場合は思ったような挙動になりません。

```ruby
package "nkf" do
  action :install
end

#
# package[nkf] が実行される前に if が評価される
#
if `nkf -g /test/hogehoge | grep -v UTF-8` # <- ここだけRubyとして先に実行される
  execute "Change encoding" do
    command "nkf -w --overwrite /test/hogehoge"
  end
end

#
# リソース実行時に条件を確認する
#
execute "Change encoding" do
  command "nkf -w --overwrite /test/hogehoge"
  only_if "nkf -g /test/hogehoge | grep -v UTF-8"
end
```

### リソース実行タイミングのコントロール

`service`リソースなどはデフォルトでは遅延実行となっており、chef実行の最後に行われます。

実行するactionとタイミングは `notifies`, `subscribes` でコントロールできます。
http://docs.getchef.com/chef/resources.html#notifications

- `notifies`: 自身のリソースの更新があったら、他のリソースのアクションを実行する
- `subscribes`: 監視しているリソースの更新があったら、自身のリソースを実行する。

```ruby
#
# リソース定義のみ。何もしない。
#
service "ntp" do
  action :nothing
end

#
# ntpd.confに変更があったら、即時
#
template "/etc/ntpd.conf" do
  mode 0644
  notifies :restart, "service[ntp]", :immediately
end
```

同じことを`subscribes`で書くとこうなります。

```ruby
#
# ntpd.confの変更を監視する
# 
service "ntp" do
  action :nothing
  subscribes :restart, "template[/etc/ntpd.conf]", :immediately
end

#
# ntpd.confテンプレート
#
template "/etc/ntpd.conf" do
  mode 0644
end
```

まとめ
------

この他にも `cron` や `git` などのリソースもあります。
また、サードパーティのクックブックを利用することで、新たに定義されるリソースもあります。（`postgresql_database_user`など。）