ChefでデフォルトのAttributesを動的に設定する方法

ChefのAttributesで悩んだことがあったのでメモ。結論から言うと、AttributeファイルではできなくてRecipe内で定義するしかなさそう。

説明のために、Chefで構築するソフトウェアが以下のディレクトリ構造であるとする。

/opt
 +-sample   ・・・インストール先のベースディレクトリ
    +-bin 
    +-data   ・・・仮想のソフトウェアのデータディレクトリ
    +-logs   ・・・仮想のソフトウェアのログ出力ディレクトリ

このソフトウェアのインストール先、データディレクトリ、ログ出力先をAttributesでパラメタ化する。Attributeを素直に書くとこうなる。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"
default[:sample][:data_dir] = "/opt/sample/data"
default[:sample][:logs_dir] = "/opt/sample/logs"

しかしこれだと、base_dir を変えたときに合わせて data_dir、logs_dir も変えないといけない。例えばRoleでデフォルトのAttributeを上書きする際に、以下のように全部定義しないといけない。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :base_dir => "/usr/local/sample",
    :data_dir => "/usr/local/sample/data",
    :logs_dir => "/usr/local/sample/logs"
  }
)
run_list(
  "recipe[sample]"
)

これは面倒なので、base_dir の値を data_dir、logs_dir に埋め込んで動的に設定されるようにしたくなる。よくあるAttributeの書き方だと以下のように#{node[...]}で埋め込む方法になっている。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"
default[:sample][:data_dir] = "#{node[:sample][:base_dir]}/data"
default[:sample][:logs_dir] = "#{node[:sample][:base_dir]}/logs"

しかしこれではうまく機能しないケースがある。デフォルト設定なら問題ないのだが、例えば下記のようにRoleで base_dir だけを変更したとする。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :base_dir => "/usr/local/sample"
  }
)
run_list(
  "recipe[sample]"
)

期待する動作としては、Roleで設定した base_dir + /data, /logs なのだがそうはならない。data_dir, logs_dir は相変わらずデフォルト設定のままとなり変更されないのだ。Environmentを使用した場合も同じです。どうも、Attributesが評価される段階ではまだRoleやEnvironmentで設定した値は反映されないらしい(参考)。

結論

動的に決定させたいAttributeはRecipeで設定するしか現状なさそうです。Recipe内であればRoleやEnvironmentで設定した値がnodeに反映されている。

下記のように、動的な部分はAttributeでは定義しないようにする。

  • sample/attributes/default.rb
default[:sample][:base_dir] = "/opt/sample"

代わりにRecipe内で設定する。

  • sample/recipes/default.rb
default_unless[:sample][:data_dir] = "#{node[:sample][:base_dir]}/data"
default_unless[:sample][:logs_dir] = "#{node[:sample][:base_dir]}/logs"

ここでdefault_unlessを使用するのがミソ。これだと未設定の場合だけ設定してくれる。こうしておくと、下記のようにRoleで設定した値が優先されるし、定義しなければデフォルト値が使用される。例のように、logs_dir だけ /var/log 以下に変更したい、という場合に使える。

  • role/sample.rb
name "sample"
description "Sample App"
default_attributes(
  :sample => {
    :logs_dir => "/var/log/sample/logs"
  }
)
run_list(
  "recipe[sample]"
)

しかし、パラメータをAttributeファイルに全て集約できないので見通しが悪くなってしまうのがちょっといまいちですね〜。Attributeファイル内で使用した#{node[...]}の評価を後で実行してくれればいいのですが・・・。

参考

上記を見るとapply_expansion_attributes(expand!) をAttributeファイルのトップにかけばうまくいく、という話もあるが、Chefの実装依存の動きらしいのでオススメできないそうです(もううまく動かないという話もあり)。