Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

Recipeが実行されるまでの仕組みと変数の関係

こんにちは、Ruby大好きMiyakeです :-)

f:id:acro-engineer:20130919213305p:plain
"DevOps"という言葉がクローズアップされるようになってから、Chefがかなり流行ってきてますよね。
ブログとかTwitterをみていても、ここ1年で実際の業務や、開発環境を構築するのにChefを使ってる人がかなり増えたな~って思っています :-)

今日は、そんなChefを使っていく中で気になった、Recipeが実行されるまでの仕組みと、Recipe内で宣言している変数の関係について調べてみたので、紹介したいと思います。
実はこのRecipeが実行されるまでの仕組みを理解することで、Recipe内で宣言した変数の使い方が、より理解できるようになります :-)

まずは、Recipeが実行されるまでの仕組みが見えてくるよう、わざと誤った変数の使い方をしたサンプルを書いてみました。
誤った変数の使い方をしているのは、リソースBlockとリソースBlockの間で、リソースBlock外で変数の値を変更しているところです。
# ③ここでchef_pathを"/etc/chef"に変更 というコメントが書かれている、chef_path変数にpath_local変数の値を設定しているところが、変数の値を変更している個所になります。

# encoding: utf-8
# 変数宣言
chef_path = "/var/chef/cache"
path_local = "/etc/chef"

Chef::Log::warn "resource外で値(test_ls 1前) chef_path = #{chef_path} (objID = #{chef_path.object_id})"
# ①script resource call 1
script "test_ls 1" do
  interpreter "bash"
  user "root"
  cwd "#{chef_path}"
  returns 0
  code <<-EOH
    echo 'script bash resourceの中での値 chef_path=#{chef_path}'
    ls -la
  EOH
end
# ②ruby block 1
ruby_block "block test 1" do
  block do
    puts "ruby_block resource の中での値 chef_path=#{chef_path}"
    Chef::Log::warn "ruby_block resource の中での値 chef_path = #{chef_path}  (objID = #{chef_path.object_id})"
  end
end

Chef::Log::warn "resource外でchef_pathの値を変更する前 chef_path = #{chef_path} (objID = #{chef_path.object_id})"
# ③ここでchef_pathを"/etc/chef"に変更
chef_path = path_local
Chef::Log::warn "resource外でchef_pathの値を変更した後 chef_path = #{chef_path} (objID = #{chef_path.object_id})"

# ④script resource call 2
script "test_ls 2" do
  interpreter "bash"
  user "root"
  returns 0
  code <<-EOH
    echo 'script bash resourceの中での値 chef_path=#{chef_path}'
    ls -la #{chef_path}
  EOH
end
# ⑤ruby block 2
ruby_block "block test 2" do
  block do
    puts "ruby_block resource の中での値 chef_path=#{chef_path}"
    Chef::Log::warn "ruby_block resource の中での値 chef_path = #{chef_path}  (objID = #{chef_path.object_id})"
  end
end

Chef::Log::warn "resource外の最後の chef_path = #{chef_path} (objID = #{chef_path.object_id})"

上記のRecipeを実行した結果として出力されたログが、以下の結果です。
※Chefの出力も含まれるので、わかりやすくするために目的のログ部分だけをピックアップしています。

Compiling Cookbooks...
[2013-09-08T19:52:58+09:00] WARN: resource外で値(test_ls 1前) chef_path = /var/chef/cache (objID = 24367420)
[2013-09-08T19:52:58+09:00] WARN: resource外でchef_pathの値を変更する前 chef_path = /var/chef/cache (objID = 24367420)
[2013-09-08T19:52:58+09:00] WARN: resource外でchef_pathの値を変更した後 chef_path = /etc/chef (objID = 26489540)
[2013-09-08T19:52:58+09:00] WARN: resource外の最後の chef_path = /etc/chef (objID = 26489540)
Converging 4 resources

Recipe: sample_recipe::scope_test


■①script[test_ls 1]実行時ログ
script bash resourceの中での値 chef_path=/var/chef/cache


■②ruby_block[block test 1]実行時ログ
ruby_block resource の中での値 chef_path=/etc/chef
[2013-09-08T19:52:58+09:00] WARN: ruby_block resource の中での値 chef_path = /etc/chef (objID = 26489540) <-- なぜか/var/chef/cache になっていない


