Packer + Serverspecでテスト済み仮想マシンイメージを自動で生成する


Packerを使うと、VagrantやVMware、Amazon EC2用のAMIなどなどのさまざまな仮想マシンのテンプレートを簡単に作成することが出来るのはご存知の通りです。

一方で作成した仮想マシンのテンプレートが必要な要件を満たしているかどうかは、仮想マシンのテンプレートを作るたびに検査しなければなりません。

ここでは、PackerとServerspecを組み合わせて、仮想マシンのテンプレートを作成する際に、テストも併せておこなう方法について解説します。

## 前提

今回仮想マシンのテンプレートを作成するにあたっては以下の方式で行ないます。

* Packer 0.7.2
* Packerでの仮想マシンの設定ではChef-SoloのProvisionerを利用
* CookbookはBerkshelfを使って管理する
* 作るテンプレートはDocker用のもの(但し他のものでもほとんど変わりません)

また、方式としては、仮想マシンが起動されて、各種セットアップや設定が終わったあとに、その仮想マシン内でServerspecを動作させます。つまり、Packerで仮想マシン内にテスト用のSpecファイルを転送したり、仮想マシン内でServerspecが動作するように設定する必要がある、ということになります。

## 処理の流れ

ここまでの方式を処理の流れにすると以下のようになります。

* 予めBerksfileを用意しておき、導入したいCookbookを準備する
* テンプレート生成の土台となる仮想マシンを起動する
* Chef-Soloがインストールされていない場合はインストール
* run_listに記載した内容に従ってクックブックを適用する
* 別で用意しておいたテスト用のSpecファイルを転送する
* 仮想マシン内でServerspecが動作する環境を用意する
* 仮想マシン内でServerspecを実行する
* これらが正常に終わったら仮想マシンからテンプレートを生成し、仮想マシンを破棄する

## Berksfileの準備

BerksfileとはChefのCookbookを管理するBerkshelf用の設定ファイルです。
以下のような内容になります。

ここでは、導入するCookbookのバージョンやタグを細かく設定しています。これは仮想マシンのテンプレートを後から追跡するときに、どのバージョンのCookbookを利用したのかを把握できるようにしておいた方が良いためです。

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

cookbook 'yum', '3.5.1'
cookbook 'apache2-simple', :git => 'git://github.com/ryuzee-cookbooks/apache2-simple.git', ref:"c47d08933d16020f002fdf802ee32a8681c4db66"
cookbook 'memcached', :git => 'git://github.com/ryuzee-cookbooks/memcached.git', ref:"6f32a279cfff3c189a7d16ac3472b77299ce93da"
cookbook 'timezone', :git => 'git://github.com/ryuzee-cookbooks/timezone', ref:"bb758452e4efb83d5608eb51597be0ecc84ec185"
cookbook 'ca-certificates', :git => 'git://github.com/ryuzee-cookbooks/ca-certificates', tag: "v0.1.0"
```

ここまで出来たら、

```
berks vendor cookbooks
```

として、ローカル環境にCookbookをダウンロードしておきます。

## 仮想マシンの起動とプロビジョニング

通常テストなしでテンプレートを作成する場合はこれで準備は終わりです。
ここまででビルドするためには、以下のような、jsonファイルを用意します。

内容自体は特に特筆すべき点はありません。

* 冒頭で作成する仮想マシンの種類を設定します。ここではDockerを使います。元となるイメージはここでは自作のものを使っていますが、適宜変えてください。
* provisionersの箇所では仮想マシンの設定をどのようにおこなうかを設定します。ここではChef Soloを使っています。また利用するCookbookは先ほどBerkshelfを使って手元に用意したものを使い、run_listの箇所で適用するCookbookを、`json`の箇所で適宜Attributeを指定しています。
* post-processorsの箇所では、プロビジョニングなどが終わったあとの処理を指定しており、ここでは最後にDockerのイメージを作成しています。

```json
{
  "builders": [{
    "type": "docker",
    "image": "ryuzee/centos_chef:6.4",
    "run_command": ["-d", "--hostname=packer-sample", "-i", "-t", "{{.Image}}", "/bin/sh"],
    "export_path": "image.tar"
  }],

  "provisioners":[
  {
    "type": "chef-solo",
    "cookbook_paths": ["./cookbooks/"],
    "run_list": ["ca-certificates", "timezone", "apache2-simple", "memcached"],
    "json": {"memcached":{"maxcon":"512","cachesize":"512"}},
    "prevent_sudo": true,
    "skip_install": false 
  }
  ],
  "post-processors": [{
    "type": "docker-import",
    "repository": "ryuzee/packer-sample",
    "tag": "0.1"
  }]
}

