How to make chef cookbook?

はじめに

この記事では chef の cookbook の作り方を説明します。この記事を通して、/etc/foo のパスに bar という文字列が書かれたテキストを作るレシピの作り方および kitchen-docker を使った docker コンテナを利用したテストコードの作成方法を学んできます。レシピの名前は、foobar です。

この文書を作成する時に利用した環境は以下です。異なるバージョンの場合は挙動が違うことがありますのでご了承ください。

macOS 10.15.7
ChefDK version: 4.6.35
Chef Infra Client version: 15.5.17
Docker version 19.03.8, build afacb8b

cookbook の雛形の作成

以下のコマンドでbookbookの雛形を作成します。

% chef generate cookbook foobar                                                                  [~/projects/private]
Generating cookbook foobar
- Ensuring correct cookbook content
- Committing cookbook files to git

Your cookbook is ready. Type `cd foobar` to enter it.

There are several commands you can run to get started locally developing and testing your cookbook.
Type `delivery local --help` to see a full list of local testing commands.

Why not start by writing an InSpec test? Tests for the default recipe are stored at:

test/integration/default/default_test.rb

If you'd prefer to dive right in, the default recipe can be found at:

recipes/default.rb

自動生成ファイルの概要

それでは、自動生成されたファイルを確認しましょう。

  • footer

    • CHANGELOG.md変更履歴をこのファイルに記載します。
    • LICENSEこの cookbook のライセンスを明記します。 Apache License 2.0MIT Licenseなどをコピーし、必要な箇所を書き換えるなどして利用しても良いでしょう。
    • Policyfile.rbこのファイルは cookbook の作成には必須ではありません。このファイルは、この cookbook の利用者がどのようにこの cookbook を実行するのかなどを手助けするための情報を簡潔に記述したものになります。Chef server などがこの情報を元に Chef Client へ run_list などの情報を提供するのに利用されます。Chef 詳細はオフィシャルページを確認してください。
    • README.mdこの cookbook の簡単な説明とオプションで指定できるものの簡単な説明を記載します。この cookbook のユーザはまず、このファイルを確認し何ができる cookbook なのか、どんなことが変更できるのかを確認します。簡潔ですが、必要な項目を記載しておくと使いやすい cookbook になるかと思います。必要であれば、詳細なドキュメントへのリンクをここに記載しておくと良いでしょう。
    • chefignoreこのファイルには cookbook としてパッケージを作り、chef supermarket へ cookbook をアップロードするときに、除外対象とするファイルを指定します。バージョン管理ツールに関わるファイル(.git など)や、bundler 関連ファイルなどを除外ファイルとして追加します。最初からいくつかのファイルが追加されていますが、必要であればここに追記しておきます。
    • kitchen.ymlTest Kitchenという cookbook のテストツールのための設定ファイルになります。次回のブログで Test Kitchen について説明をする予定です。
    • metadata.rbこのファイルは cookbook がサポートしているプラットフォーム、ライセンス、メンテナーなどの cookbook の詳細情報について記載します。
    • recipesこの cookbook のサポートするレシピ(他の cookbook や run_list で指定できる外部から呼び出し可能な実行単位)を格納するディレクトリになります。1つの cookbook に関連した複数のレシピをまとめたいときに、ここに複数のファイルを格納し、必要に応じて run_list で指定して利用します。標準で作られる default.rb という名前のレシピは run_list で cookbook 名のみを指定したときに実行されます。
      • default.rbこの cookbook で利用されるデフォルトのレシピになります。上で記載したように run_list でレシピ名のみを指定した場合に実行されるレシピでもあります。
    • specこの cookbook のユニットテストを格納するディレクトリです。
      • spec_helper.rbunit テスト用のヘルパーファイルになります。ここに unit テスト共通の require ファイルや設定などを記載しします。このファイルを各 unit テストないで読み込むことで重複した記述を避けるようにします。
        • unitunit テストを格納するディレクトリです。
          • recipesレシピ用のユニットテストを格納するディレクトリです。
            • default_spec.rbジェネレーターで作成された unit テストです。今回のブログでは扱いません。
    • testTest Kitchen 用の integration test を格納するディレクトリです。

      • integrationintegration test を格納するディレクトリです。

        • defaultkitchen.yml の default suites(標準構成でのテスト)向けの integration test を格納するディレクトリです。

          • default_test.rb標準構成の integration test ケースを記載するファイルです。

