bacula cookbookのラッパをtest-kitchenでテストする

# 動機

昨年暮れより、自宅サーバのバックアップはbaculaに任せています。非常に便利なのですが、インストールにかなり苦労しました。今年に入りchef-serverを使う機会があり、コミュニティ版cookbookのbaculaを使うとbaculaを簡単にインストール出来ることを知りました。chef-serverで適切にバックアップ対象ホストを管理すれば、baculaサーバ構築時にchef-serverは登録されている全てのホストの中からバックアップ対象ホストをサーチし、それらの情報をbaculaサーバに設定してくれます。つまり、**新規ホストをchef-serverに登録すると自動的にbaculaサーバにバックアップ対象として追加設定できる**ようになります。

このような便利なコミュニティ版cookbookのbaculaですが、設定ファイル（bacula-dir.conf）のテンプレート内容を変えたいときは、ラッパをカスタムcookbookとしてコーディングしなければなりません。そして、**動作テストが必要**となります。

試行錯誤の結果、カスタムcookbookのテストができるようになったのでテスト方法をメインに紹介したいと思います。

# 前提

Cookbook開発環境に使用するホストマシンはUbuntu12.04上にChef Development Kit（以下、chef-dkと呼ぶ） バージョン0.3.5をインストールしました。コミュニティ版cookbookのbaculaのバージョンは1.3.0, baculaのバージョンは5.2.6です。

