2012年12月18日火曜日

Chef Recipeの書き方と運用

Chefがインストールできたので次はRecipeを書いていきます。


Recipeの作成


ChefはRecipeをバージョン管理することができます。ただSVNやGitのような高機能なバージョン管理システムはもっていないので複数人でRecipeを編集する場合は編集点を明確にする上でもSVNやGitを導入した方がよいでしょう。

リポジトリは'knife configure -i'で設定した場所になります。
デフォルトだと'/var/chef/cookbooks/'になります。

knifeコマンドでbaseというRecipeを作成します。

#knife cookbook create base


以下のようなファイルとディレクトリが作成されたと思います。

-attributes/
-CHANGELOG.md
-definitions/
-files/
-libraries/
-metadata.rb
-providers/
-README.md
-recipes/
-resources/
-templates/

普段使わないものもあるので、利用頻度の高そうなものから書きたいとおもいます。

recipes


このディレクトリの中にdefault.rbというファイルがあります。
このファイルにRecipeを書きます。
Rubyが使えます。

'/tmp/work'というディレクトリを作成するには以下のように書きます。

directory "/tmp/work" do
owner "root"
group "root"
mode "0777"
action :create
end


directory部分はResourcesと言われまして、Chef側で予めサポートされているものになります。
標準Resourcesだけで十分Recipeを書くことができます。
Chef Resources一覧

Resourcesを使いこなすことでグンとRecipeを書くスピードが上がりコストも下がるので一度目を通しておくとよいでしょう。

スクリプトを記述することもできます。
ただ、毎回実行されてしまうためこのようにtar展開などをすると毎回上書きされ実行時間が長くなるのであまりオススメできません。
スクリプトはPerl,Ruby,bash,csh,Pythonが対応しています。


script "install_something" do
interpreter "bash"
user "root"
cwd "/tmp"
code <<-EOH
wget http://www.example.com/tarball.tar.gz
tar -zxf tarball.tar.gz
cd tarball
./configure
make
make install
EOH
end


files


クライアントに配布したいファイルをこのディレクトリに置きます。
CHEF-REPO/base/files/default/New_File.tgzと配置したとします。

Recipeでこのように書くと'/tmp/'に配置されます。

cookbook_file "New_File.tgz" do
source "/tmp/New_File.tgz"
end


filesはパッケージや設定ファイルを含めてファイルを配布するには非常に便利ですが設定ファイルはtemplateで配布することを推奨したいです。
そうすることでChefの本来のパワーを自然と発揮できるようになるはずです。

libraries


recipesと殆ど同様の使い方ができます。
RubyとChef既存のResourcesで記述でき独自のクラスを定義することでChefを拡張できます。

moduleを定義することもできますし、defで定義してrecipeで利用することもできます。
your_cookbook/libraries/your_example_library.rb

# define a module to mix into Chef::Recipe
module YourExampleLibrary
def your_function()
# ... do something useful
end
end


your_cookbook/recipes/default.rb


# open the Chef::Recipe class and mix in the library module
class Chef::Recipe
include YourExampleLibrary
end

your_function()


definitions


機能的にはlibrariesと近いですがChefの拡張ではなく既存のResourceを組み合わせて、再帰可能なResourceを定義します。
Rubyが使えます。

apache_siteというdefinitionsを定義します。
apache_site Definition

define :apache_site, :enable => true do
include_recipe "apache2"

if params[:enable]
execute "a2ensite #{params[:name]}" do
command "/usr/sbin/a2ensite #{params[:name]}"
notifies :restart, resources(:service => "apache2")
not_if do
::File.symlink?("#{node[:apache][:dir]}/sites-enabled/#{params[:name]}") or
::File.symlink?("#{node[:apache][:dir]}/sites-enabled/000-#{params[:name]}")
end
only_if do ::File.exists?("#{node[:apache][:dir]}/sites-available/#{params[:name]}") end
end
else
execute "a2dissite #{params[:name]}" do
command "/usr/sbin/a2dissite #{params[:name]}"
notifies :restart, resources(:service => "apache2")
only_if do ::File.symlink?("#{node[:apache][:dir]}/sites-enabled/#{params[:name]}") end
end
end
end


