Itamaeが構成管理を仕込みます! ~新進気鋭の国産・構成管理ツール~

第3回ノード属性で汎用的なレシピを書く

前々回前回でItamaeの基本的な使い方とレシピの書き方を解説しました。今回はノード属性の使い方について解説します。

ノード属性(node attributes)

Itamaeのレシピはできるだけ汎用的にし、様々なホストで利用できる形にするのが理想です。汎用的になっていることで再利用性が高まりますし、プラグインとして公開することもできます。

レシピを汎用的なものにするためには、レシピ自体に設定値などをハードコードせず、ホスト固有の情報などを実行時に注入できる必要があります。このようにレシピやテンプレートから実行時に設定した値を参照できる仕組みとしてノード属性(node attributes)があります。

Itamaeの実行時のオプションに--node-jsonを渡すと、JSONの内容がノード属性として設定され、その値がレシピ内から参照できるようになります。

templates/etc/nginx/nginx.conf.erb:
$ cat node.json
{
  "nginx": {
    "version": "1.9.3-1+trusty0"
  }
}
$ itamae local --node-json node.json recipe.rb

--node-jsonでJSONを指定した場合、レシピ内ではnodeを使って参照できます。

package "nginx" do
  version node['nginx']['version']
  # また、node[:nginx][:version] や node.nginx.version でも参照できる
end

このようにJSONでnginxのバージョンを指定できるようにしておくと、ホストやロールごとにバージョンを変更することができ、柔軟にプロビジョニングを行えるようになります。

また、JSONの代わりにYAMLを指定することもできます。

$ cat node.yaml
nginx:
  version: "1.9.3-1+trusty0"
$ itamae local --node-yaml node.yaml recipe.rb

テンプレートからの利用

nodeはレシピだけではなくテンプレート内からも参照することができます。テンプレートの使い方については前回の記事を参照してください。設定ファイルを動的に生成する場合に有用です。

recipe.rb:
node.reverse_merge!({
  'nginx' => {
    'worker_processes' => 'auto',
  }
})

template "/etc/nginx/nginx.conf" do
  owner 'root'
  group 'root'
  mode  '644'
end
templates/etc/nginx/nginx.conf.erb:
worker_processes <%= node['nginx']['worker_processes'] %>;

(後略)

このようなレシピを書いておくと、ホストごとにworker_processesを設定できます。

デフォルト値の指定

package "nginx" do
  version node['nginx']['version']
end

このようなレシピをJSONを指定せず実行すると、値が見つからずエラーになってしまいます。値が設定されなかった場合にデフォルト値を利用するように設定することでこれを防げます。

デフォルト値を設定するにはreverse_merge!メソッドを利用します。reverse_merge!は値が設定されていない場合のみ値を設定するメソッドです。

# nodeが以下のようになっていて
# {
#   "nginx" => {
#     "worker_processes" => 1
#   }
# }

# reverse_merge!を呼ぶと
node.reverse_merge!({
  'nginx' => {
    'version' => '1.9.3-1+trusty0'
  }
})

# nodeはこのようになります
# {
#   "nginx" => {
#     "worker_processes" => 1,
#     "version" => "1.9.3-1+trusty0"
#   }
# }

このメソッドを利用するとJSONで値が設定された場合にはその値を使って、設定されていない場合はデフォルト値を使うレシピを書けます。

# recipe.rb

node.reverse_merge!({
  'nginx' => {
    'version' => '1.9.3-1+trusty0'
  }
})
package "nginx" do
  version node['nginx']['version']
end

この場合、大部分のホストでは1.9.3-1+trusty0を利用して、特定のホストやロールでは他のバージョンを利用する、といったことが可能になります。また、reverse_merge!を使ってロールごとにデフォルト値を変更することもできます。例えば、appサーバとproxyサーバでnginxのプロセス数を変更したい場合、以下のように記述できます。

# recipes/nginx.rb
node.reverse_merge!({
  'nginx' => {
    'worker_processes' => 'auto',
  }
})

template '/etc/nginx/nginx.conf'
# roles/app.rb
node.reverse_merge!({
  'nginx' => {
    'worker_processes' => 1,
  }
})

# この時点でnginx.worker_processesが設定されるため、recipes/ruby.rbの設定は使われない

include_recipe '../recipes/nginx.rb'
# roles/proxy.rb
# この時点でnginx.worker_processesは設定されていないため、recipes/ruby.rbの設定が使われる

include_recipe '../recipes/nginx.rb'

appロールでは1プロセス、proxyロールではauto(nginxレシピのデフォルト値)が使われ、更に上書きしたい場合は--node-json--node-yamlを指定することができます。

バリデーション

デフォルト値を設定せず、必ず値をJSONから渡す必要があるような場合、バリデーションルールを書いておくと実行前にエラーが発見できます。また、他のレシピからinclude_recipeされるような場合、そのレシピで必要な値を明示する意味も兼ねることができます。

