chef-soloのレシピのカスタマイズの記録

こんにちは。小宮です。
おかげ様でカスタマイズする機会があったため、その一部を引用してご紹介いたします。

基本的なことなどは、
以下のリンクや「入門chef-solo」その落ち穂拾いもご覧になるとよいと思います。
Chef Soloと Knife Soloでの ニコニコサーバー構築 (2) 〜導入編〜:dwango エンジニア ブロマガ
chef-solo – Chefを読んで実行するための全知識 – Qiita
DevOpsを実現するChef活用テクニック // Speaker Deck
あとGW前後にchef実践入門的な書籍をエンジンヤード(chefが出る前から8年くらい使ってる会社)の御方が出されるそうで大変期待してるところです。

chefのnode、role、enviroment、attribute、data_bagsの解説になります。
この記事は基本の説明とカスタマイズの為の簡単な情報提供になるかと思います。
以下に記載しているレシピを実際ためした環境はCentOS6.4のみで、申し訳ありませんが異なる環境での動作保障はできないです。
異なるOS間の動作保障するような汎用的(複雑)なレシピはopscodeコミュニティの☆がいっぱいついてるクックブックを使えばいいらしいです。
(余計なものを極力入れないとか既存の環境または手順をレシピ化するという需要には不向きかとは思います。)

chefリポジトリ直下のディレクトリの解説を以下に記します。
  cookbooks :サードパーティのクックブック置き場
 data_bags :data_bagsのデータ置き場
 environments :環境設定ファイル置き場
 nodes :ノード(ホスト・各サーバ)設定ファイル置き場
 roles :役割設定ファイル置き場
 site-cookbooks :自作クックブック置き場(クックブックはレシピ、配布ファイル等を含む)

各用語を以下に軽く解説します。
・ohai
chefに同梱されているレシピを適用するホストの情報を取得するコマンドです。
ohaiで取得した値に基づいてattributeを定義することが可能です。
たとえばOS搭載メモリやCPUコア数に応じて設定値を変えたい場合やホスト名や
IPアドレスなど固有の情報を設定ファイルに載せたい場合などに利用すると便利です。

・node
nodesディレクトリ直下にhostname.jsonまたはipaddress.jsonというファイルを置き、
そこにそのサーバ固有の設定値(attribure)や割り当てる役割(role)を定義します。

・role
dbやwebなど、同じ役割のサーバをroleでまとめ、同じ役割のサーバ群に適用するレシピ
やパラメータをまとめて定義します。

・enviroment
試験環境・本番環境・開発環境、といった環境毎に変える必要がある設定値(attribure)を定義します。
(Chef-Serverではクックブックバージョンを固定が可能だそうですがChef-Soloでは不可能)

・attribute
node、role、enviroment毎に変更したいパラメータのことです。
templateリソースから呼び出すファイル内に<%= %>などで変数を定義します。
設定ファイルの値を個別の役割等ごとに変更したい時に利用します。
各クックブック内のattributes/default.rbにデフォルト値を定義することも可能で、
templateリソースからvariablesで設定することも可能です。ohaiの値をそのまま設定もできます。

・data_bags
クックブックには含めたくないグローバルなデータを入れるものです。
ユーザ情報などがそれに当たります。
attributeのようにrole毎に変えるデータでなく変えないから1か所だけに書いておいて
ロードしたいというデータを置くのに適していると思われます。

・レシピの適用方法
何かのレシピをホストに適用したい場合、knifeコマンドでホストの後にレシピを指定するか、
nodeやroleのrun_listにレシピやロールを指定する必要があります。

roleやnodeを設定せずに直接knifeコマンドに引数として与える方法:
knife solo cook -o <cookbook>::<recipe>【,<cookbook>::<recipe>】
※この方法はそのリポジトリで何のレシピがどのホストに適用されたのかが管理できなくなるため
オススメできませんが、既に稼働中の環境に影響を与えたくない場合に使う可能性があると思います。

nodeのrun_listに指定する例:
[shell]vi <chef-repository>/nodes/localhost.json
—————-
"run_list":[
"recipe[base_setting::common-pkgs]",
"recipe[base_setting::sysctl]",
"role[API-ext]"
]
—————-[/shell]

