Vagrant、Chef、rbenv を使用した Ruby 開発 VM のセットアップ

コラボレーターが私の Web パブリッシングツールチェーンを使用できるように、Vagrant VM をセットアップした私の経験からのメモです。VM のプロビジョニングには Chef を使用し、正しいバージョンの Ruby のインストールと制御には rbenv を使用しました。

2014年9月4日



martinfowler.com を構築するための独自のツールチェーンがあります。類似のツールがほとんど存在しなかった 2000 年頃に開始しました。静的ウェブサイトは当時ファッショナブルではありませんでしたが、apache のみを必要とするサーバーに rsync でウェブサイトをデプロイするのが好きでした。時間が経つにつれて、ツールチェーンはより有能で複雑になりましたが、その開発方法が好きで、作業したり、新しいアイデアを探求したりするのに快適な場所です。

近年、私のサイトでツールチェーンを使用して記事を書く同僚や友人が増えました。彼らと協力するために、私のコアウェブサイトリポジトリの簡略化されたコピーを設定し、git を使用して共同作業を行っています。私のコラボレーターはほとんどがプログラマーなので、このワークフローは非常に効果的です。

これをすべて実行するには、いくつかのソフトウェアをインストールする必要があります。ツールチェーンで使用するソフトウェアはすべてオープンソースですが、最近、いくつかのインストールに関する問題が発生しています。特に、多くの基本的な Ruby インストールが古いことがわかり、新しいバージョンの Ruby をインストールする必要があります。ツールチェーンは XML を処理するため、Nokogiri を使用します。これは優れたツールですが、インストールが面倒な場合があります。ここ数か月で、何人かのコラボレーターがインストールに何時間も無駄にしました。

1年前(または2年前)、Erik にツールチェーンがすべてインストールされてすぐに使用できる VM インスタンスをセットアップする必要があると伝えられました。そうすれば、コラボレーターは VM を起動して作業を開始できるだけです。プロジェクトでこのような仮想開発環境をセットアップするために、Vagrant のようなツールをますます使用しています。これらの最新のコラボレーターのトラブルにより、ついにそれを行うことにしました。

全体的に、思ったよりも難しく、役に立つドキュメントがあまり見つかりませんでした。そこで、同様のことをしようとしている人のために、私の経験に関するメモをここに書きました。これらのメモは、権威あるドキュメントとしてではなく、私がうまくいったことを記録しただけであることを覚えておいてください。私が出会わなかったより良い方法があるかもしれませんし、これらのツールにはあまり経験がありません(実際にそうなることをあまり望んでいません)。これは非常に時間固有のものでもあり、これらのツールの後のバージョンは異なる動作をする可能性があるため、記事の日付よりもかなり後になって私の足跡をたどろうとする場合は注意してください。

Vagrant を使用したシンプルな VM のセットアップ

最初に行うことは、シンプルな VM を起動して実行することです。同僚からの言葉では、Vagrant がこれを解決するための最良の方法でした。ゲストには Ubuntu 14.04 を選択しました。これは、ゲストシステムとして一般的な選択肢のようだったためです。都合の良いことに、これを行うために作業していたボックスも 14.04 を実行していました。ただし、Ubuntu 14.04 にパッケージされている Vagrant のバージョンは、14.04 ゲストを実行するように設定されていないため、Vagrant (1.6.3) の最新コピーを手動でダウンロードしてインストールする必要がありました。deb ファイルとしてパッケージされているため、非常に簡単でしたが、以前のバージョンで動作させようとして少し迷った後、そうする必要があることに気づきました。

ベア Vagrant ボックスを実行するには、Vagrantfile という制御ファイルが必要です。簡単な例として、この制御ファイルには次の内容だけを含めることができます

VAGRANTFILE_API_VERSION = "2"

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

これは、64 ビットの ubuntu trusty (14.04) に基づいて VM を作成するように Vagrant に指示します。この Vagrantfile が配置されたら、vagrant up で VM を作成および起動できます。マシンが作成されて起動したら、vagrant ssh でログインできます。Vagrant が vagrant というユーザーを作成し、ssh キーを使用してログインすることがわかります。[1] vagrant ユーザーはパスワードなしで sudo を実行でき、Vagrant がマシンの管理を制御するために使用します。

vagrant halt でマシンを停止し、vagrant destroy でマシンを完全に破棄できます。vagrant up コマンドは、既存のマシンを起動するか、まだ作成されていない場合はマシンを作成して起動します。Vagrant はこのためにデフォルトのマシンを使用しますが、複数のマシンを処理する方法がありますが、私はそれらを調査していません。