recipeないでapache_siteに対して渡された値はparams[:hoge]で取得できます。



# Enable my_site.conf
apache_site "my_site.conf" do
enable true
end

# Disable my_site.conf
apache_site "my_site.conf" do
enable false
end


definitionsは読み込まれたrecipeないで包括的に利用されるためActionメソッドを渡すことはできません。
Actionメソッドを使いたい場合はResouceを使います。
cookbook {cookbook_name}でcookbookを指定することも可能なのでファイルなどは特定のrecipeにまとめることができます。


templates/attributes


erbが使えます。
設定ファイルはerbを書かなくてもtemplatesで配布すること推奨します。
templateかrecipeにnilの処理を入れておかないとnilがあった際にエラーがでます。
node[:hoge]の部分はattributeから取得できる値に展開されます。

A template (templates/default/sudoers.erb)


#
# /etc/sudoers
#
# Generated by Chef for <%= node[:fqdn] %>
#

Defaults !lecture,tty_tickets,!fqdn

# User privilege specification
root ALL=(ALL) ALL

<% @sudoers_users.each do |user| -%>
<%= user %> ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL
<% end -%>

# Members of the sysadmin group may gain root privileges
%sysadmin ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL

<% @sudoers_groups.each do |group| -%>
# Members of the group '<%= group %>' may gain root privileges
%<%= group %> ALL=(ALL) <%= "NOPASSWD:" if @passwordless %>ALL
<% end -%>


attributeを定義しておくと、プラットフォームやOSが代わっても柔軟に対応させることができます。個々のパラメータなどはattributeに書いておきましょう。

default["authorization"]["sudo"]["groups"] = [ "sysadmin","wheel","admin" ]
default["authorization"]["sudo"]["users"] = [ "jerry","greg"]


A template resource (recipes/default.rb)

template "/etc/sudoers" do
source "sudoers.erb"
mode 0440
owner "root"
group "root"
variables({
:sudoers_groups => node[:authorization][:sudo][:groups],
:sudoers_users => node[:authorization][:sudo][:users]
})
end



recipeの更新


recipeを書き終えるとcookbookの更新を行わければ最新バージョンは適用されません。


//cookbooK_nameのrecipeを更新
#knife cookbook upload {cookbooK_name}

//すべてのrecipeを更新
#knife cookbook upload -a


metadata.rbでバージョンを0.1.1にあげてuploadするとバージョンがあがることがわかります。

Uploading hoge [0.1.1]


バージョンを区切ると前回のバージョンは別で保持されるので、クライアントごとにバージョンを指定したり特定のバージョンに固定化させることもできます。バージョンの運用方法はそれぞれあるので環境に適した方法を選んでください。

オススメは、とにかく編集した後はバージョンを上げるてクライアントにはバージョン指定でrecipeを適用する方法でそうすることでデグレードを最小限に防ぐことができます。

recipeの適応


recipeの適応はクライアント側でchef-clientを実行するのみです。
chef-clientを実行すると即座に適応が開始されるためDryRunを適応前に行いましょう。

chef-client --why-run


knife sshを使いこなせ


数百台レベルのサーバになるとchef-clientの実行自体が容易ではないのでサーバからknifeコマンドでクライアントのrecipeを適用します。

knife sshコマンドはオプションが多数あるので一例です。
hostnameの部分はhostnameを元にマッチしたホストにSSHを実行します。デフォルトはFQDNになります。

interactive オプションをつけることで接続できたクライアントに対してインタラクティブに操作することが可能になります。

-aは接続に利用するattributeを選択でいます。attributeで取得できる値であればなんでも利用可能なのでIPアドレスベースでの接続も可能です。例はhostnameを指定しています。

-x:接続ユーザ
-P:パスワード

knife ssh 'hostname:client-1[0-9]' interactive -x root -P password -a hostname

Opscode Knife

サーバのホスト名を自動で取得し割り当てる方法


※タイトルが不適切だったので変えました(笑)
Chefで構成管理ができると電源ポチでサーバ構築までしてほしいですよね。
ホスト名とIPを動的に結びつけることができれば少なくともDHCPで払い出すIPのレンジ単位でホストの管理でき、うまくいけばIP管理表を捨てれるかもしれません。
今回紹介する仕組みはBINDの拡張機能を利用します。