テストファースト

それでは、実際にレシピの開発を始める前にテスト環境を整えます。テストファーストについてはネットで検索をしてもらえれば詳しい説明があると思いますので詳細な説明は他に譲りますが、テストを記述しておくことでレシピを再利用、変更する再に安心できますし、インフラのレシピは息が長いことが多く変更が少ない傾向にあるのでテストが有用だと考えています。

是非面倒臭がらず少しでもテストを書いてみる事を試してみてください。

テストの実行準備

それでは、実際のレシピの開発を始める前に、レシピのテストを実行する環境を整えテストを実行できるようにします。

ここでは実行時間を考え docker を用い ubuntu 20.04 を対象にテストを実行します。docker と kitchen(テスト実行ツール)との接続ドライバは dokken を利用します。

以下の内容 Gemfile を cookbook のトップディレクトリに作成し、bundler を用いてポータブルなテスト環境を実現します。

# frozen_string_literal: true

source 'https://rubygems.org'

gem 'berkshelf'
gem 'kitchen'
gem 'kitchen-dokken'
gem 'kitchen-inspec'

その後、以下のコマンドで必要な gem ファイルをインストールしておきます。

% bundle config set path vendor
% bundle install

また、rbenv または rvm を利用している人は ruby のバージョンも指定しておくと良いと思います。ruby のバージョンを指定した人は、 chefignore へ以下の内容を追加し、 .ruby-version ファイルが chef のパッケージへ登録されないようにしておきます。

.ruby-version

テスト環境の準備と確認

まず最初に、 Policyfile.rb を削除します。このファイルにはレシピのデフォルトの動作を記載するのですが、chef のバグで、このファイルがある状態ですと dokken および chef zero が失敗するという不具合があります。そのため、最初にこのファイルを削除します。

次に kitchen.yml に dokken を使うための設定と対象のプラットフォームの変更をします。

driver:
  name: dokken

transport:
  name: dokken

provisioner:
  name: dokken
  chef_license: accept-no-persist

verifier:
  name: inspec

platforms:
  - name: ubuntu-20.04
    driver:
      image: dokken/ubuntu-20.04

suites:
  - name: default
    run_list:
      - recipe[foobar::default]
    verifier:
      inspec_tests:
        - test/integration/default
    attributes:

今回は対象を ubuntu-20.04 としていますが、 centos-8 など他のプラットフォームも指定できます。

ここまで設定ができたら、以下のコマンドを実行し設定が正しいか確認します。

% bundle exec kitchen list

正しく設定ができていれば以下の出力が表示されます。

Instance             Driver  Provisioner  Verifier  Transport  Last Action    Last Error
default-ubuntu-2004  Dokken  Dokken       Inspec    Dokken     <Not Created>  <None>

Transport の項目が Dokken になっているかを確認してください。ここが間違っていると、kitchen を使って環境を構築するところでエラーが発生するもしくは、ssh で接続しパスワードを入力するプロンプトが表示されてしまいます。

それでは、何もテストを書いていない状態でテストを実行し、成功することを確認します。

% bundle exec kitchen test

正しく設定されていれば、しばらく待つと以下の出力が表示されます。

Test Summary: 0 successful, 0 failures, 2 skipped
       Finished verifying <default-ubuntu-2004> (0m3.69s).