```

ここまで出来たら、

```
packer build docker.json
```

のようにして仮想マシンを作成します。

## Serverspecによるテストを組込み

ここからが肝です。まずChef Soloのプロビジョニングが終わったら、Serverspecが実行できるように準備します。
ここでは、Shell Provisionerを使います。

以下のようなスクリプトを用意し、`scripts/serverspec.sh`に保存してください。

当たり前ですが、Serverspecの動作にはRubyが必要です。Rubyを仮想マシンにインストールしても良いのですが、既にChef Soloを動かすために、`/opt/chef/embedded/bin`以下にRubyがインストールされていますので、これを使うように設定します。

```bash
#!/bin/bash
export PATH=/opt/chef/embedded/bin:$PATH
cd /tmp/tests
bundle install --path=vendor
bundle exec rake spec
```

また実際のテストやRakefileを用意しないといけませんので、`tests`ディレクトリを作成し、`Gemfile`、`Rakefile`、`.rspec`、そして実際のテストを用意します。

Gemfile

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

gem "rake"
gem "serverspec"
```

Rakefile

```ruby
require 'rake'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec) do |t|
  t.pattern = 'spec/*/*_spec.rb'
end
```

spec/spec_helper.rb

```ruby
require 'serverspec'

set :backend, :exec
```

spec/localhost/sample_spec.rb

```ruby
require 'spec_helper'

describe package('httpd'), :if => os[:family] == 'redhat' do
  it { should be_installed }
end

describe service('httpd'), :if => os[:family] == 'redhat' do
  it { should be_enabled }
  it { should be_running }
end

describe port(80) do
  it { should be_listening }
end
```

.rspec

```
--format documentation
```

ここまで出来たら、先ほど用意したpacker用のjsonを以下のように変更しましょう。
追加したのは、テスト関連のファイルを仮想マシン側の`/tmp/tests`に転送する処理と、用意した`scripts/serverspec.sh`を動作させる箇所です。

```
{
  "builders": [{
    "type": "docker",
    "image": "ryuzee/centos_chef:6.4",
    "run_command": ["-d", "--hostname=packer-sample", "-i", "-t", "{{.Image}}", "/bin/sh"],
    "export_path": "image.tar"
  }],

  "provisioners":[
  {
    "type": "chef-solo",
    "cookbook_paths": ["./cookbooks/"],
    "run_list": ["ca-certificates", "timezone", "apache2-simple", "memcached"],
    "json": {"memcached":{"maxcon":"512","cachesize":"512"}},
    "prevent_sudo": true,
    "skip_install": false
  },
  {
    "type": "file",
    "source": "tests",
    "destination": "/tmp"
  },
  {
    "type": "shell",
    "script": "scripts/serverspec.sh"
  }
  ],
  "post-processors": [{
    "type": "docker-import",
    "repository": "ryuzee/packer-sample",
    "tag": "0.1"
  }]
}
```

ここまでの作業のディレクトリ構成は以下のようになっているはずです。

```
.
├── Berksfile
├── Gemfile
├── docker.json
├── scripts/
│  └── serverspec.sh
└── tests/
    ├── .rspec 
    ├── Gemfile
    ├── Rakefile
    └── spec/
        ├── localhost/
        │  └── sample_spec.rb
        └── spec_helper.rb
```

ここまで出来たら、仮想マシンをビルドしましょう。いつも通り以下のような形でOKです。もしデバッグログが見たい場合は、`PACKER_LOG=1`を設定してください。

```
packer build docker.json
```

ビルドすると、以下のように、Serverspecによるテストが実行されることが分かります！

![10.png](https://qiita-image-store.s3.amazonaws.com/0/43246/652c01fe-90c5-d6c6-e7b2-e6312f8d7bde.png "10.png")


## (おまけ) Cookbookに含まれているテストをまとめて実行する方法

この手順を応用すると、利用したCookbookに含まれているテストを全部一気に実行することが出来ます。

やり方は簡単です。Chef Soloプロビジョナーでは、`/tmp/packer-chef-solo/cookbooks-0`などにCookbookを転送します。従ってこのディレクトリ全体の中の`*_spec.rb`をテスト実行の対象にすれば良いわけです。
(なお、このパスは元のjsonの設定によって変わります)

```ruby
require 'rake'
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new(:spec) do |t|
  t.pattern = '/tmp/packer-chef-solo/cookbooks-0/**/*_spec.rb'
end
```

これで仮想マシンのテンプレートを作成する際に全部のテストを改めてまとめて実行することもできます。