Cookbook開発の基本ワークフロー

先日まで、複数のcookbookを含めたchef-repo全体をgitリポジトリ化して運用していたという非常に アンチパターンな運用をしていて、このたびきちんとcookbook単位でバージョン管理しようと決意。 ついでにChef-soloもやめてChef serverの使い方を覚えた。

単一Cookbookをchef-repoなしに独立に開発する流れがどこにもまとまっていなかったのでメモ。 いちおう自分の考える王道のようなものになっているはず。

Chefはchefdkでインストールしてあるものとする。

今回はサーバーの基本設定をまとめたbase-settingsというcookbookの作成を例に取る。

Cookbookの作成

新しいCookbookを作る場合は以下のコマンドで作る。

$ knife cookbook create base-settings -o .
WARNING: No knife configuration file found
** Creating cookbook base-settings in /Users/degawaikuo/workspace
** Creating README for cookbook: base-settings
** Creating CHANGELOG for cookbook: base-settings
** Creating metadata for cookbook: base-settings

このコマンドはchef-repoのトップディレクトリで使うことを想定されているようで、 デフォルトだと./cookbooksディレクトリを探してそこにcookbookをつくろうとするので、 -oオプションでカレントディレクトリに作成場所を指定する。

$ cd base-settings
$ ls
CHANGELOG.md README.md    attributes   definitions  files
libraries    metadata.rb  providers    recipes      resources    templates

さっき「knifeの設定がないよ」と言われたので下のように作成する。 この単一cookbook開発ではChef Serverを使わないのでlocal_modeをtrueにしておく。 これで-zオプションが不要になり、 ERROR: Your private key could not be loaded from /etc/chef/client.pemのエラーがなくなる。 local_modeで使っていると.chef/local-mode-cacheができるのでこいつをgitignoreしておく。

$ mkdir .chef/
$ echo "local_mode true" > .chef/knife.rb
$ echo "/.chef/local-mode-cache" >> .gitignore

ひととおりファイルができたところでgit initしてバージョン管理に乗せる。 GitHubへの登録は省略。

$ git init 
Initialized empty Git repository in /path/to/cookbook/base-settings/.git/
$ git add .
$ git commit -m'initial commit'
[master (root-commit) 1c73a3b] initial commit
 4 files changed, 96 insertions(+)
 create mode 100644 CHANGELOG.md
 create mode 100644 README.md
 create mode 100644 metadata.rb
 create mode 100644 recipes/default.rb

test-kitchenの設定

cookbookのテストは基本的にtest-kitchenの仮想マシン上で行う。Vagrantfileは作らない。 test-kitchenの設定ファイルである.kitchen.ymlにrun_listやテストスイートをなどを細かく書けて非常に便利なので 手動でvagrantコマンドをうつよりもkitchen testする方がオススメ。

$ kitchen init
IkuosMacmini:base-settings degawaikuo$ kitchen init
      create  .kitchen.yml
      create  test/integration/default
      append  .gitignore
      append  .gitignore
      append  .gitignore
Successfully installed kitchen-vagrant-0.15.0
1 gem installed

.gitignoreに追記され.kitchen.ymlが新しく追加された。

.kitchen.ymlはtest-kitchenで使う種々の設定を書くファイルで非常に重要。デフォルトでは以下のようになっている。

---
driver:
  name: vagrant

provisioner:
  name: chef_solo

platforms:
  - name: ubuntu-12.04
  - name: centos-6.4

suites:
  - name: default
    run_list:
      - recipe[base-settings::default]
    attributes:

driverは仮想マシンを立ち上げる環境を指定する。デフォルトだとvagrantなのでこれでよい。 他にもec2やdockerを指定できるらしい。

platformsはテストするサーバーOSを指定する。指定した数だけ仮想マシンが立ち上がるので、 開発マシンのスペックと相談かな。

provisionerがdeprectedなchef_soloなのでこれをchef_zeroにする。 あとplatformsのOSのバージョンが古いので新しくする。 結果以下のようになる。

---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.10
  - name: centos-6.6

suites:
  - name: default
    run_list:
      - recipe[base-settings::default]
    attributes:

ここまで書けばkichen testが動くので動かしてみる。 新しく仮想マシンを立ち上げるときに結構時間かかるので、仮想マシン壊さないように--destroy=neverをつけるととっても捗る。 あと-cをつけると複数の仮想マシンを並列にテストしてくれるので速い。

