1.はじめに

こんにちは! "ドワンゴ 弁当" で最近少し話題になったドワンゴエンジニア、の氏家です。
どんな人が中で働いてるのか想像しにくい方も多いかもしれませんが、普通の人・オタクな人・ギークな人・家庭持ち・リア充イケメンいろんな人が混じってる、楽しい会社だと思っています。
人と同じように 多種多様なサービス・システム・ミドルウェア・デバイス・プログラム言語を駆使してみんながニコニコできるものを産み出そうとがんばっていますので、こういったエンジニアリングに興味がある方は是非コチラからご応募ください!ニコニコ入社一時金制度もやっています。

そしていろいろと長くなってしまいましたが、今回でChef Solo話、完結したいと思います。今回はやってみて気づいた点・はまった点などを詳しく説明しますので、少しでもみなさんの参考になれば幸いです。

2.TIPS!
もくじ
  • roleはjsonで書くべき? それともruby?
  • recipe・attribute内でのcookbook名の参照
  • 秘密情報の管理
  • どれを使うといいの? bash, script, execute
  • serviceで起動がされない
  • ファイル・ディレクトリ属性"mode"の書き方
  • OS標準で用意されてるconfの書き換えはどうする?
  • templateとcookbook_fileは自動バックアップがされてる
  • ifとnot_ifとonly_if どれを使えばいい?
  • 同一名だとWarning
  • attribute内で他の値を参照するときの書き方
  • attributeの配列定義の注意

roleはjsonで書くべき? それともruby?

roleのファイル形式はjson・ruby どちらの形式でも書くことが出来ます。ただroleだとif文による条件分岐を書けたり他のファイルをincludeできるなど、ruby構文によるプログラム制御が出来るので、rubyで書くのがおすすめです。

nodeファイルは残念ながらjson形式しか対応していません。

recipe・attribute内でのcookbook名の参照

recipe内では、自動的に割り当てられている"cookbook_name"という変数で 現在のcookbook名が参照できます。

log cookbook_name

一方attribute内では直接参照はできませんが、

cookbook_name = __FILE__.split('/')[-3]
default[cookbook_name]['prefix'] = '/usr/local/mysql'

とすることで、attribute内でもcookbook名を参照することが出来ます。

これは、attributeが次のようなディレクトリ構造になってるからです。
/cookbooks/mysql/attributes/default.rb

秘密情報の管理

例えばユーザーやDBアカウントのパスワードだったり、開発者には知られてはいけない インフラ担当者のみが知ってよい設定値など、他の人には見られたくない情報があったりします。

そういったものはdata_bagに保存しておくのが正しいのですが、Chef Soloの場合 構成ファイルをgit等で管理する関係上、取り扱いをうっかりしちゃうと誰でもパスワードが見れてしまう危険性があります。

そこでChef Soloでdata_bagの値を暗号化して秘匿化する方法があります。それは、Knife Soloと同じようなアドオン「knife-solo_data_bag」を導入することです。

詳細な説明は省きますが、これを使えばdata_bagの中に置いたテキストファイルを暗号化でき、recipe内で暗号化された中身を直接参照することも出来ます。

ただ大きな問題がありまして、この秘匿化を行う際の鍵は共通鍵です。つまりこの鍵が流出してしまうと、結局内容が丸裸になってるのと同じです。なのでこの鍵だけは別管理で保管しておくのが(面倒ですが)現状では最善な策です。

ちなみにですが、Knife Soloで転送する=秘密鍵もあわせて転送されます。Knife Solo 0.3.0からは、転送先は /home/実行ユーザー/chef-solo/ になります。なので実行ユーザーのhomeディレクトリも他の人は見れないように注意しとく必要があります。

どれを使うといいの? bash, script, execute

例えば「tarを展開する」などChef標準のリソースでは対応してない場合、自分でスクリプトを書くことになります。そのスクリプトを実行するリソースにはいくつか種類があります。どれを使えばよいでしょうか?

  • script vs bash (csh, ruby, perl, python)
script "copy file" do
    interpreter "bash"
    code <<-EOH
        cp /tmp/file1 /tmp/file2
    EOH
end

bash "copy file" do
    code <<-EOH
        cp /tmp/file1 /tmp/file2
    EOH
end

は同じ動きをします。

  • bash vs execute
execute "copy file" do
    command "cp /tmp/file1 /tmp/file2"
end