Chef を使用したプロビジョニング

Vagrant はベアマシンを提供してくれますが、このようなマシンには、便利なことができるようにソフトウェアをプロビジョニングする必要があります。この操作の要点は、できる限り自動化して、コラボレーターがいくつかのコマンドを起動するだけで、面倒なインストール手順なしに VM を利用できるようにすることです。

これを行う 1 つの方法は、VM でインストールスクリプトを実行することですが、一般的には、PuppetChef、または Ansible など、マシンのプロビジョニング用に設計されたソフトウェアを使用する方が良い方法です。詳細な評価のためではなく、影響力のある友人が必要な場合に備えて、そこで働いている人を知っているため、Chef を選びました。

残念ながら、Chef のドキュメントはこの時点でかなり役に立ちませんでした。これは、数百台のサーバーを管理する場合に書かれているからです。このような単一のサーバーをセットアップする方法に関するドキュメントはほとんどなく、何をすべきかを理解するのにしばらく探し回る必要がありました。

検索する重要なキーワードは、この種の単一サーバーシナリオを処理するための Chef のバージョンである chef-solo です。Vagrant には chef-solo を使用するための必要なフックがあるため、2 つはうまく連携します。[2]

Vagrant VM をセットアップするためのフォルダーには、Vagrantfilecookbooks の 2 つのエントリが含まれています。これは、Chef の手順を含むディレクトリです。(Chef は料理のメタファーを誇張しすぎています。)Chef で基本的なサーバーを起動してプロビジョニングするには、Vagrantfile に次のものが必要でした。

Vagrantfile

  VAGRANTFILE_API_VERSION = "2"
  
  Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.box = "ubuntu/trusty64"
  
    config.vm.provision :chef_solo do |chef|
      chef.add_recipe "mfweb"
    end
  end

これにより、VM イメージを ubuntu trusty 64 ビットシステムに基づいて作成し、「mfweb」レシピを使用して新しいマシンをプロビジョニングするように指示します。

mfweb レシピは cookbooks フォルダー内にあり、展開すると次のようになります

cookbooks
└── mfweb
    ├── files
    │   └── default
    │       └── home-web
    │           └── …
    ├── libraries
    │   └── helpers.rb
    └── recipes
        └── default.rb

Chef では、クックブックはプロビジョニング情報のグループです。クックブックにはさまざまなものが表示されますが、私は主に次の 3 つのセクションが必要でした。

  • files: VM にコピーする必要があるさまざまなファイルとディレクトリ
  • libraries: レシピのヘルパーコード
  • recipes: VM の構成を指定するコード

私は Ruby をよく知っているので、Chef (と Vagrant) の両方がプログラミング言語として Ruby を使用していると便利です。Chef レシピは Ruby の内部 DSL を使用しており、私には非常にうまく機能します。

残念ながら、Chef の全体的な構造は複雑であり、私の単一サーバーの例には必要以上のものです。Chef の使用で最も難しかったのは、実際に理解する必要があるシステムの小さな部分を把握することでした。Chef の主な対象者にとっては複雑さは不要ではありませんが、私にとってはトリッキーでした。

多くの構成ツールと同様に、Chef は可能な限り宣言的な方法で動作します。さまざまなコマンドを順序付けする構成スクリプトではなく、Chef レシピは代わりにマシンの状態を記述しようとします。次に、Chef ランタイムは、この目的の状態とマシンの実際の状態を比較し、マシンを目的の状態にするために必要なアクションを実行します。

たとえば、/home/vagrant にファイル "hello.txt" を表示したいと想像しましょう。レシピファイル (cookbooks/recipes/default.rb) のこのフラグメントは次のとおりです

file "/home/vagrant/hello.txt" do
  content "hello world"
end

このフラグメントは、指定された場所に指定されたコンテンツを持つファイルを配置したいと言っています。レシピを実行すると、Chef はその場所にそのようなファイルがあるかどうかを確認し、ない場合は作成します。また、コンテンツが正しいかどうかを確認し、必要に応じて再度変更します。

このような宣言的な構造は、マシンのプロビジョニングに非常に適しています。ただし、欠点は、物事がうまくいかなくなってデバッグする必要がある場合、どの順序で何が実行されているかを把握するのが非常に難しい可能性があることです。私は Chef の専門家になりたいのではなく、単純な VM を構成したいだけなので、それは問題になる可能性があります。しかし、全体的に、ほとんどの場合、うまく機能します。確かに、私が再び通常のシステム管理作業を行っていたとしたら、このようなツールに精通したいと思うでしょう。