roleのrun_listに指定する例:
[shell]$ vi roles/API-ext.json
—————-
{
"name":"API-ext",
"chef_type": "role",
"json_class":"Chef::Role",
"default_attributes":{
"base_setting": {
"swappiness": "0",
"tcp_tw_reuse": "0",
"tcp_tw_recycle": "0",
"tcp_fin_timeout": "10",
"tcp_max_syn_backlog": "8192",
"somaxconn": "8192"
}
},
"override_attributes":{},
"description":"API’s role",
"run_list": [
"recipe[roles::API-ext]"
]
}
—————-
$ cat site-cookbooks/roles/recipes/API-ext.rb
—————-
include_recipe "base_setting::common-pkgs"
include_recipe "base_setting::sysctl"
include_recipe "base_setting::ntpd"
include_recipe "base_setting::rps_cpus"
include_recipe "login-users::create_user"
include_recipe "login-users::key_copy"
include_recipe "login-users::api_user"
include_recipe "java::java"
include_recipe "nginx::nginx"
include_recipe "nginx::nginx_virtual"
include_recipe "play::play"
include_recipe "mysqld::mysql-client"
include_recipe "git::git"
include_recipe "Flydata::Flydata"
include_recipe "munin::munin-node"
include_recipe "munin::munin-node-API-PVP-robi"
include_recipe "zabbix::zabbix-agent"
—————-[/shell]
※上記ではroleファイル内にrecipeを列記せずrolesクックブックのAPI-ext.rbに必要なrecipeをincludeしています。
※nodeやroleのjsonファイルのrun_list内にはrecipeとroleをカンマ区切りで併記することが可能です。
[和訳] 初心者Chefアンチパターン by Julian Dunn #opschef_ja
ではroleをcookbookで管理する方法が推奨されています。
role、nodeの設定が適切に行われている場合、-oでクックブックやレシピを指定しなくてもホスト名を指定し
knife solo cookを実行すれば設定したとおりにレシピが適用されます。

レシピの適用順序については基本指定した順ですがnotifiesに指定したサービスのリスタート等は、
全てのレシピが適用(収束)した後になります。以下がわかりやすいのでご覧ください。
Chefのレシピは上から下に実行されるという誤解

・テスト方法について
追って追記するかもしれませんが、クックブックのシンタックスは以下のコマンドで確認できます。
[shell]knife cookbook test <cookbook-name>[/shell]
レシピ適用後の実環境のテストはServerspecがいいと思います。

・attributeとdata_bagsの利用例と実装手順
事例を列挙していきますが、以下リンクも参考にしてください。
[chef] attributeの理解 | ITインフラ雑記帳

・ohaiのデータを利用してOS搭載メモリやCPUコア数に応じた設定をする
 ohaiを使う場合、templateリソースをレシピに定義し、設定ファイル内に変数を定義するのみで、
roleやnodeのattributeを定義する必要がありません。
ohaiコマンドを直接打ってみるとどんな値がとれるかわかります。
[shell]$ ohai ipaddress
[
"192.168.1.133"
][/shell]

OS搭載メモリの75%をinnodb_buffer_pool_sizeに設定する
[shell]$ vi site-cookbooks/mysqld/recipes/mysqld-server.rb
template ‘/etc/my.cnf’ do
owner ‘root’
group ‘root’
source ‘etc/my.cnf.erb’
notifies :restart, ‘service[mysql]’
end
$ vi site-cookbooks/mysqld/templates/default/etc/my.cnf.erb
innodb_buffer_pool_size = <%= ("#{node[:memory][:total]}"[/\d+/].to_f * 1024 * 0.75 ).to_i %>[/shell]

OS搭載CPUコア数と同等の値をworker_processesに設定する。
[shell]$ vi site-cookbooks/nginx/recipes/nginx.rb
template "nginx.conf" do
path "/etc/nginx/nginx.conf"
source "etc/nginx/nginx.conf.erb"
owner "root"
group "root"
mode 0644
notifies :reload, ‘service[nginx]’
end
$ vi site-cookbooks/nginx/templates/default/etc/nginx/nginx.conf.erb
worker_processes <%= node[:cpu][:total] %>;[/shell]

・ohaiのデータを利用してホスト名やIPアドレスを設定する
[shell]$ vi site-cookbooks/zabbix/recipes/zabbix-agent.rb
template ‘/etc/zabbix/zabbix_agentd.conf’ do
owner ‘root’
group ‘root’
source ‘etc/zabbix/zabbix_agentd.conf’
end