executeはよくホームページで見かけるサンプルを見ると、だいたい上記のように1行で実行する場合のみ使える、ように見えますが、実際は

execute "copy file" do
    command <<-EOH
        cp /tmp/file1 /tmp/file2
        cp /tmp/file1 /tmp/file2
    EOH
end

のように複数行書くことも出来ます。じゃあ違いは何かというと、

bash
記述されたスクリプトを、一時ファイルとして保存し、bashで実行する

execute
記述されたスクリプトを、"sh -c" で実行する

なので、結論からすると、
問題なければ基本はexecute、問題があればbashなど他のリソースを使う
がよいかと考えます。

ちなみに

execute "copy file" do
    command "cp /tmp/file1 /tmp/file2"
    creates "/tmp/file2"
end

と書けば、file2がすでに存在してるときはcommandを実行しません。

serviceで起動がされない

例えば

service "httpd" do
    action [ :start ]
end

と書いても起動しないときがあり、理由は大きく2つあります。

1.conf設定などに不備があって、そもそも起動が失敗する
2."service httpd status" の終了コードが0 (既に起動中とChefが判断している)

特に2.のときは 手動では起動できてしまうので、要注意です。あと、

service "httpd" do
    supports :restart => true
    action [ :restart ]
end

上記の場合は"service httpd restart"に相当することをしますが、

service "httpd" do
    supports :restart => false
    action [ :restart ]
end

の場合は"service httpd stop; service httpd start" と実行されるので注意が必要です。

ファイル・ディレクトリ属性"mode"の書き方

ファイルやディレクトリの属性は"mode"で指定しますが、解説によって書き方がまちまちです。主な例だと下記のようなのが挙げられます。

1.mode 0644
2.mode 00644
3.mode "0644"

rubyは 0から始まる数値は8進数として認識します。なので1番 は"644"、2,3番は"0644"ということになります。
おすすめの書き方は3番目です。

1番の場合、chmodで例えると、
一見は "chmod 0644" と指定しているように見えますが、
実際は "chmod 644" と書いてることになります。
Chef上の表記と、実際の指定値は異なるので紛らわしいかなというところです。

OS標準で用意されてるconfの書き換えはどうする?

例えばsshd_configとかです。

だいたいはデフォルトのsshd_configの中の、一部の値のみを変更します。
が、Chefではそういった変更に対応する方法がありません。

実際どうするか。
0.手作業で編集する
 → chefはサーバーをあるべき状態に収束するって考えからすると、論外ですね。

1.templateリソースなどを使って、Chefで管理してるconfファイルをサーバーに置く
 → OS標準のconfをまるっと置き換えるので、もしかしたら動作がおかしくなるかも…

2.bashリソース内でsedを使って、一部分のパラメータ値だけを変更する
 → べき等性の担保が難しい

ただ、Chefのレシピを見ればサーバーがどういう状態になってるかがわかる、という観点からすると、1.でやるのがよいのかなと思っています。

templateとcookbook_fileは自動バックアップがされてる

templateやcookbook_fileを用いてテキストファイルを置き換えるとき、元々あったファイルが自動でバックアップされるようになっています。
デフォルトでは /var/chef/backup に、日時が付加されたファイル名になって保存されます。

ifとnot_ifとonly_if どれを使えばいい?

「特定の条件のときには○○はしない」という条件判定をするときの書き方について。

「既に同名のファイルが存在する場合はコピーをしない」というのを例にあげると、まずnot_if・only_ifで書く方法があります。

execute "copy file (not_if)" do
    command "cp /tmp/file1 /tmp/file2"
    not_if do File.exists?("/tmp/file2") end
end
execute "copy file (only_if)" do
   command "cp /tmp/file1 /tmp/file2"
   only_if do !File.exists?("/tmp/file2") end
end

これらの違いは真と偽が逆転してるだけで、上記の2例は同じ挙動をします。

一方、ifとの違いについてです。

if !File.exists?("/tmp/file2")
    execute "copy file (if)" do
        command "cp /tmp/file1 /tmp/file2"
    end
end

execute "copy file (only_if)" do
    command "cp /tmp/file1 /tmp/file2"
    only_if do !File.exists?("/tmp/file2") end
end

一見すると上の例はどちらも同じような動きをしそうですが、実は違います。上の例では 想定通り最初のcpでファイルがコピーされるのでファイルが存在することになり、only_ifの方は実行されません。

ところが