なお、chefの実行やcookbookのテストに用いる各種テストツール、フレームワークは個別にインストールされるものでしたが、今回用いるchef-dkはrubyランタイムも含み、chefに関連して必要になるであろうプログラム群を１つのパッケージにしたものです。簡単にインストールできて非常に便利です。使い方などの詳しい情報は[Chef Development Kit](https://downloads.chef.io/chef-dk/)を参照ください。

# カスタムCookbookの開発環境

どこか適当なディレクトリを作成して以下のコマンドを実行します。

`$ berks cookbook <cookbook名>`

上記コマンドを実行すると、`<cookbook名>`というサブディレクトリを作成した後、その下にcookbookに必要なスケルトンを作成します。`<cookbook名>`をmy_baculaという名前にして説明を進めます。

構文チェックを行うfoodcriticと単体テストを行うchefspecは説明しません。ここでは**結合テストに関してのみ**説明します。結合テストにはtest-kitchenを使用します。

# 結合テスト環境

カスタムCookbookの開発環境を berks コマンドで作成した際に、**test-kitchenのテスト環境は既に作成**されています。結合テストは実動作で行いたいので、実動作に必要なVMを立ち上げてテストします。実動作に必要なVMは

+ baculaサーバ
+ ストレージサーバ
+ baculaクライアント

の３つとなります。さらにこれらのVMは**chef-server**で管理させます（そうしないと、baculaサーバにストレージサーバ、baculaクライアントの情報を自動に設定することができない）。

test-kitchenはVMの立ち上げから、その動作状態をチェックするための各種テストツール（例えばserverspecなど）を実行し、テスト結果を出すまでの一連の作業を行なってくれますが、一度に立ち上げるVMは１台という制約があるようです。**これだと今回行うテストに合いません**。

# test-kitchenではVMの立ち上げを行わない

要するに**VMの立ち上げはVagrantなどの他のツールに事前に行わせておいて、test-kitchenはテストだけ行わせることができればOK**です。このような使い方が本流なのかどうかは別にして、やってみてできればOKということでトライします。もちろん、test-kichen自体を使わないという考えもありますが、test-kitchenでは.kitchen.yamlという設定ファイルを記述しておけば、それに従って自動にテストしてくれるという仕組みがあり、それを使いたいがためtest-kitchenにテストを任せる方針で進めます。

今回の方法を検討するにあたり、VMをあらかじめ立ち上げたあとにkitchen-sshというtest-kitchenのプラグインを用いてserverspecでテストをしている方がいらっしゃったのでそれを参考にさせていただきました（参考：[test-kitchenでserverspecを使ってみる](http://think-t.hatenablog.com/entry/2014/07/23/025552)）

# VMの立ち上げまで

VMの立ち上げとchef-serverを用いたプロビジョニングをVagrantで行うことにします。そのため、Vagrantを実行する前にchef-serverの立ち上げとchef-serverに各VMのプロビジョニングに必要となるCookbookやrole情報を予めアップロードしておく必要があります。

テストのためだけにchef-serverを立ち上げるのは面倒なので、認証が必要なく、インメモリで動作するchef-zeroを使うことにします。chef-zeroにアクセスするための設定ファイルは.chef/以下に作成します。knife.rbとstickywicket.pemというdummy用のキーがあればOKです。https://github.com/opscode/chef-zero の playground/.chef/ を参考にさせていただきました。

プロビジョニングに必要となるcookbookはBerkshelfで管理すると非常に楽です。Berksfileで必要なcookbookを記述し、berksコマンドを用いると、必要なcookbookをローカルマシンにダウンロードし、chef-zeroにアップロードしてくれます。以下は今回用いたBerksfileです。

```lang:Berksfile
source "https://supermarket.getchef.com"

metadata

cookbook "ntp"
cookbook "locale-gen"
cookbook "locales"
cookbook "system"
```

次に各々のVM用にプロビジョニング設定条件をroleとして作成しておきます。それらのファイルはroles/ディレクトリ以下に入れておきます。以下が各々のroleファイルとなります。

（ストレージサーバ用）

```bacula_sd.json
{
  "name": "bacula_sd",
  "override_attributes": {
    "system": {
      "packages": {
        "install": [
          "emacs"
        ]
      }
    },
    "bacula": {
      "sd": {
        "address": "192.168.33.11",
        "backup_dir": "/backup2"
      }
    }
  },
  "run_list": [
    "recipe[apt]",
    "role[locale_jst]",
    "recipe[ntp]",
    "recipe[system::install_packages]",
    "recipe[bacula::storage]"
  ]
}
```
（baculaクライアント用）

```bacula_fd.json
{
  "name": "bacula_fd",
  "override_attributes": {
    "system": {
      "packages": {
        "install": [
          "emacs"
        ]
      }
    },
    "bacula": {
      "fd": {
        "address": "192.168.33.12",
        "files": {
          "includes": [
            "/home",
            "/var",
            "/etc"
          ],
          "excludes": [
            "/dev"
          ]
        }
      }
    }
  },
  "run_list": [
    "recipe[apt]",
    "role[locale_jst]",
    "recipe[ntp]",
    "recipe[system::install_packages]",
    "recipe[bacula::client]"
  ]
}
```
（baculaサーバ用）

```bacula_dir.json
{
  "name": "bacula_dir",
  "override_attributes": {
    "system": {
      "packages": {
        "install": [
          "emacs"
        ]
      }
    },
    "mysql": {
      "server_root_password": "hogefoge"
    },
    "bacula": {
      "dir": {
        "address": "192.168.33.10"
      }
    }
  },
  "run_list": [
    "recipe[apt]",
    "role[locale_jst]",
    "recipe[ntp]",
    "recipe[system::install_packages]",
    "recipe[my_bacula::server]"
  ]
}
```
（ロケールの設定用）

```locale_jst.json
{
  "name": "locale_jst",
  "override_attributes": {
    "localegen": {
      "lang": [
        "ja_JP.UTF-8 UTF-8"
      ]
    },
    "locales": {
      "default": "ja_JP.UTF-8",
      "available": [
        "ja_JP.UTF-8 UTF-8"
      ]
    },
    "system": {
      "timezone": "Asia/Tokyo"
    }
  },
  "run_list": [
    "role[locale]"
  ]
}
```

```locale.json
{
  "name": "locale",
  "run_list": [
    "recipe[locale-gen::default]",
    "recipe[locales::default]",
    "recipe[system::default]"
  ]
}
```

上記roleにはbaculaとは関係のないemacsをインストールするための設定が混入しています。これはVMにリモートログインし、自分として使いやすいエディタでデバッグ作業を行いたいために設定しているものです。内容に関しては今回の主題ではないので割愛させていただきますが、baculaを使っている方であれば大体予想できると思います。

上記定義が終わったら、chef-zeroへknifeコマンドでアップロードします。chef-zero, berks, knifeコマンドはchef-dkにあらかじめインストールされています。これまで説明した処理は以下のコマンド達が快く引き受けてくれます。

```
$ chef-zero --daemon -H 0.0.0.0
$ berks
$ berks upload
$ knife upload roles
```

これで、Vagrantを用いてVMを立ち上げると共に、chef-zeroに登録したcookbookおよびroleでまとめたrun_listを用いてプロビジョニングできる用意が整いました。おっと、Vagrantfileを忘れていました。以下が今回の記述となります。

```lang:Vagrantfile
VAGRANTFILE_API_VERSION = '2'

Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
  config.vm.box = 'trusty64'

  config.vm.define 'sd' do |sd|
    sd.vm.hostname = 'sd'
    sd.vm.network 'private_network', ip: '192.168.33.11'

    sd.vm.provider 'virtualbox' do |vb|
      vb.customize ['modifyvm', :id, '--memory', '1024']
    end

    sd.vm.provision 'chef_client' do |chef|
      chef.chef_server_url = 'http://develop:8889'
      chef.validation_key_path = '.chef/stickywicket.pem'
      chef.add_role 'bacula_sd'
    end
  end

  config.vm.define 'fd' do |fd|
    fd.vm.hostname = 'fd'
    fd.vm.network 'private_network', ip: '192.168.33.12'

    fd.vm.provider 'virtualbox' do |vb|
      vb.customize ['modifyvm', :id, '--memory', '1024']
    end

    fd.vm.provision 'chef_client' do |chef|
      chef.chef_server_url = 'http://develop:8889'
      chef.validation_key_path = '.chef/stickywicket.pem'
      chef.add_role 'bacula_fd'
    end
  end

  config.vm.define 'dir' do |dir|
    dir.vm.hostname = 'dir'
    dir.vm.network 'private_network', ip: '192.168.33.10'

    dir.vm.provider 'virtualbox' do |vb|
      vb.customize ['modifyvm', :id, '--memory', '1024']
    end

    dir.vm.provision 'chef_client' do |chef|
      chef.chef_server_url = 'http://develop:8889'
      chef.validation_key_path = '.chef/stickywicket.pem'
      chef.add_role 'bacula_dir'
    end
  end
end
```

ここで重要なのはVMを立ち上げる順番です。baculaサーバとなるdirはストレージサーバ(sd)およびbaculaクライアント(fd)の設定情報をchef-zeroを介して取り込む必要があるので、sd, fdが確実に立ち上がった後、最後に立ち上げる必要があります。

設定ファイルができたら以下のコマンドでVMが順次、立ち上がります。

`$ vagrant up`

# ようやくテスト

test-kitchenの管理の元、serverspecを用いて統合テストを行います。serverspecで対象VMのテストをするためにsshでリモートログインできなければなりません。今回はVMをVagrantでtest-kitchenとは別に立ち上げていますのでsshでアクセスできるよう、kitchen-sshというtest-kitchen用プラグインを用います。このプラグインはchef-dKにはインストールされていません。gem installで導入しますが、chef-dkとして導入する必要があるのでchef gem installというコマンドを用います。以下のコマンドを実行することにより~/.chefdk/以下にインストールされます。

`$ chef gem install kitchen-ssh`

これでtest-kitchenの管理の元、serverspecを用いて統合テストを行えるようになりました。最後に.kitchen.ymlを用意しましょう。ここでは以下のような設定ファイルを用意しました。

```lang:.kitchen.yml
---
driver:
  name: ssh
  username: vagrant
  password: vagrant
  sudo: true

platforms:
  - name: ubuntu

suites:
  - name: sd
    driver:
      hostname: 192.168.33.11
  - name: fd
    driver:
      hostname: 192.168.33.12
  - name: dir
    driver:
      hostname: 192.168.33.10
```

たったこれだけです。kitchen-sshを用いて各VMにアクセスするためのusernameおよびpasswordと、テストsuite毎にsshでアクセスする先のhostnameをIPで設定（DNS名前解決できるのであればホスト名でも良いのかも？）するだけの簡単な設定です。

# テストコードを書く前に動作テスト

テストコードを書く前にtest-kitchenを実行して確かに動作することを確認しましょう。

```
$ kitchen list
Instance    Driver  Provisioner  Last Action
sd-ubuntu   Ssh     ChefSolo     <Not Created>
fd-ubuntu   Ssh     ChefSolo     <Not Created>
dir-ubuntu  Ssh     ChefSolo     <Not Created>
```

kitchen createでdir-ubuntuインスタンスを生成してみると確かにsshアクセスしているようです。

```
$ kitchen create dir-ubuntu
-----> Starting Kitchen (v1.2.1)
-----> Creating <dir-ubuntu>...
       Kitchen-ssh does not start your server '192.168.33.10' but will look for an ssh connection with user 'vagrant'
       Kitchen-ssh found ssh ready on host '192.168.33.10' with user 'vagrant'

       Finished creating <dir-ubuntu> (0m0.01s).
-----> Kitchen is finished. (0m0.20s)
```

# テストコードを書いて実行する

baculaサーバのテストはdirホストにアクセスしてテストします。テストファイルを置く場所は`test/integration/<suite名>/<テストツール名>/`です。dirホストのテスト記述はsuite名:dirで書かれています。テストツールはserverspecです。よって`test/integration/dir/serverspec/`です。
baculaサーバdirがbaculaクライアントfdの対象ファイルをバックアップするように設定されていることを確かめるためには以下のテスト項目が必要です。

+ baculaクライアントfdが定義されていること
+ バックアップ対象ディレクトリが設定されていること

です。bconsoleというコマンドを用いると上記情報を取得することができ、帰ってきた情報の中身をチェックすることでテストを行うことにします。１番目のテスト項目に関してテストコードを書いて見ました。

```lang:client_spec.rb
require 'serverspec'

set :backend, :exec

### client check

show_client =<<"EOF"
sudo bconsole << END_OF_DATA
show client=fd.localdomain
quit
END_OF_DATA
EOF

describe command(show_client) do
  its(:stdout) { should_not match /not found/ }
end
```

bconsoleのshowサブコマンドにclient=xxxを指定して実行すると、xxxで指定したクライアントが設定されていない場合、帰ってきた結果にnot foundという文字列が含まれます。この文字列が入っていない場合、すなわち(should_not match /not fount/)でテストがパスするという記述になります。本クライアントが存在しているということは、chef-zeroからbaculaクライアントfdに関する設定情報を適切に採取できているということになります。２番目のテスト項目に関しても同様に以下のコードで記述しテストすることができます。

```lang:fileset_spec.rb
require 'serverspec'

set :backend, :exec

### Fileset_fd.localdomain check

fileset_name = "Fileset_fd.localdomain"

show_fileset =<<"EOF"
sudo bconsole << END_OF_DATA
show fileset=#{fileset_name}
quit
END_OF_DATA
EOF

describe command(show_fileset) do
  %w(/home /var /etc).each do |inc_file|
    its(:stdout) { should match /I +#{inc_file}/ }
  end

  %w(/dev).each do |exc_file|
    its(:stdout) { should match /E +#{exc_file}/ }
  end
end
```

上記コードはバックアップ対象ディレクトリとして/home, /var, /etcが、対象外として/devが設定されているかどうかをテストします。

以下のコマンドを実行すると、dirホストに対してseverspecを実行するためのプログラムをインストールし、serverspecでテストまで実施してくれます。

`$ kitchen verify sd-ubuntu`

以上で実際にテストを実行できることを確認できました。

# 最後に

とりあえず、テスト環境が出来上がったので、足りないテストコードを追加していけばOKです。統合テストは実際のVMをプロビジョニングしてテストするのでかなりの時間がかかります。Jenkinsで夜間実行を自動で行えるようにすれば楽ができそうです。ここ２，３ヶ月アプリケーション開発を止めて開発環境の見直し（主に自動化を主眼）を行って来ましたが、ようやくトンネルの出口にたどり着きそうです。来年からは本腰いれてアプリケーション開発に勤しむことができます。今回構築したテスト環境を図で表して見ました。これで説明を終わりにしたいと思います。

![テスト環境図](https://qiita-image-store.s3.amazonaws.com/0/26969/b9ef5538-b884-ab77-2ff2-9f56a3a531dd.png "テスト環境図")