バリデーションはvalidate!を呼ぶことで実行できます。例えば、先ほどのnginxの例だと

node.validate! do
  {
    nginx: {
      version: string,
    }
  }
end

このように記述すると、node['nginx']['version']が指定されていて、かつStringになっていることが保証されます。このバリデーションはレシピの実行前に行われるので、実行時のエラーを防ぐことができます。バリデーションルールにはオプショナルな値の指定や、正規表現も指定できます。

node.validate! do
  {
    nginx: {
      user: string, # 文字列のみ(必須)
      worker_processes: optional(integer), # 整数のみ(任意項目)

      # sitesは配列で、server_name, root, allowed_ipsが必須
      sites: array_of({
        server_name: string,           # 文字列(必須)
        root: string,                  # 文字列(必須)
        allowed_ips: array_of(string), # 文字列の配列(必須)
      }),

      listen: match(/^(80|443)$/), # /^(80|443)$/にマッチする文字列(必須)
    },
  }
end

例えば、上記のレシピに誤ったノード属性を指定すると、エラーが出力され、レシピの実行が中止されます。

$ cat node.json
{
  "nginx": {
    "user": 123
  }
}
$ itamae local --node-json node.json recipe.rb
 INFO : Starting Itamae...
 INFO : Loading node data from /private/tmp/node.json...
ERROR : 'nginx->user' is not String
ERROR : 'nginx->sites' is required but missing
ERROR : 'nginx->listen' is required but missing

nodeを参照する際の注意点

nodeはRubyのHash(厳密にはHashie::Mash)なので、存在しないキーを参照した場合にエラーにならずnilが返ってきます。例えば、テンプレートで存在しないキーを参照すると

node.reverse_merge!({
  nginx: {}
})

template "/etc/nginx/nginx.conf"
worker_processes <%= node['nginx']['worker_processes'] %>;

生成される設定ファイルは

worker_processes ;

このようになります。これを防ぐには、

  1. デフォルト値を設定する
  2. バリデーションルールを記述し、実行前に中止する
  3. fetchを使って値を参照する

といった方法があります。1、2はここまでで紹介したとおりです。3のfetchを使うと、存在しないキーを参照した時に例外が発生するため、nilが返ってきて設定が空になることが防げます。多少記述が冗長になるのが難点です。

# worker_processesが設定されていない場合、例外が発生する
worker_processes <%= node.fetch('nginx').fetch('worker_processes') %>;

ホスト情報の取得

何も設定しなくても、メモリのサイズやOSの種類などのホスト情報はデフォルトでnode経由で取得できるようになっています。これを利用すると、例えば、ディストリビューションごとにインストールするパッケージを変更することができます。

case node['platform']
when 'ubuntu'
  # ubuntuの場合の処理
when 'redhat'
  # redhatの場合の処理
end

また、AWSのEC2インスタンスの場合、EC2特有の値も取得できます。

p node['ec2']
# {"ami-id"=>"ami-35072834",
#  "ami-launch-index"=>"0",
#  "ami-manifest-path"=>"(unknown)",
#  "block-device-mapping"=>{"ami"=>"/dev/xvda", "root"=>"/dev/xvda"},
#  "hostname"=>"ip-172-30-1-119.ap-northeast-1.compute.internal",
#  "instance-action"=>"none",
#  "instance-id"=>"i-0d307c14",
#  "instance-type"=>"t2.micro",
#  "local-hostname"=>"ip-172-30-1-119.ap-northeast-1.compute.internal",
# (後略)

ホスト情報の取得はSpecinfraのHost Inventory機能を利用しているので、取得できる値の詳細はSpecinfraのドキュメントをご参照ください。

プラグイン

このホスト情報取得機能はプラガブルになっていて、取得項目の拡張が可能になっています。例えば、specinfra-ec2_metadata-tagsを使うと、EC2インスタンスのタグが取得できるようになります。

role = node['ec2']['tags']['Role'] # Roleタグが取得できる
include_recipe File.join("roles", "#{role}.rb")

このようにRoleタグを参照して、読み込むレシピを変更することも可能になります。

コマンドの実行結果を使ってレシピを構築する

レシピを動的に変化させ、汎用的にするもう一つの方法としてrun_commandがあります。レシピやテンプレートとrun_commandを呼ぶと、コマンドの実行結果(SSH実行の場合、SSH越しの対象サーバでの結果)が取得できます。

case run_command("your-command").stdout.chomp
when "foo"
  # 処理
when "bar"
  # 処理
end

まとめ

今回は設定を外から注入しレシピを汎用的に開発する方法を紹介しました。

次回は汎用的なレシピや独自リソースをプラグインとして公開するための方法をご紹介します。

おすすめ記事

記事・ニュース一覧