vagrant-configspecを使ってプロビジョニングする

@nobu_ohtaです。 teck.kayac.com Advent Calender 2013 19日目のエントリです。

今回、advent calendarを口実にvagrant-configspecをリリースしました。 インストールは

vagrant plugin install vagrant-configspec

です。 コード自体は https://github.com/ankoromochi/vagrant-configspec で公開しています。

動機

configspecを触ってみたかった。

mizzyさんのブログで紹介されているように、immutable infrastructureという文脈で出てきた、冪等性や依存関係をあまり気にしない感じのconfiguration management tool。

同じくmizzyさんが作者のサーバの状態のテストを行うserverspecと対になっているような感じで、実際その共通する部分はspecinfraというgemとして分離されたりしています。

configspecはざっくりとしたイメージとしてはシェルスクリプトやDockerfileをより抽象化した感じ。実際にconfigspecからDockerfileを生成したり、configspec/serverspecからshell scriptを生成したりすることができたりします。このあたりは、まあmizzyさんのブログを読んでください。

というわけで、immutable infrastructureみたいな話やchef/ansibleなどのサーバのprovisioningの話の流れでDocker, configspecあたりはさわってみたいなーとぼんやり考えてるだけで時間だけが過ぎてたわけです。 で、ryuzeeさんがvagrant-serverspecを紹介しているのを見かけて、よしadvent calendarを口実にvagrant-configspecでも書くかーという感じになりました。

とりあえずconfigspecさわってみた

なにはともあれ、言い訳がましくconfigspecを触ってみる。 てきとーなVagrantfile書いて、とりあえずsystem ruby使って

$ vagrant up
$ vagrant sandbox on
$ vagrant ssh

% sudo gem install bundler --no-ri --no-rdoc
% cat > Gemfile
source 'https://rubygems.org'

gem 'configspec'
gem 'rake'
% bundle install --path ./vendor/bundler
% bundle exec configspec-init
% sudo bundle exec rake spec

とりあえずこんな感じでconfigspecのサンプルで置いてあるhttpdを入れてみる。(ちなみにいろいろ試行錯誤する際は、vagrant-saharaを使っています。)

入ったか確認してみる。

% rpm -qa httpd
httpd-2.2.15-29.el6.centos.x86_64

おお、ちゃんと入ってる。

次は、confgspec入れるところをvagrantのshell provisioningで書いて、vagrant-serverspecを使ってprovisionのtestまでをvagrant up一発でやりたい。ということで、やる。

$ vagrant plugin install vagrant-serverspec
$ cat > Vagrantfile
Vagrant.configure("2") do |config|
  config.vm.box = "centos6.4-x86-minimal"

  config.vm.provision :shell, inline: <<-EOF
    sudo gem install bundler --no-ri --no-rdoc && \
    echo "source 'https://rubygems.org'\n\ngem 'configspec'\ngem 'rake'" > Gemfile && \
    bundle install --path ./vendor/bundler && \
    echo 2 | bundle exec configspec-init && \
    sudo bundle exec rake spec
  EOF

  config.vm.provision :serverspec do |spec|
    spec.pattern = 'serverspec/*.rb'
  end
end

$ cat > serverspec/001_httpd.rb
require 'serverspec'
require 'pathname'
require 'net/ssh'

include SpecInfra::Helper::Ssh
include SpecInfra::Helper::DetectOS

describe package('httpd') do
  it { should be_installed }
end

$ vagrant up
Bringing machine 'default' up with 'virtualbox' provider...
[default] Importing base box 'centos6.4-x86-minimal'...
\# 中略
[default] Running provisioner: shell...
[default] Running: inline script
Successfully installed bundler-1.3.5
1 gem installed
Fetching gem metadata from https://rubygems.org/......
Fetching gem metadata from https://rubygems.org/..
Resolving dependencies...
Installing rake (10.1.0) 
Installing net-ssh (2.7.0) 
Installing rspec-core (2.14.7) 
Installing diff-lcs (1.2.5) 
Installing rspec-expectations (2.14.4) 
Installing rspec-mocks (2.14.4) 
Installing rspec (2.14.1) 
Installing specinfra (0.0.16) 
Installing configspec (0.0.8) 
Using bundler (1.3.5) 
Your bundle is complete!
It was installed into ./vendor/bundler
Select a backend type:

  1) SSH
  2) Exec (local)
  3) Dockerfile

Select number: 
 + spec/
 + spec/localhost/
 + spec/localhost/001_httpd_spec.rb
 + spec/spec_helper.rb
 + Rakefile