$ vi site-cookbooks/zabbix/templates/default/etc/zabbix/zabbix_agentd.conf
Hostname=<%= node[‘hostname’] %>
$ cat site-cookbooks/mysqld/templates/default/etc/my.cnf.erb|grep ipaddress
server-id = <%= node[:ipaddress].split(".").last %>
report-host = <%= node[:ipaddress] %>[/shell]

ノードごとに異なる値を自動設定可能なため、
事前に準備したりログイン後に修正してリロードをいちいち実施する必要がなくなって大変便利かと思います。

・role毎に異なるattributeをtemplateリソースで呼び出して設定ファイルに反映
base_settingクックブックのsysctlのレシピがそれを実施しています。

[shell]$ cat site-cookbooks/base_setting/recipes/sysctl.rb
template "/etc/sysctl.conf" do
owner ‘root’
group ‘root’
source ‘etc/sysctl.conf.erb’
mode 0644
end
$ cat site-cookbooks/base_setting/templates/default/etc/sysctl.conf.erb
vm.swappiness = <%= node[‘base_setting’][‘swappiness’] %>
net.ipv4.tcp_tw_reuse = <%= node[‘base_setting’][‘tcp_tw_reuse’] %>
net.ipv4.tcp_tw_recycle = <%= node[‘base_setting’][‘tcp_tw_recycle’] %>
net.ipv4.tcp_fin_timeout = <%= node[‘base_setting’][‘tcp_fin_timeout’] %>
net.ipv4.tcp_max_syn_backlog = <%= node[‘base_setting’][‘tcp_max_syn_backlog’] %>
net.core.somaxconn = <%= node[‘base_setting’][‘somaxconn’] %>
$ cat roles/db.json
~略~
"base_setting": {
"swappiness": "0",
"tcp_tw_reuse": "1", #バックエンドのdbはtcp接続をリサイクルする
"tcp_tw_recycle": "1", #バックエンドのdbはtcp接続をリサイクルする
"tcp_fin_timeout": "10",
"tcp_max_syn_backlog": "8192",
"somaxconn": "8192",
"ntpserver1": "ntp.nict.jp"
}
~略~
$ cat roles/API.json
~略~
"base_setting": {
"swappiness": "0",
"tcp_tw_reuse": "0", #フロントエンドのAPIはtcp接続をリサイクルしない
"tcp_tw_recycle": "0", #フロントエンドのAPIはtcp接続をリサイクルしない
"tcp_fin_timeout": "10",
"tcp_max_syn_backlog": "8192",
"somaxconn": "8192",
"ntpserver1": "ntp.nict.jp"
}
~略~[/shell]
※role毎に変えてる意味合い的にはスマホからwifi接続の接続元IPが同じになってる
複数の端末が同時にアクセスした場合tcp接続をリサイクルしてると片方のパケットが
破棄されるため必ず異なるIPから接続があるバックエンドのみでtcp接続リサイクルを
有効化して負荷軽減するというものです。