お気づきかと思いますがタイトルと内容が若干ずれていますがタイトルの内容は十分把握できると思います。

概要
  1. DHCPからIPを割り当てる
  2. 割り当てられたIPから逆引きで名前を引いてくる
  3. ホスト名に割り当てる
DHCP

DHCPは任意のIPを割り当てられる環境がればなんでもよいです。
安物ルータのDHCPでも可。

Linuxのdhcpdを使うのであれば以下のような設定になると思います。

/etc/dhcp/dhcpd.conf
ddns-update-style interim;
ignore client-updates;

subnet 192.168.0.0 netmask 255.255.255.0 {

# --- default gateway
        option routers                  192.168.0.1;
        option subnet-mask              255.255.255.0;

        #option nis-domain              "domain.org";
        #option domain-name             "domain.org";
        #option domain-name-servers      192.168.10.1;

        option time-offset              -18000; # Eastern Standard Time
        #filename "/linux-install/pxelinux.0";
        #next-server                     192.168.1.2;
#       option ntp-servers              192.168.10.1;
#       option netbios-name-servers     192.168.10.1;
# --- Selects point-to-point node (default is hybrid). Don't change this unless
# -- you understand Netbios very well
#       option netbios-node-type 2;

        range dynamic-bootp 192.168.0.100 192.168.0.200;
        default-lease-time 21600;
        max-lease-time 43200;

        # we want the nameserver to appear at a fixed address
#        host ns {
#                next-server marvin.redhat.com;
#                hardware ethernet 12:34:56:78:AB:CD;
#                fixed-address 207.175.42.254;
#        }
}


後はDHCPを受けるクライアントをBOOTPROTO=dhcpにしてネットワークを再起動してください。

BIND

BINDはDHCPによって割り当てられたIPからホスト名を逆引きするために利用します。
BINDにはGENERATEという拡張機能がありゾーンフィアルを簡略化することができます。

これが通常の逆引きのゾーンファイルです。
200     IN      PTR     client-200.
201     IN      PTR     client-201.
202     IN      PTR     client-202.


サーバ台数が増えるとゾーンファイル内が煩雑になるので以下のように書き換えることができます。
$GENERATE 200-202 $.0.168.192.in-addr.arpa. PTR client-$.


前者と後者は同意になります。

複数セグメントがある場合は
$GENERATE 200-202 $.0.168.192.in-addr.arpa. PTR client-168-$.

とかするとユニークなホスト名が生成できます。

$GENERATE 200-210/2 $.0.168.192.in-addr.arpa. PTR client-${-99,3,d}.

200-210/2

/nはステップになります。
192.168.0.200
192.168.0.202
192.168.0.204
192.168.0.206
192.168.0.208
192.168.0.210
に展開されます。

${-99,3,d}

${オフセット,桁数,ベース(o,d,X,x)}
オフセット
ex)192.168.0.200
200-99=101
逆引きのホスト名はclient-101になる。

桁数
client-{桁数}

ベース
d:小数
o:8進数
X or x:16進数

このようにオプションがあり、自由に規則性を持たせることができます。
GENERATEはPTR以外にもCNAME, DNAME, A, AAAA and NSで利用できます。
BIND Master File Extension: the $GENERATE Directive

ホスト名を逆引きから割り当てる

/etc/sysconfig/network
HOSTNAME=
のように空にして逆引きができれば起動時に動的に割り当てられます。

hostname -fで引けるFQDNやホスト名を固定化させたい場合はhostsに追加するしたり、networkに追記するなどの工夫が必要です。
ChefのOhaiが取得するFQDNはhostname -fの値になるので注意してください。

自動でホスト名を割り当てる基準としてDHCPを利用しているため起動のタイミングによってはIPが変わる可能性があります。
そのため一度逆引きすると固定化するようにnetworkファイルを書き換えたほうが無難です。

PXE+Kickstart+Chefを組み合わせれば電源ポチで構築できるようになります。

2012年12月11日火曜日

ChefかPuppetで悩んだお話

