ブログ

ryuzeeによるブログ記事。不定期更新
アジャイル開発に取り組むチーム向けのコーチングや、技術顧問、認定スクラムマスター研修などの各種トレーニングを提供しています。ぜひお気軽にご相談ください(初回相談無料)

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

みなさんこんにちは。@ryuzeeです。

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を利用したのかを把握できるようにしておいた方が良いためです。

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のイメージを作成しています。
{
  "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がインストールされていますので、これを使うように設定します。

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

(補足)なお、環境によっては(例えばAmazon Linuxなど)では以下のようにsudoを使った形で設定する必要がある場合もあります。

#!/bin/bash
cd /tmp/tests
sudo sh -c "PATH=/opt/chef/embedded/bin:$PATH; bundle install --path=vendor"
sudo sh -c "PATH=/opt/chef/embedded/bin:$PATH; bundle exec rake spec"

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

Gemfile

source "https://rubygems.org"

gem "rake"
gem "serverspec"

Rakefile

require 'rake'
require 'rspec/core/rake_task'

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

spec/spec_helper.rb

require 'serverspec'

set :backend, :exec

spec/localhost/sample_spec.rb

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によるテストが実行されることが分かります!

出力結果

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

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

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

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

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

それでは。