/usr/bin/ruby -S rspec spec/localhost/001_httpd_spec.rb
.

Finished in 9.56 seconds
1 example, 0 failures
[default] Running provisioner: serverspec...
.

Finished in 1.09 seconds
1 example, 0 failures

という感じで、vagrant upでサーバのprovisioningとそのtestが通しで走りました。

vagrant pluginを作る

次はようやくvagrant plugin化。

pluginの作り方は、Vagrant: Up and Runningを読みながら、vagrant-serverspecのソースを参考にしました。

開発にはRuby 1.9.3以上、Bundlerと実際には使わないけどそれが依存してるGitが必要。 Vagrantのplug-insはRubyGemsとしてpackagingされるので、

$ bundle gem vagrant-configspec

でgemの開発環境としてセットアップできます。出来上がるディレクトリ構成はこんな感じ。

$ tree .
.
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── lib
│?? └── vagrant
│??     ├── configspec
│??     │?? └── version.rb
│??     └── configspec.rb
└── vagrant-configspec.gemspec

pluginの開発の仕方

公式documentを参考にしました。

  1. systemのVagrantを使う
  2. 開発のみGemとして配布されているVagrantに依存する

上記の2つの方法があります。

systemのVagrantを使う場合

$ cat > Rakefile
require 'rubygems'
require 'bundler/setup'
Bundler::GemHelper.install_tasks

$ rake -T
rake build    # Build vagrant-configspec-0.0.1.gem into the pkg directory.
rake install  # Build and install vagrant-configspec-0.0.1.gem into system gems.
rake release  # Create tag v0.0.1 and build and push vagrant-configspec-0.0.1.gem to Rubygems

\# vagrant-configspec.gemspecにTODOが残ってるので編集してから、
$ rake build
$ vagrant plugin install pkg/vagrant-configspec-0.0.1.gem
$ vagrant plugin list                                    
vagrant-configspec (0.0.1)

このやり方だと、毎回buildしてvagrant plugin installする必要があって不便なので、開発では後者でやることが多いみたいで、vagrant-awsの開発は後者でやってるみたいです。

開発のみGemとして配布されているVagrantに依存する

まずはGemfileに依存を追加します

$ cat >> Gemfile

group :development do
  gem "vagrant", :git => "git://github.com/mitchellh/vagrant.git"
end
$ bundle install --path ./vendor/bundler
$ bundle exec vagrant --version
Vagrant 1.4.2.dev

これで、bundler経由でvagrantを使えばgemで落としたやつが使われるようになります。 Vagrantfileを編集してrequire_pluginをすることで、localの開発中のpluginをvagrantから参照できるようになります。

$ cat Vagrantfile
Vagrant.require_plugin "vagrant-configspec"

Vagrant.configure("2") do |config|
  config.vm.box = 'precise64'
  config.vm.box_url = 'http://cloud-images.ubuntu.com/precise/current/precise-server-cloudimg-vagrant-amd64-disk1.box'
end

とりあえず、pluginの雛形を用意してvagrant upしてみる。

$ cat lib/vagrant-configspec.rb 
begin
  require 'vagrant'
rescue LoadError
  raise 'The Vagrant ConfigSpec plugin must be run within Vagrant.'
end

require_relative 'vagrant-configspec/version'

module VagrantPlugins
  module ConfigSpec
    def self.source_root
      @source_root ||= Pathname.new(File.expand_path('../../', __FILE__))
    end
  end
end

require_relative 'vagrant-configspec/plugin'

$ cat lib/vagrant-configspec/version.rb
module VagrantPlugins
  module ConfigSpec
    VERSION = '0.0.1'
  end
end

$ cat lib/vagrant-configspec/plugin.rb 
module VagrantPlugins
  module ConfigSpec
    class Plugin < Vagrant.plugin('2')
      name 'configspec'
    end
  end
end

$ git diff vagrant-configspec.gemspec
-require 'vagrant/configspec/version'
+require 'vagrant-configspec/version'

Gem::Specification.new do |spec|
   spec.name          = "vagrant-configspec"
-  spec.version       = Vagrant::Configspec::VERSION
+  spec.version       = VagrantPlugins::ConfigSpec::VERSION

$ bundle exec vagrant up

実際にpluginを書く

関係のあるところだけ。Vagrant: Up and Runningが詳しいのでオススメです。 デバッグするときは

$ VAGRANT_LOG=info bundle exec vagrant provision

みたいにするとlogが出るようになって参考になります。

config追加

$ cat lib/vagrant-configspec/plugin.rb 
...
      config(:configspec, :provisioner) do
        require_relative 'config'
        Config
      end