mhaのmanagerの場合はmanager用のrpmを導入する、そうでない場合は導入しない等
[shell]$ cat site-cookbooks/mysqld/recipes/mysql-mha.rb
if node[:mysqld][:mha] == ‘manager’
%W{perl-Config-Tiny perl-Log-Dispatch perl-Parallel-ForkManager perl-Params-Validate perl-Time-HiRes}.each do |pkg|
package pkg do
not_if "rpm -qa|grep #{pkg}"
action :install
end
end
cookbook_file "/usr/local/src/#{mhamanager}" do
not_if "ls /usr/local/src/#{mhamanager}"
source "usr/local/src/#{mhamanager}"
end
~略~
end
$ cat roles/mhamanager.json
~略~
"default_attributes":{
"mysqld": {
"mha":"manager"
},
~略~[/shell]

・node毎に異なるattributeをifで呼び出してリソースを実行
これは、設定ファイルの内容やcron登録するかをを切り替えたり等あります。

dbのマスタと切り替わるはずのスレーブは設定ファイルでread_onlyを有効にしない、
切り替わらない2つ目のスレーブはread_onlyを有効化
[shell]$ cat site-cookbooks/mysqld/recipes/mysqld-server.rb
~略~
if node[:mysqld][:mha_nomaster] == ‘true’
template ‘/etc/my.cnf’ do
owner ‘root’
group ‘root’
source ‘etc/my.cnf_ro.erb’
notifies :restart, ‘service[mysql]’
end
else
template ‘/etc/my.cnf’ do
owner ‘root’
group ‘root’
source ‘etc/my.cnf.erb’
notifies :restart, ‘service[mysql]’
end
end
~略~
$ cat nodes/db3.hoge.jp.json
{
"mysqld" : {
"master" : "false",
"only_mysql" : "true",
"only_innodb":"true",
"mha_nomaster" : "true"
},
~略~[/shell]

スレーブでのみ登録実行するcronリソース
[shell]~略~
if node[:mysqld][:master] == ‘false’
cron "mysql_replication_check" do
not_if "crontab -l|grep rep_fail_mail.sh"
minute "0-59/15"
hour "*"
day "*"
month "*"
weekday "*"
command "/opt/bin/rep_fail_mail.sh > /dev/null 2>&1"
action :create
end
end
~略~
$ cat nodes/db1.hoge.jp.json
{
"mysqld" : {
"master" : "true",
~略~
$ cat nodes/db2.hoge.jp.json
{
"mysqld" : {
"master" : "false",
~略~[/shell]

・enviroment毎に異なる設定ファイルをifで呼び出してリソースを実行
APIのnginxの設定ファイルの配布に用いています。(sourceのディレクトリパスを変えてます)
[shell]$ cat site-cookbooks/nginx/recipes/nginx_virtual.rb
if node[:environment] == ‘huka’
template "/etc/nginx/conf.d/virtual.conf" do
source "etc/nginx/conf.d/huka/virtual.conf.erb"
owner "root"
group "root"
mode 0644
notifies :reload, ‘service[nginx]’
end
elsif node[:environment] == ‘prd’
template "/etc/nginx/conf.d/virtual.conf" do
source "etc/nginx/conf.d/prd/virtual.conf.erb"
owner "root"
group "root"
mode 0644
notifies :reload, ‘service[nginx]’
end
elsif node[:environment] == ‘stg’
template "/etc/nginx/conf.d/virtual.conf" do
source "etc/nginx/conf.d/stg/virtual.conf.erb"
owner "root"
group "root"
mode 0644
notifies :reload, ‘service[nginx]’
end
end[/shell]
設定ファイルは同一で中身のURLなどのattributeの値を変えるのが一般的そうですが、事前にパラメータがわからない為こうなった感じです。
参考:Chef Solo の Environments – naoyaのはてなダイアリー

・data_bagsからバージョンデータをロードしてパッケージ名を定義
各クックブックで殆ど利用してます。
[shell]$ cat site-cookbooks/nginx/recipes/nginx.rb
version = data_bag_item(‘pkg_versions’,’nginx’)[‘version’]
nginx_pkg="nginx-#{version}.ngx.x86_64.rpm"
~略~

$ knife solo data bag create pkg_versions nginx
$ knife solo data bag edit pkg_versions nginx
$ knife solo data bag show pkg_versions nginx
id: nginx
version: 1.4.4-1.el6[/shell]

・data_bagsからユーザデータをロードしてユーザ作成
login_usersクックブックのcreate_userレシピ
[shell]$ cat site-cookbooks/login-users/recipes/create_user.rb
data_ids = data_bag(‘users’)
data_ids.each do |id|
u = data_bag_item(‘users’, id)
name = u[‘username’]
user u[‘username’] do
not_if "id #{name}"
home u[‘home’]
shell u[‘shell’]
uid u[‘uid’]
gid u[‘gid’]
password u[‘password’]
end
end[/shell]

mysqldクックブックのmysqld-serverレシピで暗号鍵で暗号化したデータをロードして用いるレシピ。
[shell]$ cat site-cookbooks/mysqld/recipes/mysqld-server.rb
~略~
## load data_bag data.
root = Chef::EncryptedDataBagItem.load("mysqlusers","root")
repl = Chef::EncryptedDataBagItem.load("mysqlusers","repl")
suuser = root["user"]
supass = root["pass"]
repluser = repl["user"]
replpass = repl["pass"]
### secure installation,etc.
mysqlconn = "/usr/bin/mysql -u root"
if #{version} == ‘5.6’
package ‘expect’ do
:install
not_if "rpm -qa|grep expect"
end
template "/tmp/setpass.sh" do
only_if ‘ls /root/.mysql_secret’
source "setpass.sh"
end
bash "set_password" do
only_if "ls /root/.mysql_secret"
code <<-EOC
chmod +x /tmp/setpass.sh && /tmp/setpass.sh && rm -f /tmp/setpass.sh
EOC
end
end
bash "secure_installation" do
ignore_failure true
only_if "#{mysqlconn} -e ‘show databases;’"
code <<-EOC
#{mysqlconn} << EOF
grant all on *.* to #{suuser}@’%’ identified by "#{supass}";
grant all on *.* to #{suuser}@’localhost’ identified by "#{supass}";
grant all on *.* to #{suuser}@’::1′ identified by "#{supass}";
grant all on *.* to #{suuser}@’127.0.0.1′ identified by "#{supass}";
grant replication slave,replication client on *.* to #{repluser}@’%’ identified by "#{replpass}";
drop database test;
delete from mysql.user where password=”;
flush privileges;
EOF
EOC
end
pfile="/root/.my.cnf"
bash "create-pfile" do
not_if "ls #{pfile}"
code <<-EOC
echo ‘[mysqladmin]’ >> #{pfile}
echo "user= #{suuser}" >> #{pfile}
echo "password = #{supass}" >> #{pfile}
echo "" >> #{pfile}
echo ‘[mysql]’ >> #{pfile}
echo "user= #{suuser}" >> #{pfile}
echo "password = #{supass}" >> #{pfile}
chmod 400 #{pfile}
echo "#{supass}" >> /root/.mysql_pwd
chmod 400 /root/.mysql_pwd
EOC
end
~略~[/shell]
5.6系の初期パスワードをbashでexpectでリセットしてmysql接続してgrantして
 不要なものを消して他のスクリプト等から呼ばれるパスワードファイルを生成しています。
chefで作るmysql – Qiita を参考にしました。
※暗号鍵については、data_bag_keyを生成してchefリポジトリ直下の.chef/knife.rbのdata_bag_path設定作業が先に必要です。
data_bag_keyの生成
[shell]$ openssl rand -base64 512 |tr -d ‘\r\n’ > ~/.chef/encrypted_data_bag_secret
$ chmod 400 ~/.chef/encrypted_data_bag_secret
$ vi .chef/knife.rb[/shell]

・data_bagsから配列データをロードして複数行に反映
これはbase_settingクックブックのntpdレシピのtemplateリソースからdata_bagsのデータをロードしてます。
[shell]$ cat site-cookbooks/base_setting/recipes/ntpd.rb
ntpservers = data_bag_item(‘base_setting’,’ntpserver’)[‘ntpservers’]
template "/etc/ntp.conf" do
path "/etc/ntp.conf"
source "etc/ntp.conf.erb"
owner "root"
group "root"
mode 0644
notifies :restart, ‘service[ntpd]’
variables({
:ntpservers => ntpservers
})
end

$ cat site-cookbooks/base_setting/templates/default/etc/ntp.conf.erb
driftfile /var/lib/ntp/drift
#server -4 <%= node[‘base_setting’][‘ntpserver1’] %> iburst
<% for ntpserver in @ntpservers %>
server -4 <%= ntpserver["server"] %> iburst
<% end %>

data_bagsの定義
$ knife solo data bag create base_setting ntpserver
$ knife solo data bag edit base_setting ntpserver
{
"id": "ntpserver",
"ntpservers": [
{
"server" : "ntp.nict.jp"
},
{
"server" : "ntp.nict.jp"
},
{
"server" : "ntp.nict.jp"
}
]
}
[/shell]
3つともntp.nict.jpにしてるのはdigで見るとDNSラウンドロビンで複数のサーバに向いているためです。

data_bagsから配列データを取り出す方法に関して詳しくは以下URLがわかりやすく解説しているのでご覧ください。
databags を使ってみた一部始終(1)

また、カスタマイズに関しては以下も参考になるかと思います。
Chef レシピによる環境のカスタマイズ : Developer Center
肝心なところがポインタになってる等と感じた方は申し訳ありません。

以上、長々読んでいただいてありがとうございました。

おすすめ記事