これからはじめるChefのチュートリアル　基本編

## はじめに
次の現場でChefを使う可能性が出てきたので、ちょっと真面目にレシピを書き残しておく。
前回は以下の事を記載しました。

* [初心者によるchef-solo,vagrant,berkshelf等で作るrails+mysqlの環境](http://qiita.com/syou007/items/04e1f8a4439a69b6dbb2)
* [初心者によるchef-solo,vagrant,berkshelf等で作るrails+mysqlの環境 その２](http://qiita.com/syou007/items/469286077d2b6561720f)

これらの記事はまとまった記事では無いので、今回はしっかりまとめた記事を書きます。
最終的には企業レベルで使える（と思われる）レシピの書き方を記事にしていきます。

※本記事はMacを使って確認をしてます。
※本記事は`Chef-Solo`について書いてます。

## 事前準備
事前に以下のものを入れておいてください。

* VirtualBox
* Vagrant

最終的にはAWSの`Amazon-Linux`へ環境構築する想定で進めますので、上記を入れずに直接`Amazon-Linux`を使用しても問題ありません。
`Docker`などの仮想OSを使っても問題ありません。
→DockerでChefを流す[実験](http://qiita.com/syou007/items/77e3c295f44de991abf6#%E3%81%8A%E3%81%BE%E3%81%91)をしてみました。

* Homebrew
* Ruby
* Bundle(rubyのGem)

`Homebrew`は無理に入れる必要はありませんが、Macで行う場合は入れておいた方が良いです。
`Ruby`ははじめに入っているものでも問題ありません。

```sh
$ gem install bundle
```

こちらを行なっておいてください。

## 仮想OSの準備
Chefは冪等性（平たく言うと、何度実施しても同じ結果になる事）が保証されていますが、いきなり本番でテストするのは危険が伴います。
（なぜなら、自分が作ったレシピに冪等性が保証されていると言う確証が無いためです。）
ですので、仮想OSもしくはAWSの無料枠などを準備します。

以下にVagrantを使った仮想OS構築の手順を記載しておきます。

```sh
mkdir opscode-centos65
cd opscode-centos65
vagrant init opscode-centos-6.5 http://opscode-vm-bento.s3.amazonaws.com/vagrant/virtualbox/opscode_centos-6.5_chef-provisionerless.box
```

起動前に設定内容を書き換えます。
（SSHでアクセスできれば良いので、適当に設定します。）

```sh
vi Vagrantfile
```

内容を以下のように書き換えます。

```rb
Vagrant.configure(2) do |config|
  # コメントアウトを外します。（必要に応じてIPアドレスを変更してください。）
  config.vm.network "private_network", ip: "192.168.33.10"
end
```

仮想OSを起動して[SSH](http://qiita.com/syou007/items/9bbc1b50b9608651c390)でアクセスできるようにします。
※以下の例は`chef-centos65`と言う名前でアクセス出来るようにしてます。

```sh
$ vagrant up
$ vagrant ssh-config --host chef-centos65 >> ~/.ssh/config
```

念のためSSHでアクセスできる事を確認します。
アクセス後に`sudo`が使用できることを確認します。

```sh
$ ssh chef-centos65
$ sudo ls
$ exit
```

仮想OSは環境構築のテストでしか使用しないので、最後のSSH接続+SUDOができていればどのような環境でもOKです。

## Chefリポジトリの準備
ここからが本番です。
仮想OSのディレクトリとは別に`Chef`用のディレクトリを用意します。

```sh
$ mkdir chef-rep
$ cd chef-rep
```

必要なプログラムをインストールします。

```sh
$ vi Gemfile
```

以下の内容を記載します。

```rb
source "https://rubygems.org"

gem 'chef', '~> 12.0'
gem 'knife-solo'
# 公開されているレシピを使用するために必要
gem 'berkshelf'
# 暗号化が必要な場合は以下のGemも入れる。（今回は使わない。）
gem 'knife-solo_data_bag'
```

プログラムをインストールします。
他のプログラムに影響しないように、`vendor`へ入れます。

```sh
$ bundle install --path=vendor/bundle
```

`Chef`のリポジトリを作成する。

```sh
$ bundle exec knife solo init .
```

これで`Chef`のリポジトリが出来上がりました。
以下の処理はこのディレクトリを使って行なっていきます。

## Berkshelfの設定
`Chef`はクックブックの中にレシピを作成していきます。
構図のイメージとしては以下のような感じです。

```
・カレーライスのクックブック
　・標準レシピ
　・和風カレーライスレシピ
　・インドカレーレシピ
　・タイカレーレシピ
・鍋のクックブック
　・ポン酢で作るおいしい鍋のレシピ
　・闇鍋の作り方レシピ
```

これらのクックブックを作っていくのですが、自分でこれらのクックブックを全て作るのは大変です。
`クックパッド`のようにレシピがまとまってれば、それを使いたいものです。
その要望に応えたものが`Berkshelf`になります。

今回はこれを使います。
では、早速設定しましょう。

```sh
$ vi Berksfile
```

すでにファイルが作成されていると思います。
以下のように変更します。

```rb
source "https://supermarket.getchef.com"

cookbook 'yum-epel'
# rbenvのクックブック内にレシピがあるので、不要かも・・・。
cookbook 'ruby_build'
cookbook 'rbenv'
# 例えばimagemagickのクックブックを使いたい場合は以下のように設定する。
cookbook 'imagemagick'
```

定義したクックブックをインストールします。

```sh
$ bundle exec berks install
```

`~/.berkshelf/cookbooks/`にクックブックが作成されてます。

## 環境構築　その１　冪等性について
ここまで設定した内容を実際に仮想OSへ反映させます。
サーバーを指定する場合は`nodes`の中に記載します。

```sh
# chef-centos65は「ssh chef-centos65」でアクセスできる名前をつけます。
$ vi nodes/chef-centos65.json
```

実施するレシピを記載します。
クックブックのみを指定した場合は、クックブックの中の`default.rb`が使われます。
とりあえず`Berkshelf`で落としてきた`rbenv`をインストールする設定を書きます。

```json
{
  "run_list": [
    "rbenv"
  ]
}
```

実行する前に、対象サーバーが`Chef-Solo`を実行できるように対象サーバーへ`Chef-Solo`をインストールします。

```sh
$ bundle exec knife solo prepare chef-centos65
```

実際に`rbenv`を入れます。

```sh
$ bundle exec knife solo cook chef-centos65
```

コンソールを見ていると、自分が設定していない`yum`などがインストールされていく様子がみられます。
`chef`の素晴らしいところはこのような依存関係を解決してくれるところです。

実際にインストールされているか確認してみます。

```sh
$ ssh chef-centos65
$ rbenv -v
rbenv 0.4.0-210-gf0e8bdc
```

次にインストールできるRubyのバージョンを確認します。

```sh
$ rbenv install --list
rbenv: no such command `install'
```

しかし、installコマンドは使えません。
`ruby_build`を入れ忘れたためです。

先ほどの`nodes`ファイルを書き直します。

```sh
# SSHから抜けます。
$ exit
$ vi nodes/chef-centos65.json
```

これで先ほどのファイルを以下のように書き直します。

```json
{
  "run_list": [
    "rbenv",
    "ruby_build"
  ]
}
```

再度環境設定を行います。

```sh
$ bundle exec knife solo cook chef-centos65
```

さて、今度はどうでしょうか？

```sh
$ ssh chef-centos65
$ rbenv install --list
Available versions:
  1.8.6-p383
  ...
  topaz-dev
```

今度は入っている事が確認できました。
このように、設定を変更して`cook`を行っても環境が壊れる事なく同じ結果が反映されます。
試しに`vagrant destroy`と`vagrant up`をして`cook`を再度行っても同じ結果になります。
これが`冪等性`です。

この性質があるため、複数台のサーバーに対して`cook`を行うと同じ設定のサーバーを量産する事ができます。

## 環境構築　その２　レシピを作る
では肝心の`ruby`コマンドは使えるでしょうか？

```sh
$ ruby -v
-bash: ruby: コマンドが見つかりません
```

残念ながらまだrubyは使えません。

```sh
$ rbenv install [rubyバージョン]
```

上記のコマンドを行えば`ruby`は入りますが、サーバー内で何かしらの作業を行ってしまうと`chef`の良さがなくなります。
もし５台のサーバーに対して環境構築を行う場合には、５台のサーバーに入って上記のコマンドを打たなければなりません。

これは`chef`の利便性や冪等性の観点から見ても、NGであることは明らかです。

これを解決するためのレシピを作りましょう。

```sh
# SSHから抜けます。
$ exit
```

現状を確認します。

```sh
$ ls -l cookbooks
$ ls -l site-cookbooks
```

確認すると`cookbooks`に様々なファイルが存在してることがわかります。
これは`knife`の中身を見るとわかります。

```sh
$ cat .chef/knife.rb 
cookbook_path    ["cookbooks", "site-cookbooks"]
node_path        "nodes"
role_path        "roles"
environment_path "environments"
data_bag_path    "data_bags"
#encrypted_data_bag_secret "data_bag_key"

knife[:berkshelf_path] = "cookbooks"
Chef::Config[:ssl_verify_mode] = :verify_peer if defined? ::Chef
```

この中の`knife[:berkshelf_path] = "cookbooks"`で`Berkshelf`が使用するクックブックを指定しているためです。
また、`cookbook_path    ["cookbooks", "site-cookbooks"]`にクックブックのパスが定義されていることから`site-cookbooks`に自分のレシピを作れば良いことがわかります。

早速作っていきましょう！

```sh
$ bundle exec knife cookbook create ruby -o site-cookbooks/
```

`ruby`のバージョンを指定するためのクックブックとして`ruby`を作成します。
作成したら中身を記載します。

```sh
$ vi site-cookbooks/ruby/recipes/default.rb
```

内容を記載します。

```rb
# インストールするバージョンを指定します。
rbenv_ruby "2.2.3" do
  ruby_version "2.2.3"
  # グローバルとして使用するかを定義します。
  global true
end
```

`node`に追加します。

```sh
$ vi nodes/chef-centos65.json
```

最後に`ruby`を追加します。

```json
{
  "run_list": [
    "rbenv",
    "ruby_build",
    "ruby"
  ]
}
```

サーバーに反映させます。

```sh
$ bundle exec knife solo cook chef-centos65
```

`ruby`をインストールするため実施には時間がかかります。

```sh
$ ssh chef-centos65
$ ruby -v
ruby 2.2.3p173 (2015-08-18 revision 51636) [x86_64-linux]
$ exit
```

`ruby`がインストールされました。

## 環境構築　その３　依存関係
このレシピにはいくつか問題があります。
例えば`nodes/chef-centos65.json`の順番を間違えてしまったり、書き忘れた場合はどうなるでしょうか？

```json:nodes/chef-centos65.json
{
  "run_list": [
    "ruby",
    "rbenv",
    "ruby_build"
  ]
}
```

または以下のケース

```json:nodes/chef-centos65.json
{
  "run_list": [
    "ruby"
  ]
}
```

このように記載されても実行できるように、依存関係を記載しておきます。

```rb:site-cookbooks/ruby/recipes/default.rb
# 使用するレシピを事前に記載しておきます。
include_recipe "rbenv"
include_recipe "ruby_build"
# レシピも指定する場合は以下の記述になる。
#include_recipe "rbenv::default"
#include_recipe "ruby_build::default"

# インストールするバージョンを指定します。
rbenv_ruby "2.2.3" do
  ruby_version "2.2.3"
  # グローバルとして使用するかを定義します。
  global true
end
```

さらに使用するレシピのデフォルト値を読み込むために以下を記載します。

```rb:site-cookbooks/ruby/metadata.rb
depends "rbenv"
depends "ruby_build"
```

これで`node`が以下のような状態になっていても実行されるようになります。

```json:nodes/chef-centos65.json
{
  "run_list": [
    "ruby"
  ]
}
```

シンプルになりましたね！
実際に仮想OSを作り直して、上記の状態で`cook`をしても同じ状態になることを確認してください。

### 参考
[include_recipeだけでは読み込み先のattributesが読み込まれない](http://qiita.com/mistymagich/items/5d6c429863e950ed92cf)

### 注釈
実際に使用する際は、ここまで行う必要はほとんどありません。
しかし、こういう方法もあるということを覚えておくとレシピの書き方に幅が出てきます。

## 環境構築　その４　汎用化
さて、このレシピにはまだ問題があります。
それはRubyのバージョンが`2.2.3`というように固定されてしまっていることです。

今のままでは良いかもしれませんが、将来的に新しいRubyのバージョンを入れる事を考えるとこのままではよろしくありません。

各レシピに設定値を受け渡せれれば問題が解決します。
`chef`では以下の記述を行う事で設定値を記載できます。

```json:nodes/chef-centos65.json
{
  "ruby": {
    "version": "2.2.3"
  },
  "run_list": [
    "ruby"
  ]
}
```

上記で設定した値を使用して構築できるようにレシピを書き直します。

```rb:site-cookbooks/ruby/recipes/default.rb
include_recipe "rbenv"
include_recipe "ruby_build"

rbenv_ruby node["ruby"]["version"] do
  ruby_version node["ruby"]["version"]
  global true
end
```

ではサーバーに反映してみましょう！
（反映を確認する場合は、Rubyのバージョンを`2.2.2`などにすると確認ができます。）

```sh
$ bundle exec knife solo cook chef-centos65
```

このままでも良いのですが、せっかく`rbenv`を使っているのに固定されたバージョンしか設定できないのは不便です。
以下のようなパラメーターを受け付けれるように変更しましょう。

```json:nodes/chef-centos65.json
{
  "ruby": {
    "versions": [
      {
        "version": "2.2.3",
        "global": "true"
      },
      {
        "version": "2.2.2"
      }
    ]
  },
  "run_list": [
    "ruby"
  ]
}
```

レシピも変更します。
ついでに必ず使うgemである`bundle`も入れておきます。

```rb:site-cookbooks/ruby/recipes/default.rb
include_recipe "rbenv"
include_recipe "ruby_build"

node['ruby']['versions'].each { |v|
  # 指定バージョンのrubyをインストール
  rbenv_ruby v['version'] do
    ruby_version v['version']
    global true if v["global"]
  end
  
  # bunldleを設定する。
  rbenv_gem "bundler" do
    ruby_version v['version']
  end
}
```

設定を反映させてみましょう！

```sh
$ bundle exec knife solo cook chef-centos65
```

### レシピの初期値
このレシピを使うときには必ず`ruby`のバージョンを指定する必要があります。
しかし、実際にレシピを使う時にバージョンはあまり重要ではない局面もあります。

そういう時にまでバージョンを指定を強制すると使い勝手の悪いレシピが出来上がってしまいます。

```json:nodes/chef-centos65.json
{
  "run_list": [
    "ruby"
  ]
}
```

この状態でも実行できるように以下のファイルを作成します。
ここで指定した値がデフォルトで使用されます。

```rb:site-cookbooks/ruby/attributes/default.rb
default["ruby"]["versions"] = [{"version" => "2.2.3", "global" => true}]
```

### 参考
[ruby_rbenvのgithub](https://github.com/chef-rbenv/ruby_rbenv)

ところで`rbenv_ruby`コマンドとかってどこに定義されているのだろうか・・・。
レシピに存在しないのに使える・・・というところが謎である。。。

## まとめ
いかがだったでしょうか？
基本的な使い方に関してはこの記事の内容だけで十分使えると思います。
しかし、ここまでの内容ではまだ企業で使うためには不十分な内容となってます。

本当は環境構築のその５以降も書いていこうと思ったのですが、思ったよりも長くなってしまったので今回の記事を基本編として次回の記事を応用編で書いていこうと思います。

応用編を実践すればより使いやすいChefのレシピが書けるようになると思いますので、ご期待ください！

## 次は・・・
[これからはじめるChefのチュートリアル　応用編](http://qiita.com/syou007/items/e753f1f84c7b6b45f4ea)を書きましたのでこちらも続けてやってみてください！

## 筆者について
筆者は[Townsoft](http://townsoft.jp)を屋号に活躍（？）しているフリーランスです。
お仕事の依頼なども受けてますので、お気軽にご連絡ください。