$ kitchen test --destroy=never -c

だいぶ時間をかけて、vagrantで2つの仮想マシンが立ち上がり、それぞれにVirtualBoxのGuestAdditionsがインストールされ、 さらにchef-clientがインストールされる。仮想マシンごとに色分けして出力されるのがわかりやすい。 recipeもテストコードはなにも書いていないので何も実行されない。

ここまででtest-kitchenの最低限の設定は完了。この辺りでgit commitしておくとよい。

レシピを書いてテストコードを回す

設定がだいたい終わったので、早速テストコードとrecipeを書く。 テストコードはserverspecで書くので、

/test/integration/default/serverspec/user_spec.rb

require 'serverspec'
set :backend, :exec

describe user('ikuwow') do
    it { should exist }
    it { should belong_to_group 'ikuwow' }
    it { should have_home_directory '/home/ikuwow' }
end

/recipes/default.rb

include_recipe "base-settings::user"

base-cookbook/recipes/user.rb

group "ikuwow" do
    action :create
end

user "ikuwow" do
    gid "ikuwow"
    home "/home/ikuwow"
    action :create
end

recipeが書けたらkitchen testする前にまずknife cookbook test base-settings -o ..するとよい。 文法チェックだけしてくれる。 -oオプションではcookbookの設置されているディレクトリを指定する必要があることに注意。

$ knife cookbook test base-settings -o ..
checking base-settings
Running syntax check on base-settings
Validating ruby files
Validating templates

それではkitchen test

$ kitchen test --destroy=never -c
(省略)
-----> serverspec installed (version 2.10.2)
       /opt/chef/embedded/bin/ruby -I/tmp/busser/suites/serverspec -I/tmp/busser/gems/gems/rspec-support-3.2.2/lib:/tmp/busser/gems/gems/rspec-core-3.2.2/lib /opt/chef/embedded/bin/rspec --pattern /tmp/busser/suites/serverspec/\*\*/\*_spec.rb --color --format documentation --default-path /tmp/busser/suites/serverspec

       User "ikuwow"
         should exist
         should belong to group "ikuwow"
         should have home directory "/home/ikuwow"

       Finished in 0.09826 seconds (files took 0.27158 seconds to load)
       3 examples, 0 failures

       Finished verifying <default-ubuntu-1410> (0m14.02s).
       Finished testing <default-ubuntu-1410> (14m58.27s).
-----> Kitchen is finished. (14m58.54s)

無事にレシピを適用できテストも通過した。 神経質な人はkitchen login default-centos-66でsshログインして手動で確認すればよい。

依存cookbookの解決

supermarket.chef.ioにあるコミュニティクックブックはぜひ積極的に使いましょう。 車輪の再発明は悪。先人の知恵は活用すべし。 ただサービスを動かすサーバーなどセキュリティ要件が大きい場合はよくクックブックの中身を読むこと。 自分はなるべくダウンロードが多いものを選ぶようにしている。

今回は以下のSELinuxのクックブックを使う。 Chef公式でかつダウンロード数がずば抜けて多かったので信用度高い。

https://supermarket.chef.io/cookbooks/selinux

追加が必要なのはmetadata.rbBerksfile

metadata.rbを編集して依存クックブックを記入する。 dependsという項目に依存cookbookを書く。 ついでにmaintainerなどの必要事項を書き足しておく。

name             'base-settings'
depends          'selinux', '~> 0.9.0'
maintainer       'ikuwow'
maintainer_email 'ikuwow@example.com'
license          'All rights reserved'
description      'Installs/Configures base-settings'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '0.1.0'

またBerksfileを作成して以下のようにする。berks initで様々作成できるがこれは使わない。

source "https://supermarket.chef.io"

metadata

このmetadataというのがポイント。 これを書くとmetadata.rbのdependsを確認して、 test-kitchenを使った時にBerkshelfで依存cookbookをダウンロードしてくれる。 つまりこの状態でselinuxクックブックが使えるようになったというわけ。

recipes/default.rbを以下のように編集。

include_recipe "base-settings::user"

include_recipe "selinux::disabled"

そしてkitchen converge。少し長いけどcentosの方のみ変更があったのでこちらを載せる。

$ kitchen converge
(ubuntuは省略)
-----> Converging <default-centos-66>...
       Preparing files for transfer
       Preparing dna.json
       Resolving cookbook dependencies with Berkshelf 3.2.3...
       Removing non-cookbook files before transfer
       Preparing validation.pem
       Preparing client.rb