-----> Destroying <default-ubuntu-2004>...
       Deleting kitchen sandbox at /Users/aaa/.dokken/kitchen_sandbox/a255d504ca-default-ubuntu-2004
       Deleting verifier sandbox at /Users/aaa/.dokken/verifier_sandbox/a255d504ca-default-ubuntu-2004
       Finished destroying <default-ubuntu-2004> (0m10.25s).
       Finished testing <default-ubuntu-2004> (1m6.33s).
-----> Test Kitchen is finished. (1m8.02s)

このように表示がされたらテストを実行するための環境設定は完了です。

もし以下のようなエラーが表示される場合は docker をインストールしていない、または起動していない可能性があります。docker が正しくインストール/セットアップされているかご確認ください。

kitchen-dokken could not connect to the docker host at unix:///var/run/docker.sock. Is docker running?

ファイルの確認をするかのテストの作成

それでは、 /etc/foo のファイルが存在するかのテストを書き、実行します。

test/integration/default/default_test.rb のファイルを開き、中身を一度削除して以下の内容を記載します。

# InSpec test for recipe foobar::default

describe file('/etc/foo') do
  it { should be_file }
  it { should be_owned_by 'root' }
  it { should be_grouped_into 'root' }
  it { should be_mode 0o644 }
  its('content') { should match('bar') }
end

せっかくなので、以下の 5 点をチェックするテストを記載しておきます。

  • /etc/foo ファイルが存在するか
  • オーナーは root
  • グループは root
  • ファイルのパーミッションは 644
  • ファイルには bar という文字列がふくまれているか

それでは、ここからはレシピを直して、テストしての繰り返しになるので、コンテナと破棄を繰り返す、kitchen test コマンドでは待ち時間が増えるので以下のコマンドを1ステップずつ実行していきます。

まずは、テスト対象コンテナの作成。

% bundle exec kitchen create

次に作成したレシピの適用

% bundle exec kitchen converge

そして、テストの実行

% bundle exec kitchen verify

以下のような出力結果になると思います。もちろんファイルを作成するレシピを書いていないため、実行する全てのテストが失敗します。まずは、テストが失敗し、正しくテストが実行されている事を確認します(テストファーストで)。

-----> Starting Test Kitchen (v2.8.0)
-----> Setting up <default-ubuntu-2004>...
       Finished setting up <default-ubuntu-2004> (0m0.00s).
-----> Verifying <default-ubuntu-2004>...
       Loaded tests from {:path=>"....foobar.test.integration.default"}

Profile: tests from {:path=>"/.../foobar/test/integration/default"} (tests from {:path=>"....foobar.test.integration.default"})
Version: (not specified)
Target:  docker://86400d9278f3122419897a76ca84ba7312953e2bdd37dbd152eb1e409a1ba6e0

  File /etc/foo
     ×  is expected to be file
     expected `File /etc/foo.file?` to return true, got false
     ×  is expected to be owned by "root"
     expected `File /etc/foo.owned_by?("root")` to return true, got false
     ×  is expected to be grouped into "root"
     expected `File /etc/foo.grouped_into?("root")` to return true, got false
     ×  is expected to be mode 420
     expected `File /etc/foo.mode?(420)` to return true, got false
     ×  content is expected to match "bar"
     expected nil to match "bar"

Test Summary: 0 successful, 5 failures, 0 skipped
>>>>>> ------Exception-------
>>>>>> Class: Kitchen::ActionFailed
>>>>>> Message: 1 actions failed.
>>>>>>     Verify failed on instance <default-ubuntu-2004>.  Please see .kitchen/logs/default-ubuntu-2004.log for more details
>>>>>> ----------------------
>>>>>> Please see .kitchen/logs/kitchen.log for more details
>>>>>> Also try running `kitchen diagnose --all` for configuration

ファイルを作るレシピの作成

それでは、ファイルを作成するレシピを作ります。 今回は、chef の templates リソースを使いファイルを作成します。chef レシピのディレクトリ内に、