Vagrant のコンテキストでは、プロビジョニングの動作はさまざまな時点で発生する可能性があります。マシンを最初から作成する場合は、作成時にプロビジョニングされます。実行中のマシンがあり、再起動せずに再プロビジョニングする場合は、vagrant provision で実行できます。Vagrant で再起動するには、vagrant reload を使用します。これにより、Vagrantfile の変更も再ロードされます。ただし、vagrant reload --provision で指示しない限り、リロードではプロビジョニングレシピは実行されません。

プロビジョニングの重要な部分の 1 つはソフトウェアをロードすることであり、Ubuntu でこれを行う主な方法は、そのパッケージングシステムを使用することです。Chef では、package コマンドを使用してパッケージをインストールできます。

package 'nodejs'

ChefのレシピはRubyで記述されているため、必要に応じてRubyの言語機能も利用できます。例えば、複数のパッケージをインストールする場合、私はワード配列を簡単に定義して利用する機能を好んで使います。

%w[build-essential openssl libreadline6 libreadline6-dev].each {|p| package p}

新しいユーザーの作成

この仮想マシンを動作させる上で苦労したことの一つは、異なるRubyバージョンへの対応でした。vagrantアカウントは管理に使用しており、その中に異なるRubyバージョンが存在するとプロビジョニングが複雑になるのではないかと懸念しました。そのため、プログラミング作業用に別のユーザーを作成しました。後から考えると、これが本当に役立ったかは疑問であり、将来的に仮想マシンのセットアップを簡略化するために削除するかもしれません。しかし、ここではその方法について説明します。

新しいユーザーの作成は、レシピファイルでユーザーを定義することから始まります。

cookbooks/mfweb/recipes/default.rb

  user "web" do
    home '/home/web'
    shell '/bin/bash'
  end

しかし、これだけではユーザーを作成し、ホームディレクトリを指定するだけで、実際にホームディレクトリを作成するにはさらに作業が必要です。

default.rb

  remote_directory "/home/web" do
    user 'web'
    files_owner 'web'
    source 'home-web'
  end

Chefのremote_directoryリソースを使用して、ソースディレクトリcookbooks/mfweb/files/default/home-webの内容を仮想マシンのホームディレクトリに配置します。仮想マシン上に存在するが、ソースディレクトリにないファイルは変更されません(このようなファイルを削除するオプションもあります)。これにより、.bashrcなどの便利なファイルをソースディレクトリに配置し、プロビジョニング時にそれらをマシンにコピーできます。

これらの手順でユーザーとホームディレクトリは作成されますが、ログインする方法がありません。vagrantユーザーと同じSSHメカニズムを使用するのが妥当であるため、vagrantユーザーの.sshディレクトリをコピーするのが賢明です。Chefのfileリソース(非セキュアキーであるため)の使用も検討しましたが、どの方法を使用しても所有権やパーミッションモードの調整が必要になるため、Chefのシェルコマンド実行機能に頼ることにしました。

default.rb

  execute "copy-ssh" do
    command "cd ~web ; cp -r ~vagrant/.ssh . && chmod 700 .ssh && chown -R web .ssh"
  end

これで、新しいアカウントにログインできます。

vagrant ssh -- -l web

重複を削除するためのヘルパーの使用

これにより、ユーザーとディレクトリはほぼ完璧に作成されますが、ユーザー名とフォルダ名が重複しており、レシピファイルの記述が増えるにつれて、この重複はさらに悪化します。このような重複は定数を使用することで回避できます。例えば、次のようにします。

USER = 'web'
HOME_DIR = File.join('/home', USER)
user USER do 
  home HOME_DIR
  shell '/bin/bash'
end

しかし、私は異なるアプローチを選択し、代わりにヘルパーオブジェクトを定義しました。ヘルパーオブジェクトに必要なデータを設定し、設定内で繰り返しコードが発生するたびにそれを使用します。ヘルパーはcookbooks/mfweb/librariesに存在します。そこにあるRubyファイルは自動的にrequireされ、レシピで使用できるようになります。

helper.rb

  module Mfweb
    class Helper
      attr_reader :user, :ruby_version
  
      def initialize ruby_version, user
        @ruby_version = ruby_version
        @user = user
      end
      def home *args
        File.join("/home", @user, *args)
      end

そして、以下のように使用できます。

default.rb

  helper = Mfweb::Helper.new("2.1.2", 'web')
  
  user helper.user do
    home helper.home
    shell '/bin/bash'
  end
  remote_directory helper.home do
    user helper.user
    files_owner helper.user
    source 'home-web'
  end
  execute "copy-ssh" do
    command  "cd #{helper.home} ; cp -r ~vagrant/.ssh . && chmod 700 .ssh && chown -R #{helper.user} .ssh"
  end