...

みたいな感じで、Pluginに関連するconfigを追加します。

上記だと、:configspecっていう名前の:provisionerに対するconfigを追加する感じになります。実際に追加するのは以下。

$ cat lib/vagrant-configspec/config.rb
module VagrantPlugins
  module ConfigSpec
    class Config < Vagrant.plugin('2', :config)
      attr_accessor :spec_files

      def initialize
        super
        @spec_files = UNSET_VALUE
      end

      def pattern=(pat)
        @spec_files = Dir.glob(pat)
      end

      def finalize!
        @spec_files = [] if @spec_files == UNSET_VALUE
      end

      def validate(machine)
        errors = _detected_errors

        if @spec_files.nil? || @spec_files.empty?
          errors << I18n.t('vagrant.config.configspec.no_spec_files')
        end

        missing_files = @spec_files.select { |path| !File.file?(path) }
        unless missing_files.empty?
          errors << I18n.t('vagrant.config.configspec.missing_spec_files', files: missing_files.join(', '))
        end

        { 'configspec provisioner' => errors }
      end
    end
  end
end

initialize, finalizeができるのと、validationが定義できます。

validationはvagrantがprovisionなどの処理を始める前に処理されるので、動き始めてからconfigエラーみたいなことが起きにくくなってます。

あとは、UNSET_VALUEが初期化されてない値として利用されてます。

provisioner追加

以下のようにPluginに:configspecというprovisionerを定義します。

$ cat lib/vagrant-configspec/plugin.rb
...
      provisioner(:configspec) do
        require_relative 'provisioner'
        Provisioner
      end
...

中身はこんな感じで、ほぼvagrant-serverspecの実装と同じになってます。

$ cat lib/vagrant-configspec/provisioner.rb
require 'configspec'

module VagrantPlugins
  module ConfigSpec
    class Provisioner < Vagrant.plugin('2', :provisioner)
      def initialize(machine, config)
        super(machine, config)

        @spec_files = config.spec_files

        RSpec.configure do |spec|
          spec.before :all do
            ssh_host                 = machine.ssh_info[:host]
            ssh_username             = machine.ssh_info[:username]
            ssh_opts                 = Net::SSH::Config.for(machine.ssh_info[:host])
            ssh_opts[:port]          = machine.ssh_info[:port]
            ssh_opts[:forward_agent] = machine.ssh_info[:forward_agent]
            ssh_opts[:keys]          = machine.ssh_info[:private_key_path]

            spec.ssh = Net::SSH.start(ssh_host, ssh_username, ssh_opts)
          end

          spec.after :all do
            spec.ssh.close if spec.ssh && !spec.ssh.closed?
          end
        end
      end

      def provision
        RSpec::Core::Runner.run(@spec_files)
      end
    end
  end
end

initializeでテストで使われるssh configをvmのもので置換して、provisionでconfigspecを走らせるだけ。

configspecの本体はpluginの一部として~/.vagrant.d/gems/gems/にinstallされて、configspecのテストはホストマシンからvagrantで立ち上げられたマシンに対して行われます。なので、configspecのhelperとしては

include SpecInfra::Helper::Ssh

を指定するのが正しいです。

完成

というわけで

$ bundle exec vagrant provision
You appear to be running Vagrant in a Bundler environment. Because
Vagrant should be run within installers (outside of Bundler), Vagrant
will assume that you're developing plugins and will change its behavior
in certain ways to better assist plugin development.

[default] Running provisioner: configspec...
.

Finished in 1.59 seconds
1 example, 0 failures

無事出来上がり。

vagrant pluginのリリースの仕方

とりあえず、

$ rake build
$ vagrant plugin install pkg/vagrant-configspec-0.0.1.gem

して簡単なVagrantfileを書いて動いているのを確認。

https://rubygems.org/sign_upからアカウント作成して、

$ rake release
vagrant-configspec 0.0.1 built to pkg/vagrant-configspec-0.0.1.gem.
Tag v0.0.1 has already been created.
Pushed vagrant-configspec 0.0.1 to rubygems.org.

ということで、無事Rubygemsにあがりました。確認。

$ vagrant plugin install vagrant-configspec
Installing the 'vagrant-configspec' plugin. This can take a few minutes...
Installed the plugin 'vagrant-configspec (0.0.1)'!

rubygems.orgに上げると、vagrant plugin installもできるようになる感じですね。

おわりに

というわけで、無事configspec触れてよかった!

明日はPerl使いとして洗脳された @mackee_w です。