Chef-Soloの構成についての考察

================

Chefを実際にプロジェクトで運用して感じたことをまとめてみます。

Chefに含むものは主に「手順」と「属性(値やパラメータ)」にわかれます。
まず大事なのはDRYの原則が守られることで、環境(development, staging, production) が増えたり、サーバRole(WebとかBatchなど)が増えたりしたときにDRYが崩れないことが望ましいです。

いろいろ試行錯誤した結果、次はこういう風にしようと思ったことを書きます。

実行環境
-------------

chef 11.6.0  と knife-solo (0.3.0) で動かすことを想定しています。
特に knife-solo は 0.2 と 0.3 でかなり仕様が違うので注意してください。

用語
------

* 属性： 色々な値やパラメータのことです。
	* ユーザIDやパスワード、ホスト名やPATH のような色々な値
* 環境： development, staging, production のようなものを区別するものです
* Role： 「エンドユーザ向けWebサーバ」「メールサーバ」など、１つのサーバグループを指す意味で使っています。
	* 「エンドユーザ向けWebサーバ」や「管理系Webサーバ」などが１つのRoleという意味です
* Endpoint:  他のシステムへの接続情報
	* MySQLやfluentdなどへの接続情報（hostやportなど)
	* APIのBASE URLなど

手順について
==========


手順自体の記述
----------------------

いわゆる recipe と呼ばれる実際にサーバに何をするか（例えば、httpdのInstallとか、fluentdのInstallとか）というものを記述する部分。
関係のある箇所として主に下記のものがあります。

### cookbooks/{cookbook}/

* どのシステムにも適用するできる汎用的な手順
* 外部からとってきたcookbookなどはここに置く

### site-cookbooks/{cookbook}/

* 今回のプロジェクト・システムに適用する手順
* プロジェクト依存の手続きは全てここに書くことになる
	* ※ここがメインになる
* 属性は node Objectから取得するようにする
	* そうすると後述の属性上書き機構が使える

手順をどのサーバに適用するか
---------------------------------------

どのレシピを実行するかは、run_listで指定します。
run_listとは上記のrecipeのリストです。
run_listの指定は主に下記の2箇所で行います。
１つのRoleを１つのサーバのグループとみなして定義すると運用しやすいです（fromt-web, cms-web, log, batch 等）。

### roles/*.rb

* そのRoleに適用する run_list を指定する
	* apache 入れて、PHP入れて、Ruby入れて、Conf配置して、、、など
* 必要があれば、環境依存の処理を入れる（なるべくしない）

### nodes/*.json
* ここでも run_list を指定することができるが、あまり使わない（と思う）

属性について
====


属性自体の記述
-----------------

属性は以下の箇所に記述できます。
上書き関係を作ることができ、その関係を処理した値が node Objectとして recipe から参照できます。

###  [A] cookbooks/{cookbook}/attributes/*.rb

* どのシステムにも適用する属性
* 基本ここの属性はあまりいじらないはず

### [B] site-cookbooks/{cookbook}/attributes/*.rb

* 今回のプロジェクト・システム全体に適用する（デフォルト）属性
* 特定の環境用の属性は書かない
	* デフォルト値として書いておくのはOK
* ミドルウェアのバージョンや実行ユーザやlog pathなどほとんど変わらないものを書く

### [C] data_bags/{ENV}/env/*.json

* 環境に依存する属性を記述する
	* Endpoint
	* デプロイするソースのリポジトリやリビジョン
	* ID/PASS, API_KEY/SECRET
	* HOST名やPATH
	* etc.
* 'env'の部分は何でも良い

### [D] roles/*.rb

* Role毎に異なる値を書く
	* 例えば、apacheのhttpd.confの種別名など
		* その種別名で templates や値セットを切り替えるようにrecipeに書いたりしておく
* 属性を default_attributes に入れることができる
* 必要があれば、環境依存の処理を入れる（なるべくしない）

### [E] nodes/*.json 
* 同一Instance Roleでも異なる属性を記述する

属性をよりLocalな箇所で更新する
--------------