定数ではなくヘルパーを使用するのが、今では私の習慣になっています。文字列操作は関数内で保持し、シンプルな定数を必要に応じて簡単に関数に置き換えられるように、定数よりも関数を使用することを好みます。関数をオブジェクトにまとめることで、状態と関数を明確な名前空間で保持できます。クラスの名前として「ヘルパー」は、単に恣意的な関数とデータの集まりであることを意味するため、通常は嫌いですが、この種のコンテキストでは、その役割を完璧に表現しています。

開発ツリーの同期

何かをビルドできるようにするには、様々なソースを仮想マシンに取得する必要があります。ソースはGitで管理しているため、一つの方法としてGitを使用して仮想マシンにリポジトリをクローンする方法があります。しかし、仮想マシンはビルドを実行するために使用したい一方で、編集はホストマシンで行いたいと考えています。幸いなことに、Vagrantでは、ホストと仮想マシンの間でディレクトリを共有し、変更を加えるたびに同期することが容易です。これを行うには、Vagrantfileで同期されたファイルを宣言します。

Vagrantfile

  Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
    config.vm.synced_folder("..", "/home/web/mfcom", :owner => 'web')
    # other steps in configuration …

私はvagrantのソースをプロジェクトリポジトリ内のフォルダにしたため、同期されたフォルダは親フォルダになります。

この作業をしている際に、面倒な問題に遭遇しました。新しいマシンを最初に作成するときに、同期されたフォルダの作成が拒否され、「"vboxsf"ファイルシステムが利用できません」というエラーが表示されます。しかし、vagrant reloadを実行すると、マシンは正常に起動します。この問題を回避するには、最初にsync_folderの設定をコメントアウトした状態で新しいマシンを実行し、その後、設定を有効にした状態でリロードします。

HTML 出力

ビルドの出力はWebサイトであるため、結果を確認できるように、仮想マシンにApacheを追加するのが妥当です。

default.rb

  package "apache2"
  
  execute "set-html_dir" do
    command "rm -r /var/www/html; ln -s #{helper.html} /var/www/html"
  end

残念ながら、ここではexecuteリソースを使用する必要があります。Chefにはリンクを設定するためのlinkリソースがありますが、Apacheのインストールによって作成された既存のディレクトリエントリを上書きできません。

これで、仮想マシンのポート80をホストのポートにマッピングできます。

Vagrantfile

  Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
      # other config …
      config.vm.network :forwarded_port, guest: 80, host: 2929
  end

rbenv を使用した Ruby 2.1.2 のインストール

Rubyユーザーの多くと同様に、私は自分のラップトップでRubyバージョンを切り替えるためにスイッチャーを使用しています。私はrvmが私のシェルを操作する方法(cdをシェル関数で置き換える)が嫌いなので、rbenvを好んで使用しています。仮想マシンは単一の目的でのみ使用されるため、仮想マシンでスイッチャーをまったく使用しないというのも良い考えです。本番システムのように、適切なRubyバージョンをシステムのRubyとしてインストールすることもできます。しかし、私はrbenvを使用することにしました。そうすることで、仮想マシンを使用せずにツールを直接マシンで実行している私のような人が使用しているシステムを反映することになります。

rbenvとそれに関連するruby-buildをインストールする最初の試みは、Chefのクックブック[3]を使用することでした。しかし、数時間苦労しても、それらを正しく動作させることができませんでした。Rubyを/usr/localではなく~/rbenvにインストールする方法が分からず、gemをインストールしてもgem listで表示されないという問題に陥りました。そこで、Chefのクックブックの使用を諦めました。

次の試みは、プロビジョニング中にインストールが実行できるように、Chefのexecuteリソースを使用することでした。しかし、そこでスクリプトを正しいバージョンで実行することに苦労しました。executeコマンドを、正しいバージョンのRubyを実行するために正しいrbenv shimsのセットを拾うような環境で動作させることができませんでした。そこで、プロビジョニング中にRubyのインストールを行うことを諦め、代わりにプロビジョニング中にできる限りのことを行い、仮想マシン内で手動で実行する必要があるブートストラップスクリプトを使用することになりました。

このすべての最初のステップは、Gitを使用してrbenvリポジトリをダウンロードすることです。

default.rb

  package 'git'
  
  git(helper.rbenv_home) do
    repository 'https://github.com/sstephenson/rbenv.git'
    user helper.user
    revision 'v0.4.0'
  end