execute "copy file (only_if)" do
    command "cp /tmp/file1 /tmp/file2"
    only_if do !File.exists?("/tmp/file2") end
end

if !File.exists?("/tmp/a")
    execute "copy file (if)" do
        command "cp /tmp/file1 /tmp/file2"
    end
end

と書くと、下のif文で書かれた方もコピーが実行されてしまいます!

これはChefがrecipeをどう解釈してるかの違いから生まれるものです。Knife SoloでChef Soloを実行するとき、内部ではおおざっぱには次のような処理がされます。

1. Knife Solo実行
 1−1.rsyncでcookbook一式rsync

2.Chef Solo実行
 2−1.recipeを解読
  実行するリソースを、Chefプログラム内の実行予定リストに追加

 2−2.attribute値を決定
  attributeファイル内で定義された値をベースに、
  同じ変数名の値が role・nodeで定義されてた場合は、それぞれ値を上書きする
  ※上記によって確定した値を、recipe内では node[]ハッシュで参照することになる

 2−3.2−1.で作られた実行リストの順に実行する

これによって、if文で書いた場合は 2−1.時点で if条件の判定がされてしまいます。一方only_ifで行うと、2−3.の実行リストが1つずつ実行されてく過程で判定がされます。

なので先の例だと、Chef Soloのrecipe解釈時にはファイルが存在しないので実行リストに"executeを実行する" と追加されてしまい(条件判定はこの段階で終わり)、実際にexecuteを実行する段階では ファイルがあろうとなかろうとコピーが実行されてしまったのです。

同一名だとWarning

recipe内で同じ名前を繰り返し使うとwarningになります。よくある例がrubyの配列で処理してるときですね。

%w{file2 file3}.each do |filename|
    execute "copy file" do
        command "cp /tmp/file1 /tmp/#{filename}"
    end
end

これも先に書いたChefの実行解釈によるもので、recipe解釈時に配列を展開して、1つずつ実行リストに付け加えてるからです。なので同じ名前が被らないように工夫しましょう。

%w{file2 file3}.each do |filename|
    execute "copy file : #{filename}" do
        command "cp /tmp/file1 /tmp/#{filename}"
    end
end
attribute内で他の値を参照するときの書き方

attribute内で下記のように書いたとします。

default['attr1'] = 'ニコニコ';
default['attr2'] = default['attr1'] + '動画';
default['attr3'] = node['attr1'] + '動画';

recipe内で下記のように中身を確認すると、どちらも、"ニコニコ動画"の値が返ってきます。

log node['attr2']
log node['attr3']

ただattributeはrole及びnodeで設定値を上書くことができます。もしrole内で

default_attributes(
    "attr1"=>"ピアピア"
)

とすると、attr3の値は"ピアピア動画"となりますが、attr2は"ニコニコ動画"のままとなってしまいます。

attributeの配列定義の注意

attributeはrubyファイルなので配列も定義できます。

default['arr'] = [
    {'name' => '動画'},
]

ただ、それをroleやnodeで定義すれば値は上書きされるはず、ですよね。

default_attributes(
    'arr' => [
        {'name' => '生放送'}
        {'name' => 'チャンネル'}
    ]
)

でも、recipe内で下記のように値を参照すると、

node['arr'].each do |value|
  log value
end

"動画"、"生放送"、"チャンネル"、と、attributeで定義した配列に追加される形となってしまいます。

もし配列も、roleやnodeで定義した値で完全に置き換えたい場合は、ハッシュにすると解決できます。

default['arr'] = { 
    0 => {'name' => '動画'},
}

default_attributes(
    'arr' => { 
        0 => {'name' => '生放送'},
        1 => {'name' => 'チャンネル'}
    }
)

node['arr'].each_value do |value|
  log value
end
※ハッシュなので値を取り出すときは、.eachではなく.each_valueにする

これで "生放送"、"チャンネル" だけがかえってくるようになります。

3.さいごに

Chefでレシピを書くのは、正直大変です。慣れないうちは「ソースからコンパイルしてインストールする」レシピを書くだけで1日かかってしまいます。ただ自分の場合はChefの用語や特有の記法などに混乱し、その情報を調べるのに無駄に時間がかかっていました。

今回の連載がどこまで皆さんの役に立つかはわかりませんが、これで1日かかっていたのが半日に減少され、Chefに挫折する人が減って、Chef人口が増えてくれれば一番何よりです。