A,B はシステム的には同じものなので、同一名前空間に上書きしたりしない方が良いです（結果がわかりにくい）。
A,B の値を Dのタイミングで default_attribtues というメソッドで上書きすることができます。
その際に Cの値を読み込んでおいてdefault_attributesに入れるようにしておくと良いです。
必要があればCの値をここで書き換えることもできます。
このようにdata_bagsの値も node 経由でアクセスするようにすると何かと便利じゃないかと思います。

あまりこういうやり方は一般的では無いのかもしれませんが、意外とこの方法は便利なのではないかと思っています。

default_attributes は Chef実行時のJSON([E])でさらに上書きされます。
ので、属性値は
`A,B -> C -> D -> E`
という順序で上書きされていくことになります。

roles/*.rb は例えば以下のようにします。

```roles/front-web.rb
# data_bags から値を読み込む
attrs = Chef::DataBag.load('env')

#
# 必要があれば、ここでattrsを書き換えることもできる
# Host名とPATHを組み合わせて BASE_URLを作るなど
#

#
# Role依存の値はここで attrs に入れる
#

# ↓recipeの属性を上書きする（但し更にEの値で自動的に上書きされる）
default_attributes (attrs)
```

data_bags を環境別に読み込ませるために、 knife.rb を少し細工しておきます。
knife を実行する際に、KNIFE_ENVという環境変数に環境の値(productionなど）をセットして実行し、それによってdata_bagの読み込みを切り替えます。

```.chef/knife.rb
env = ENV['KNIFE_ENV'] || 'production'

cookbook_path ["cookbooks", "site-cookbooks"]
node_path     "nodes"
role_path     "roles"
data_bag_path "data_bags/#{env}"
encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"
```

knife solo の実行は例えば以下のようになります。

```
KNIFE_ENV=production knife solo cook MY_INSTANCE -r 'role[front-web]'
```

属性の名前空間
---------------------

属性の名前空間は、なるべく慎重に設計しておいた方が良いです。
JSONのような階層構造を持つことができるので、どういう構造にするかよく悩むところです。
これについては正解は無いと思いますが、
DRYはもちろんのこと、一覧性や変更のパターンによっても良い構造というのは変わってくるような気がします。

例えば以下の様な構成があるでしょう。

* ミドルウェア名/{ROLE}/設定/...
	* httpd/front-web/hostname...
	* httpd/default/...
	
* {Role}/ミドルウェア名/設定/...
	* app-mail/postfix/aliases/...


最初の書き方のほうが、似たような設定が1箇所に集まりやすいのでわかりやすそうですが、
後者のような書き方も無くは無いのかなと思います。

特にアプリケーションのデプロイまでChefでやる場合は、
endpoint, credential, デプロイするソースコードの指定などは通常指定することになるので、
どう書くかは最初から検討しておいた方がいいです。

例えばこんな感じ。この辺の階層の順番はいつも悩ましいものです。

* endpoint/{ミドルウェア名}/(type)/...
	* endpoint/mysql/center/{host,port,...}
	* endpoint/memcached/web/{host, port...}
	* endpoint/s3/image/{bucket, url ...}

* credentials/{ミドルウェア名}/...
	* credentials/mysql/super-user/{id,pass}

* deploy/{リポジトリ名}/{uri, revision}...
	* deploy/web-system/{uri, revision}...


たぶんアンチパターン について
=================

環境別の属性を roles/*.rb に記述する
----------------------------------------------

* 「環境 × Role」 の数だけファイルができるので大きなシステムだとメンテナンスが辛くなります
* endpointなどは複数のRoleで参照することになるが、DRYを維持するためにもう一工夫必要になります
* run_list の指定などがDRYで無くなる他、必ず rolesを経由しないとレシピが実行できないことがあります
	* run_listを環境で変えたい場合はあるが、割りとレア(環境によって中身が違うということなので)なのでそこは `case attrs[:env] …` などで場合分けすることで妥協します

recipeのdefault.rb に全部書いてしまう
---------------------------------------------------

場合によって実行したくないレシピや内容を置き換えたい場合があるので、
default.rb では `inlucde_recipe` や `defineの呼び出し` 等で基本レシピセットを指定するようにしておかないと、融通が効かなくなることが多いです。

recipeの中で属性を指定してしまう
---------------------------------------------

絶対に不変な値以外はattributesの方に書いて node から呼び出すほうが良いです。
まあ、レシピが仕上がってから、外出ししたら良いと思います。