前回の記事でChefは動くようになったと思います。
ChefとPuppetの違いはなんだろうと思い使いながら気づいた事をツラツラと書いておきます。

DSL


これらの構成管理ツールはPuppetだとマニフェストでChefだとレシピを統一された言語とフォーマットで記述し誰が見てもわかる状態であることがとても大切だと思っています。

やはり大きな違いは外部DSL(Puppet)か内部DSL(Chef)であることがあげられます。

この点に関しては大きな差は見られないといったところでしょうか。ChefもDSLチックに書くことができRubyを殆ど書かなくてもレシピを書き上げることができます。

前述したましたが構成管理は”統一された言語とフォーマット”であるべきだと思っているので各構成管理ツールの専用の文法のみで書き上げることが望ましいですが、さすがにfor eachぐらいは使わないと辛いのでどうしてもこのへんはRubyになります。ただそれぞれ関数化など包括する仕組みがあるので実際問題それほど煩わしい作業になることはありません。

結論でいうとDSLという面で言えばPuppetでもChefでも同じですよって話でした。

ミドルウェア


PuppetはPupept以外のミドルウェアが必要なくUnicornを使ったりもできますね。
それと比べてChefはメッセージキューやDBが必要になります。

特にDBはCouchDBでそんなもの運用したことないよって思いましたがネット上にゴロゴロと情報があったので運用面では問題になりそうではありませんでした。

インストール自体はyumやGemを利用すればよいので導入コストも殆ど同じでしょう。

Attributes


アトリビュート?で僕はChefを選びました。おそらくChefで一番ウリにしている部分だと思っています。
AttributesはOhaiを使ってクライアンの情報をサーバ側に伝え、サーバ側はそれを記録したものがAttributesです。
Attributesで取得できる情報はディスクのマッピング先、メモリサイズ、インストールされている言語、Uptime、OSの種類、ファイルシステム、ネットワーク、ハイパーバイザーの種類などもっと多くあり追加することも可能です。
これらの情報を動的に取得し、レシピ内ではnode[:os]やnode[:fqdn]と記述することで利用できます。

端的に申しますと、これらの情報はオンプレ環境やマルチクラウドなどに大いに役に立てる事ができます。
例えば、ネットワーク情報を元にG/Wを変更したりOS毎にレシピを分けることなくtemplateを簡単に切り分けることができるためサーバの環境の差を柔軟に吸収することができます。

バージョン管理


Chefは標準でバージョン管理の仕組みを持っています。
metadata.rbをversionを変えることでバージョンを指定できます。
このバージョンもAttributesと同様の使い方ができProduction環境では1.0の利用を強制してDevでは1.0以上のバージョンが利用できるなどオンラインでレシピを変更しても誤ってProductionに影響を与える事がありません。
このバージョン管理はロール単位で行え、バージョンの指定はクライアンごとに行えます。

databags


databagsはJSON形式であらゆるデータを保存し暗号化することができる機能です。
databagsにユーザIDやPasswordを一元管理し保存しレシピから利用できるためレシピ内にPasswordを記述し漏洩するリスクを軽減したり、変更するコストを下げることができます。


# Load the keys of the items in the 'admins' data bag
admins = data_bag('admins')

admins.each do |login|
# This causes a round-trip to the server for each admin in the data bag
admin = data_bag_item('admins', login)
home = "/home/#{login}"

# for each admin in the data bag, make a user resource
# to ensure they exist
user(login) do
uid admin['uid']
gid admin['gid']
shell admin['shell']
comment admin['comment']

home home
supports :manage_home => true
end

end

# Create an "admins" group on the system
# You might use this group in the /etc/sudoers file
# to provide sudo access to the admins
group "admins" do
gid 999
members admins
end


Tags


AWSのタグと同等の機能を持っておりkey valueの形式で記述事ができレシピ内で利用できます。
Attributesでは取得できないMETA情報を持たせるこに利用することが多いです。

まとめ


様々なレンタルサーバーでサーバを構築したりすることが多いので今回はChefを選びました。
キーさえあれば様々な情報をJSONで取り出すことができるので監視の自動がなども容易に行えます。
ハードウェア情報やクライアン情報を元に構築する必要がない場合はPuppetで十分だと思います。