■④script[test_ls 2]実行時ログ
script bash resourceの中での値 chef_path=/etc/chef


■⑤ruby_block[block test 2]実行時ログ
ruby_block resource の中での値 chef_path=/etc/chef
[2013-09-08T19:52:58+09:00] WARN: ruby_block resource の中での値 chef_path = /etc/chef (objID = 26489540)

上記ログで、scriptリソースの①script[test_ls 1]④script[test_ls 2]の実行時ログに出力しているchef_pathの値は、最初は"/var/chef/cache"、変数変更後は"/etc/chef"と、コードに書いている通りの結果となっています。
ところが、ruby_blockリソースのruby_block[block test 1]ruby_block[block test 2]の実行時ログは、両方とも"/etc/chef"となっていて、まだ変数の値が変更される前に記述されているruby_block[block test 1]で、chef_pathの値がすでに"/etc/chef"となって出力されています。
※変数のObjectIDを出力させているのでわかると思いますが、ruby_block[block test 1]ruby_block[block test 2]で出力されている文字列は、全く同じものです。

Recipeの処理が上から順番に処理されていれば、ruby_block[block test 1]のchef_pathの値は"/var/chef/cache"となっているはずです。
ではなぜ、リソースブロック外で変数の値を変更した内容が、上記のような結果になってしまったのか....

さっきは詳しく説明していませんでしたが、ログの先頭に出力されている

Compiling Cookbooks...
[2013-09-08T19:52:58+09:00] WARN: resource外で値(test_ls 1前) chef_path = /var/chef/cache (objID = 24367420)
[2013-09-08T19:52:58+09:00] WARN: resource外でchef_pathの値を変更する前 chef_path = /var/chef/cache (objID = 24367420)
[2013-09-08T19:52:58+09:00] WARN: resource外でchef_pathの値を変更した後 chef_path = /etc/chef (objID = 26489540)
[2013-09-08T19:52:58+09:00] WARN: resource外の最後の chef_path = /etc/chef (objID = 26489540)
Converging 4 resources

の部分にカギがあります。

実を言うとChefは、Recipeを実行する前に、Recipeに記述されているリソースBlockコードを、リソースオブジェクトに変換する処理(以降、これをコンパイルということにします)を行った後に、コンパイルによって変換されたリソースオブジェクトを順番に実行しています。

コンパイルは、上記のログの"Compiling Cookbooks..."から"Converging 4 resources"の間で行われています。

それではもう一度ログの先頭を見てください。
リソースBlockの外のコードで出力しているログが、すべてコンパイル処理中に出力されています。

つまり、リソースBlockの外のコードは、リソースBlockのコードをリソースオブジェクトに変換するコンパイル処理時にすでに実行されてしまっているのです。
そしてこのコンパイル処理の段階で、リソースBlock外のコードで設定した変数の値を、リソースオブジェクトのAttribute(interpreterやcwdなど)に設定しています。
図にすると、こんな感じです。

f:id:acro-engineer:20130916215935j:plain

上記の図にあるコンパイル処理が完了したのち、コンパイルで生成されたリソースオブジェクトが順番に実行されていきます。
この際、コンパイル時に設定されているリソースオブジェクトのAttributeを使用して処理が行われます。
リソースオブジェクトを実行する段階では、すでにコンパイル時に実行されているリソースBlock外のコードは実行されません

このため、ruby_blockの block do ~ endでは、単純にRubyコードを実行するだけなので、コンパイル後に設定された変数の値を使用して処理を行ってしまいます。
図にすると、こんな感じです。

f:id:acro-engineer:20130916215917j:plain

このRecipe実行の仕組みを踏まえたうえで、Recipeを書く際に注意するポイントを以下に上げてみます。

  1. リソースBlockのAttributeに設定する値は、リソースBlock外の変数を用いて設定してもよい
  2. リソースBlock外で値を変更した変数を、ruby_block内で使用するコードは書かない
  3. ruby_block内で値を変更した変数を、リソースBlock外で使用するコードは書かない
  4. ruby_block内で値を変更した変数を、リソースBlockのAttributeに設定するコードは書かない


Recipeを書いていると、リソースBlock外の処理とリソースBlockが順番に処理されると錯覚してしまいますが、今回説明したRecipeの仕組みを理解したうえでRecipeを書くと、Recipe内で宣言した変数を正しく使うことができるようになります。

それでは :-)