Vagrantfileで使うプラグインを定義する

  1. プロジェクトで利用するプラグインを明示しておきたい
  2. プラグインのインストールを自動化したい
  3. Vagrantで任意のGemをたくさん利用したい

などの目的のために、vagrant-multiplug というVagrantのプラグインをつくりました。このプラグインをインストールした状態で、利用するプラグインの名前が定義されたVagrantflieを読み込むと、vagrant upvagrant provision などのタイミングで自動的にプラグインをインストールしてくれます。

インストール

手元の環境に vagrant-multiplug をインストールします。

$ vagrant plugin install vagrant-multiplug

使い方

Vagrantfileの中で config.plugin.add_dependency(name, version = nil) という風に利用するプラグインを定義できます。今回は、例としてプロビジョニングツールの1つのServerkit を使うためのプラグイン vagrant-serverkit と、それからServerkitでrbenvを扱うためのプラグインである serverkit-rbenv を利用することにします。

Vagrant.configure("2") do |config|
  config.vm.box = "ubuntu/trusty64"

  config.plugin.add_dependency "vagrant-serverkit"
  config.plugin.add_dependency "serverkit-rbenv", "0.0.2"

  config.vm.provision :serverkit do |config|
    config.recipe_path = "recipe.yml"
  end
end

この状態で vagrant up を利用すると、自動的にvagrant-serverkitとserverkit-rbenv (v0.0.2) がインストールされ、その後Serverkitを利用してProvisionが実行されます。使い方は以上です。

$ vagrant up

Vagrantプラグインの実体はGem

ここからは内部実装の話になります。 vagrant plugin install で入れているVagrantのプラグインは、実体としては単なるGemに過ぎません。上記の例では、vagrant-serverkitの他に、serverkit-rbenvもプラグインとして指定しました。しかし、serverkit-rbenvは特にVagrantのプラグインという訳ではなく、Serverkitの機能を拡張するだけのただのGemです。ServerkitではGemを利用したプラグイン機構を持っていますが、Vagrantの環境下では「Gemfileに書いてbundler execで起動」という方法が使えないため、読み込んでほしいGemはVagrantのプラグインとして認識させる必要がありました。今回vagrant-multiplugのようなプラグインをつくったのは、Gemを利用して機能を拡張していく仕組みをもったライブラリのために、少しでもプラグインを入れるためのコストを軽減させたいという意図もありました。

なぜVagrant環境下ではGemfileが使えないのか

先述したように、Vagrantの環境下では「Gemfileに書いてbundler execで起動」という方法が使えません。これには、Vagrantの内部実装で別の用途にBundlerを利用しているので、一緒に使われるとまずいという理由があります。Vagrantは、本体それ自体に動作するRubyをまるごと同梱しており、Vagrantで利用するGemは全てその内部で管理されています。そして、VagrantのプラグインもまたGemであるため、同じくVagrant同梱のRubyによって管理されています。

例えば vagrant plugin install vagrant-serverkit というコマンドでプラグインをインストールすると、~/.vagrant.d/gems/gems/vagrant-serverkit-0.0.4 にGemがインストールされます。また、~/.vagrant.d/plugins.json にvagrant-serverkitをインストールしているという情報が保存されます。この状態で vagrant up などのコマンドを利用すると、Vagrantはまず ~/.vagrant.d/plugins.json などの情報をもとに一時的なGemfileを生成します。Vagrantは、このGemfileのパスを ENV["BUNDLE_GEMFILE"] に、またGemをインストールしているパスを ENV["GEM_PATH"] に入れた状態で Bundler.setup を実行することで、各種プラグインの実体へのパスを $LOAD_PATH に入れ、require "vagrant-serverkit" などで読み込めるようにしています。

vagrant-multiplugはどう対処しているか

Vagrantのプラグインでは、「ある処理が行われる前に任意の処理を行う」という類のコールバックを追加できます。今回は、vagrant up などのVagrantの全てのアクションが起動されたときに、最初に必ず config.plugin.add_dependency で定義されたGemがインストールされているか確認し、されていなければインストールを行う、という処理を追加しています。ここでもしインストールが行われた場合は、再度Gemfileを生成して Bundler.setup を実行し、追加されたプラグインをすぐに読み込める状態にしておく必要があります。

そこでvagrant-multiplugでは、もしインストールが行われた場合は Kernel.#exec で現在のプロセスを最初から再実行するという方法を取りました。$0 で現在のプログラムへのパスを参照し、同じコマンドライン引数を渡しながら別プロセスへ処理を引き渡し、現在のプロセスはそこで終了させます。このとき、何もしなければ次のプロセスに環境変数が引き継がれるという点に注意しなければなりません。Vagrantでは、一度Gemfileの生成が終わると VAGRANT_INTERNAL_BUNDLERIZED に値が書き込まれますが、この値が既に設定されているときはもうこの処理を行わないという条件分岐が存在するため、この環境変数は空にしておく必要があります。また、VAGRANT_FORCE_BUNDLER が1でなければ再度実行してくれないという条件分岐もあるため、これは1にしておきます。これで、Gemfileを再生成する処理が exec で起動する次のプロセスでも実行されます。

Kernel.exec(
  {
    "VAGRANT_INTERNAL_BUNDLERIZED" => nil,
    "VAGRANT_FORCE_BUNDLER" => "1",
  },
  "#{$0} #{ARGV * ' '}",
)

参考にしたもの

2年前のプラグインでもうメンテナンスされていないみたいですが、tknerr/vagrant-plugin-bundler というものがあったので参考にしました。Vagrantfileで config.plugin.depend "vagrant-omnibus", "1.0.2" と書いておくと、vagrant-omnibus v1.0.2 がインストールされていなかった場合に、エラーを出してそこで処理を止めるというやつです。これは主に、Vagrantfileのインターフェースのデザインの参考にしました。vagrant-multiplug ではgemspecのメソッド名を少し意識して、.add_dependency というメソッド名にしました。

これももはやメンテナンスされていませんが、fgrehm/bindler というプラグインも参考にしました。これはGemfileのようなものを別途用意してプラグインを管理しようというもので、Vagrantに新しいコマンドを追加して bundle install のようにインストールできるようにしています。

他に、vagrant-omnibus も参考になりました。指定したバージョンのChefがゲストにインストールされていることを保証するもので、自動インストール機能なども付いています。プラグインで処理をフックする仕組みなども参考になりました。

また、プロビジョニングツールの1つである Itamae も、Serverkit同様にプラグインで機能を拡張する形式のため、同じ問題を抱えていました。vagrant-itamae の作者からServerkitではどう解決するつもりか尋ねられたときに初めてこの問題を認識したので、その辺りの会話がなければこのプラグインは生まれなかったと思います。

おわり

vagrant-multiplug が手動で入れる最後のプラグインになると良いですね。