templates/foo.erb

というファイルを作成し、bar という文字列だけ書いて保存します。念の為、bar のファイルの中身は以下となります。

bar

次に、レシピを作成します。 recipes/default.rb というファイルを開き、以下の内容を記載します。

#
# Cookbook:: foobar
# Recipe:: default
#
# Copyright:: 2020, The Authors, All Rights Reserved.

template '/etc/foo' do
  source 'foo.erb'
  owner 'root'
  group 'root'
  mode '0644'
end

これは、templates/foo.erb の内容を元に /etc/foo というファイルを作成します。そのときにオーナーは root で、グループも root パーミッションは 0644 とするというレシピとなります。

それでは、このレシピを適応します。

% bundle exec kitchen converge
-----> Starting Test Kitchen (v2.8.0)
-----> Converging <default-ubuntu-2004>...
       Creating kitchen sandbox in /.../.dokken/kitchen_sandbox/a255d504ca-default-ubuntu-2004
       Preparing dna.json
       Preparing current project directory as a cookbook
       Removing non-cookbook files before transfer
       Preparing validation.pem
       Preparing client.rb
Starting Chef Infra Client, version 16.8.9
Patents: https://www.chef.io/patents
[2021-01-23T15:46:41+00:00] ERROR: shard_seed: Failed to get dmi property serial_number: is dmidecode installed?
Creating a new client identity for default-ubuntu-2004 using the validator key.
resolving cookbooks for run list: ["foobar::default"]
Synchronizing Cookbooks:
  - foobar (0.1.0)
Installing Cookbook Gems:
Compiling Cookbooks...
Converging 1 resources
Recipe: foobar::default
  * template[/etc/foo] action create
    - create new file /etc/foo
    - update content in file /etc/foo from none to 7d865e
    --- /etc/foo        2021-01-23 15:46:42.479378000 +0000
    +++ /etc/.chef-foo20210123-488-3npuk0       2021-01-23 15:46:42.479378000 +0000
    @@ -1 +1,2 @@
    +bar
    - change mode from '' to '0644'
    - change owner from '' to 'root'
    - change group from '' to 'root'

Running handlers:
Running handlers complete
Chef Infra Client finished, 1/1 resources updated in 01 seconds

次にテストを実行します。

% bundle exec kitchen verify
-----> Starting Test Kitchen (v2.8.0)
-----> Setting up <default-ubuntu-2004>...
       Finished setting up <default-ubuntu-2004> (0m0.00s).
-----> Verifying <default-ubuntu-2004>...
       Loaded tests from {:path=>"....projects.private.foobar.test.integration.default"}

Profile: tests from {:path=>"/.../foobar/test/integration/default"} (tests from {:path=>"....foobar.test.integration.default"})
Version: (not specified)
Target:  docker://9b7ec11edb298a29cfadabf520ac834954fe257ff221f1304d1397f60e6dcfed

  File /etc/foo
     ✔  is expected to be file
     ✔  is expected to be owned by "root"
     ✔  is expected to be grouped into "root"
     ✔  is expected to be mode 420
     ✔  content is expected to match "bar"

Test Summary: 5 successful, 0 failures, 0 skipped
       Finished verifying <default-ubuntu-2004> (0m2.97s).
-----> Test Kitchen is finished. (0m4.09s)

無事全てのテストは成功し、これでレシピの開発は終了となります。 お疲れさまでした。

レシピのソースコードはGithub の foober リポジトリへアップロードしておきました。

次回予告

次回は、

  • /etc/foo の中身をパラメータで動的に変えられるようレシピを変更
  • パラメータを指定した場合に正しくファイルに反映されている事を確認するテストの記載方法
  • CircleCI を用いた CI 環境の設定
  • TravisCI を用いた CI 環境の設定
  • Github Actions を用いた CI 環境の設定

について書きたいと思います。