-----> Chef Omnibus installation detected (install only if missing)
       Transferring files to <default-centos-66>
       Starting Chef Client, version 12.1.1
       [2015-03-21T15:47:39+00:00] WARN: Child with name 'dna.json' found in multiple directories: /tmp/kitchen/dna.json and /tmp/kitchen/dna.json
       resolving cookbooks for run list: ["base-settings::default"]
       Synchronizing Cookbooks:
         - base-settings
         - selinux
       Compiling Cookbooks...
       Converging 5 resources
       Recipe: base-settings::user
        (up to date)
         * user[ikuwow] action create (up to date)
       Recipe: selinux::_common
        (up to date)
         * directory[/etc/selinux] action create (up to date)
       Recipe: selinux::disabled
        (up to date)
         * execute[selinux-disabled] action run


           - update content in file /etc/selinux/config from 2b3b43 to 8a6a4a
           --- /etc/selinux/config  2015-02-24 14:25:26.023999934 +0000
           +++ /tmp/chef-rendered-template20150321-8033-ndcsnv  2015-03-21 15:47:42.136045491 +0000
           @@ -1,14 +1,12 @@
           -
            # This file controls the state of SELinux on the system.
            # SELINUX= can take one of these three values:
           -#     enforcing - SELinux security policy is enforced.
           -#     permissive - SELinux prints warnings instead of enforcing.
           -#     disabled - No SELinux policy is loaded.
           -SELINUX=permissive
           -# SELINUXTYPE= can take one of these two values:
           -#     targeted - Targeted processes are protected,
           -#     mls - Multi Level Security protection.
           -SELINUXTYPE=targeted
           -
           +#       enforcing - SELinux security policy is enforced.
           +#       permissive - SELinux prints warnings instead of enforcing.
           +#       disabled - SELinux is fully disabled.
           +SELINUX=disabled
           +# SELINUXTYPE= type of policy in use. Possible values are:
           +#       targeted - Only targeted network daemons are protected.
           +#       strict - Full SELinux protection.
           +SELINUXTYPE=targeted

           - restore selinux security context

       Running handlers:
       Running handlers complete
       Chef Client finished, 2/7 resources updated in 4.01651555 seconds
       Finished converging <default-centos-66> (0m7.70s).
-----> Kitchen is finished. (0m29.39s)

Berksfileでcookbokの依存パケージが解決され、仮想マシンに同期されている様子がわかる。 無事にSELinuxの無効も済んだよう。

テストコードselinux_spec.rbを後から書いた。

require 'serverspec'
set :backend, :exec

describe selinux do
    it { should be_disabled }
end

git commitする前にBerksfile.lockはバージョン管理しないことが推奨されるので、 .gitignoreに追加しておいたほうがよい。

attributesの使い方

environmentsやrolesやnodesオブジェクトに書くattributesも.kitchen.ymlで指定することで利用できる。 ここではコミュニティクックブックのうちopensshを使って、 SSHのルートログイン不許可とパスワード認証無効を設定する。 これもChef謹製で2009年に作成されて着々とアップデートされているので信頼できそう。

https://supermarket.chef.io/cookbooks/openssh

このcookbookはattributeに細かい設定をするようになっている。

依存クックブックを解決するためにmetadata.rbに追加。

name             'base-settings'
depends          'selinux', '~> 0.9.0'
depends          'openssh', '~> 1.3.4'
maintainer       'ikuwow'
maintainer_email 'ikuwow'
license          'All rights reserved'
description      'Installs/Configures base-settings'
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
version          '0.1.0'

そんでrecipeに追記。

include_recipe "base-settings::user"

include_recipe "selinux::disabled"

include_recipe "openssh::default"

とりあえずattributesを書かない状態で適用してみる。

$ kitchen test --destroy=never -c 

成功したのを確認したら.kitchen.ymlにattributeを書いていく。

どのような形でattributesを書けばいいかはソースのattributesを見るとわかる。 nodesやenvironmentsにはjsonで書くがこちらはyamlなので注意。

https://github.com/opscode-cookbooks/openssh/blob/master/attributes/default.rb

.kitchen.yaml

---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.10
  - name: centos-6.6

