Ruby
itamae

itamae実践Tips

More than 1 year has passed since last update.

最近仕事で itamae を触っているので、ハマったところや工夫したところをまとめておきます

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

最初に読むべきドキュメント

ファイルが更新された時のみ特定のコマンドを実行したい

解) notifiessubscribes を使う

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

どっちも似てるけど(いまだに区別ついてないw)だいたいこんな感じ

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

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

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"

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

例)sources listが更新されたらapt-get update したい

# 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実行

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とかと同じ)

かといって

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

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

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

みたいに書ける。

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

[
  "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

ちなみに cwdItamae::Resource::Base 1 に定義されているので execute 以外でも使える。
https://github.com/itamae-kitchen/itamae/blob/v1.9.2/lib/itamae/resource/base.rb#L94

makeでインストールしたいんだけどインストール済ならスキップしたい

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

解)not_if を使う

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

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

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

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

# 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で定義した値を使いたい

こういうやつ

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

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

spec/spec_helper.rb
require "yaml"
require "itamae/node"

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 Inventorynode 経由で取れるようになる

specはこんな感じ

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] を使う

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

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

参考になるやつ

packageからインストールした設定ファイルを一部分だけ変更したい

解)file リソースの :edit を使う

例)aptからインストールしたJenkinsの設定ファイルでJAVA_ARGSを編集

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

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

# 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の挙動によるもの。

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

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

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

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

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 リソースがそのまま使える。td-agentを使ってる場合は gem_binaryを下記のようにしてやるだけでインストールできる

td-agent 2系

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

td-agent 1系

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 で動的定義するのが楽

[
  ["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のプラグインも同様の方法でインストールできるはず


  1. 全てのresourceの基底クラス