このコードについて2つほど。まず、チェックアウトして使用する特定のタグを指定していることに気づくでしょう。これは、再現可能なビルドを持つことが重要であるためです。そうすることで、問題が発生した場合、VagrantセットアップのGit履歴を使用して問題を追跡するのに役立ちます。次に、rbenvのインストール場所のための別のメソッドをヘルパーオブジェクトに記述しました。

helper.rb

  class MfCom::Helper
      def rbenv_home *args
        home('.rbenv', *args)
      end
      …

また、新しいRubyをインストールするrbenvの姉妹プロジェクトであるruby-builderもインストールしたいと考えています。rbenvのinstallコマンドを使用できるように、rbenvのpluginsディレクトリにインストールします。

default.rb

  directory(helper.rbenv_home('plugins')) do
    user helper.user
  end
  
  git(helper.rbenv_home('plugins/ruby-build')) do
    repository 'https://github.com/sstephenson/ruby-build.git'
    user helper.user
    revision 'v20140702'
  end

Chefは、Rubyのコンパイルに必要な様々なライブラリもインストールできます。このリストは、インターネット上のどこかから入手したもので、一部は不要なものもあるかもしれません。

default.rb

  %w[build-essential bison openssl libreadline6 libreadline6-dev
  zlib1g zlib1g-dev libssl-dev libyaml-dev libsqlite3-0
  libsqlite3-dev sqlite3 libxml2-dev libxslt1-dev autoconf
  libc6-dev ssl-cert subversion].each do |p|
    package p
  end

これらすべてが、正しいRubyをインストールするための準備となります。ジョブを完了するために、cookbooks/mfweb/files/default/home-webにブートストラップスクリプトを含めます。

read -r VERSION < mfcom/.ruby-version
if [ -f .rbenv/versions/${VERSION}/bin/ruby ]; then
  echo "ruby ${VERSION} is already installed"
else
  rbenv install $VERSION
fi
cd mfcom
gem install bundler --no-rdoc --no-ri
rbenv rehash
bundle install --without=mac

実行するには、仮想マシンのユーザーはWebアカウントにログインして次を実行する必要があります。

sh bootstrap

ブートストラップは、rbenvで管理される正しいRubyバージョンをコンパイルしてインストールするため、実行に時間がかかります。その後、bundlerもインストールし、それを使用して開発に必要なすべてのgemをインストールします。

coffeescript のインストール

Rubyだけでなく、開発環境にはCoffeeScriptも必要です。幸いなことに、これは簡単にインストールできます。

default.rb

  %w[nodejs npm].each {|p| package p}
  
  execute "node-packages" do
    command "npm install -g coffee-script@1.6.3"
  end
  
  # annoyingly mac and ubuntu use different commands for node
  link "/usr/bin/node" do
    to  "/usr/bin/nodejs"
  end

npm用のChefクックブックを探しませんでしたが、executeオプションで十分に機能するようです。coffeeのバージョンは現在私のラップトップにあるもので、アップグレードを検討する必要があるかもしれません。

それだけの価値はあったか?

全体として、これは私が期待していたよりもはるかに時間がかかり、1週間分の執筆時間を吸い取ってしまいました。これが、将来的に共同作業者の時間を節約できたり、この記事が同様のことを行う他の人々の時間を節約できれば、それだけの価値があるでしょう。もし私が始める前にこの記事に書かれていることを知っていれば、もっと早く終わらせることができたでしょう。

もし、ここで私が述べていることに誤った考えがあれば、ぜひ教えてください。私が現在持っているセットアップを更新する価値はないかもしれませんが、少なくともこの記事に警告や他のアプローチへのポインタを記載することができます。


さらに詳しく知る

Pete Hodgsonは、開発環境をセットアップするためのプロジェクトリポジトリ内の単一の「go」スクリプトの価値について語っています。

謝辞

Danilo Satoは、私がこれらすべてを実行しようとしているときに、いくつかの問題を解決するのを手伝ってくれました。

脚注

1: これは非セキュアキーであり、プライベートキーはVagrantに付属しています。ホスト経由でのみアクセス可能な単純なマシン(このケースのように)では問題ありません。

2: Chefのドキュメントでは、chef-soloではなくchef-clientのローカルモードを使用する必要があると述べています。しかし、その使用方法に関するドキュメントを見つけることができず、少なくとも現時点ではchef-soloがVagrantに適しているようです。

3: これらはchef-rbenvchef-ruby_buildでした。

重要な修正

2014年9月4日: 初版公開