suites:
  - name: default
    run_list:
      - recipe[base-settings::default]
    attributes:
      openssh: 
        server:
          password_authentication: "no"
          permit_root_login: "no"

これでkichen convergeすれば望んだ状態になるはず。

$ kitchen converge 
〜(省略)〜
       Recipe: openssh::default
         * apt_package[openssh-client] action install (up to date)
         * apt_package[openssh-server] action install (up to date)
         * service[ssh] action enable (up to date)
         * service[ssh] action start (up to date)
         * template[/etc/ssh/ssh_config] action create (up to date)
         * template[/etc/ssh/sshd_config] action create
           - update content in file /etc/ssh/sshd_config from 330f51 to d89d12
           --- /etc/ssh/sshd_config 2015-03-22 00:52:55.336641973 +0000
           +++ /tmp/chef-rendered-template20150322-1880-18smxl  2015-03-22 01:19:51.899348600 +0000
           @@ -3,5 +3,7 @@

            ChallengeResponseAuthentication no
            UsePAM yes
           +PasswordAuthentication no
           +PermitRootLogin no

         * service[ssh] action restart
           - restart service service[ssh]

       Running handlers:
       Running handlers complete
       Chef Client finished, 2/12 resources updated in 6.559877352 seconds
       Finished converging <default-ubuntu-1410> (0m10.03s).

/etc/ssh/sshd_configに二行追加されて、restartがnotifyされて無事適用された。

data bagsの使い方

これは.kitchen.ymlにdata bagsを置くディレクトリを指定して、その場所にdata_bagsを作ってやればよい。 あくまでテスト用のdata bagsなので、test/ディレクトリ以下に入れるとよいかも。 ここではdata_bag userにdata_bag_item ikuとwowを追加して、これを使ってユーザー作成を行う。

.kitchen.ymlは以下のようになる。

---
driver:
  name: vagrant

provisioner:
  name: chef_zero

platforms:
  - name: ubuntu-14.10
  - name: centos-6.6

suites:
  - name: default
    run_list:
      - recipe[base-settings::default]
    attributes:
      openssh: 
        server:
          password_authentication: "no"
          permit_root_login: "no"
    data_bags_path: test/data_bags

次にdata bagの作成。-oオプションでdata_bagsディレクトリの場所を指定できたらいいのだがどうやら存在しないようなので、 cdしてからknife data bag create ...してdata bagを作る。

$ cd test
$ knife data bag create users iku
(vimが立ち上がるので以下のように編集する)
{
  "id": "iku",
  "comment": "ゐく"
}
(:q)
Created data_bag[users]
Created data_bag_item[iku]
$ knife data bag create users wow
(vimが立ち上がるので以下のように編集する)
{
  "id": "wow",
  "comment": "を"
}
(:q)
Data bag users already exists
Created data_bag_item[wow]

そしてrecipeとテストコードを書く。

recipes/users.rb

group "ikuwow" do
    action :create
end

user "ikuwow" do
    gid "ikuwow"
    home "/home/ikuwow"
    action :create
end

user_list = data_bag('users')

user_list.each do |ul|
    u = data_bag_item('users',ul)
    user u["id"] do
        comment u["comment"]
        home "/home/#{u["id"]}"
        action :create
    end
end

test/integration/default/serverspec/users_spec.rb

require 'serverspec'
set :backend, :exec

describe user('ikuwow') do
    it { should exist }
    it { should belong_to_group 'ikuwow' }
    it { should have_home_directory '/home/ikuwow' }
end

%w{iku wow}.each do |u|
    describe user(u) do
        it { should exist}
    end
end
$ kitchen converge && kitchen verify

これでikuユーザーとwowユーザーが作成される。

まとめ

metadataとBerksfileでcookbookの依存は解決でき、あとは.kitchen.ymlに必要なことを書けばよいということで、 非常にわかりやすい手順が得られたかなと満足している。 依存クックブックやattributesも一つのcookbook内で完結させられるというのがよいですね。

補足として、run_listをnodesやrolesに書くよりも、 ここで示した例としてinclude_recipeを並べるcookbookを作ったほうがよいようだ。 run_listとrolesはChef Server環境だとバージョン管理を行わない一時ファイルの扱いにするのが普通なので、 バージョン管理するcookbookのrecipeに入れておくと運用しやすい。

ここで作ったbase-settingsはGitHubに上げてあるので参考までに。

https://github.com/ikuwow/base-settings

参考資料