itamae実践Tips

最近仕事で [itamae](https://github.com/itamae-kitchen/itamae) を触っているので、ハマったところや工夫したところをまとめておきます

下記のスニペットはitamae 1.9系で確認してます

# 最初に読むべきドキュメント
* https://github.com/itamae-kitchen/itamae#in-japanese
  * 日本語はここにまとまってる
* https://github.com/itamae-kitchen/itamae/wiki
  * コマンドの使い方がだいたいまとまってる

# ファイルが更新された時のみ特定のコマンドを実行したい
解） `notifies` か `subscribes` を使う

https://speakerdeck.com/ryotarai/itamae-infra-as-code-xian-zhuang-que-ren-hui?slide=24

どっちも似てるけど（いまだに区別ついてないｗ）だいたいこんな感じ

* `notifies` : 自分が変更された時に他を実行したい
* `subscribes` : 他が変更された場合に自分を実行したい

## 例）`/etc/nginx/sites-available/jenkins.conf` が変更されたら nginxをrestartする

```ruby
execute "/etc/init.d/nginx configtest" do
  subscribes :run, "template[/etc/nginx/sites-available/jenkins.conf]"
  action :nothing
end

execute "/etc/init.d/nginx restart" do
  subscribes :run, "template[/etc/nginx/sites-available/jenkins.conf]"
  action :nothing
end

template "/etc/nginx/sites-available/jenkins.conf"
```

`subscribes` と `action :nothing` を一緒に使うのがポイント。
（`execute` のデフォルトactionは `run` なので `template` に来る前に実行されてしまう）

## 例）sources listが更新されたらapt-get update したい
```ruby
# NOTE: source listが更新されればapt-get updateする
execute "apt-get update" do
  subscribes :run, "remote_file[/etc/apt/sources.list.d/jenkins-ci.list]", :immediately
  action :nothing
end

remote_file "/etc/apt/sources.list.d/jenkins-ci.list"

package "jenkins"
```

* `subscribes` の第3引数に `:immediately` （コールバック受けたらすぐに実行）つけるのがポイント
  * デフォルトだと `:delay` （遅延実行）なので `remote_file` 直後に `apt-get update` が実行されずに `package "jenkins"` でエラーになることがある
  * ~~`:delay` だとレシピ全部流してから実行？（実行タイミングはどこにも書いてないので不明）~~
  * `:delay` だと「そのレシピファイルをすべて実行し終わった後に実行される」とのこと

# cdした先でコマンドを実行したい
解）`cwd` 使う

## 例：よくあるmake実行

```ruby
execute "cd /usr/local/src/tig"
execute "make configure"
execute "make clean",
execute "make prefix=/usr/local",
execute "make install prefix=/usr/local",
```

とか実行しても、1行目と2行目のセッションは違うので、意図したディレクトリでmakeが実行されません。（capistranoとかと同じ）

かといって

```ruby
execute "cd /usr/local/src/tig && make configure" 
```

とか書いてもいいんですがウザいのでそういう時は `cwd` が便利。

```ruby
execute "make configure" do
  cwd "/usr/local/src/tig"
end
```

みたいに書ける。

実行するコマンドが多いと配列でぶん回してもいい。

```ruby
[
  "make configure",
  "./configure",
  "make clean",
  "make prefix=/usr/local",
  "make install prefix=/usr/local",
].each do |command|
  execute command do
    cwd "/usr/local/src/tig"
  end
end
```

ちなみに `cwd` は [Itamae::Resource::Base](https://github.com/itamae-kitchen/itamae/blob/master/lib/itamae/resource/base.rb) [^base] に定義されているので `execute` 以外でも使える。
https://github.com/itamae-kitchen/itamae/blob/v1.9.2/lib/itamae/resource/base.rb#L94

[^base]: 全てのresourceの基底クラス

# makeでインストールしたいんだけどインストール済ならスキップしたい
`package` を使えばインストール済かどうかチェックしていい感じに冪等性担保してくれるんだけど、 `execute` 使う場合には自前で頑張る必要がある。

解）`not_if` を使う

```ruby
execute "make" do
  not_if "/usr/local/bin/tig -v | grep #{note[:tig][:version]}"
end
```

`not_if` のコマンドが失敗した時のみに `execute` 実行される。

インストールされていなければ `tig -v` でこけるし、違うバージョンがインストールされていれば `grep` でこける。

個人的には `ls` と組み合わせて使うことも多い。

```ruby
# Jenkinsユーザの秘密鍵が作られていなければ作る
execute "ssh-keygen -t rsa -N '' -C 'jenkins@my.server' -f #{node[:jenkins][:home_dir]}/.ssh/id_rsa" do
  not_if "ls #{node[:jenkins][:home_dir]}/.ssh/id_rsa"
end
```

# serverspecからもitamaeのnodeで定義した値を使いたい
こういうやつ

```yaml:nodes/default.yml
jenkins:
  home_dir: "/var/lib/jenkins"
```

解）spec/spec_helper.rb 辺りに下記を追加

```ruby:spec/spec_helper.rb
require "yaml"
require "itamae"

ENV["NODE"] ||= "nodes/default.yml"

def node
  return @node if @node

  hash = YAML.load_file("#{__dir__}/../#{ENV["NODE"]}")

  @node = Itamae::Node.new(hash, Specinfra.backend)
end
```

* ローカル変数だとrequireしても別ファイルから使えないのでメソッドにするのがポイント
* `Itamae::Node` のインスタンスを返すことで `node[:platform]` などの specinfraの [Host Inventory](http://serverspec.org/host_inventory.html) も `node` 経由で取れるようになる

specはこんな感じ

```ruby:jenkins_spec.rb
describe file("#{node[:jenkins][:home_dir]}/.ssh/id_rsa") do
  it { should be_file }
  it { should be_mode 600 }
  it { should be_owned_by "jenkins" }
  it { should be_grouped_into 'jenkins' }
end
```

# OSごとに分岐をしたい
解）`node[:platform]` を使う

```ruby
case node[:platform]
when 'debian'
  package 'libffi-dev'
when 'redhat'
  # CentOSは redhat 扱い。
  package 'libffi-devel'
end
```

さすがにpackage名まではspecinfraで抽象化できないので複数OS考慮しようとするとこういうところで分岐が必要になってくる。

参考になるやつ

* http://serverspec.org/host_inventory.html
* https://github.com/k0kubun/itamae-plugin-recipe-rbenv/blob/v0.5.0/lib/itamae/plugin/recipe/rbenv/dependency.rb

# packageからインストールした設定ファイルを一部分だけ変更したい
解）`file` リソースの `:edit` を使う

## 例）aptからインストールしたJenkinsの設定ファイルでJAVA_ARGSを編集
```ruby
package "jenkins"

# Jenkinsの設定が更新されれば再起動
execute "/etc/init.d/jenkins restart" do
  subscribes :run, "file[/etc/default/jenkins]"
  action :nothing
end

file "/etc/default/jenkins" do
  action :edit
  block do |content|
    content.gsub!(/^JAVA_ARGS=".+".*$/, %q(JAVA_ARGS="-Djava.awt.headless=true -Dhudson.util.ProcessTree.disable=true"))
  end
end
```

行の途中で置換されても困るので `/^〜$/` で明示的に行頭と行末を指定するのがいいと思う

`".*$` みたいにダブルクオーテーションの後ろで0文字以上マッチングしてるのは、下記のような文字列の行末のコメントアウト部分を削るため

```
JAVA_ARGS="-Djava.awt.headless=true"  # Allow graphs etc. to work even when an X server is present
```

# 存在するはずのファイルが無いってエラーになる

```ruby
# NOTE: itamae local で実行
Dir.glob("/etc/pam.d/*").each do |filename|
  file filename do
    action :edit
    block do |content|
      content.gsub!(/pam_ldap\.so/, "pam_sss.so")
    end
  end
end
```

こういうレシピを書いていてなぜか  `/etc/pam.d/smtp` が無いというエラーが。
これ自体は、別のレシピでpostfixをインストールしててその時にこのファイルを消してる（ように見えた）のが原因だった。

で、なんで `Dir.glob` でヒットしたファイルがレシピ実行時に存在しないのかというと、itamaeの挙動によるもの。

```ruby
Dir.glob("/etc/pam.d/*").each do |filename|
  file filename do
    # 〜
  end
end
```

実は↑の時点では `file` リソースの動的定義だけで中のレシピは実行されていない。

itamaeはレシピを全部読み込んだ後にリソースのブロックを評価しているので、レシピ定義時点では  `/etc/pam.d/smtp`  は存在してたかもしれないけど、レシピ実行時には消されていたためファイルが存在しないというエラーになってた。

今回の場合は 下記のように `only_if "ls #{filename}"` でエラーは回避できた。（`ls` して成功した時のみレシピを実行）

```ruby
Dir.glob("/etc/pam.d/*").each do |filename|
  file filename do
    action :edit
    only_if "ls #{filename}" # <- これを追加
    block do |content|
      content.gsub!(/pam_ldap\.so/, "pam_sss.so")
    end
  end
end
```

今回みたいにファイルが消えてる場合は `only_if "ls 〜"` で回避できるけど、ファイルが追加された場合は編集すべきファイルがそのまま残るというリスクがある。（対処できるんだろうか。。。）

# fluentdのプラグインをインストールしたい
解）`gem_package` を使う

fluentdのプラグインも普通のgemなので [gem_package](https://github.com/itamae-kitchen/itamae/wiki/gem_package-resource) リソースがそのまま使える。td-agentを使ってる場合は `gem_binary`を下記のようにしてやるだけでインストールできる

td-agent 2系

```ruby
gem_package "fluent-plugin-forest" do
  gem_binary "/usr/sbin/td-agent-gem"
end
```

td-agent 1系

```ruby
gem_package "fluent-plugin-forest" do
  gem_binary "/usr/lib/fluent/ruby/bin/fluent-gem"
end
```

ref. http://docs.fluentd.org/articles/faq#i-installed-td-agent-and-want-to-add-custom-plugins-how-do-i-do-it

数が多い場合はこんな風に `each` で動的定義するのが楽

```ruby
[
  ["fluent-plugin-forest", "0.3.0"],
  ["fluent-plugin-rewrite", "0.0.13"],
  ["fluent-plugin-rewrite-tag-filter", "1.5.4"],
].each do |plugin_name, plugin_version|
  gem_package plugin_name do
    gem_binary "/usr/sbin/td-agent-gem"
    version    plugin_version
  end
end

```

vagrantのプラグインも同様の方法でインストールできるはず