使用 Vagrant、Chef 和 rbenv 設定一個 Ruby 開發 VM
我在設定一個 Vagrant VM 時的一些經驗筆記,以幫助合作者使用我的網路出版工具鏈。我使用 Chef 準備 VM,並使用 rbenv 安裝和控制正確版本的 Ruby。
2014 年 9 月 4 日
我有一個用於建置 martinfowler.com 的工具鏈。我大約在 2000 年開始使用它,當時類似的工具很少。在那些日子裡,靜態網站並不流行,但我滿喜歡透過 rsync 將我的網站部署到只需要 Apache 的伺服器上。隨著時間的推移,工具鏈變得更強大、更複雜,但我喜歡它開發的方式,它是一個讓我可以舒適地工作和探索新想法的家。
近年來,我有更多同事和朋友使用工具鏈在我的網站上撰寫文章。為了與他們合作,我設定了一個精簡版的核心網站儲存庫,我們使用 git 進行協作。由於我的合作者大多是程式設計師,因此這個工作流程相當有效。
要執行所有這些,有必要安裝一些軟體。我為工具鏈使用的所有軟體都是開源的,但最近出現了一些安裝問題。特別是,你會發現許多基本的 Ruby 安裝都很舊,所以我們需要安裝一個較新的 Ruby 版本。由於工具鏈會處理 XML,所以我使用 Nokogiri。這是一個很好的工具,但安裝起來可能很麻煩。在最近幾個月,我有幾個合作者浪費了數小時試圖安裝它。
一年前(或兩年前),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
這會告訴 Vagrant 建立一個基於 64 位元 ubuntu trusty (也就是 14.04) 的 VM。有了這個 Vagrantfile,您就可以使用 vagrant up
建立並啟動一個 VM。一旦機器建立並啟動,您就可以使用 vagrant ssh
登入。您會注意到 Vagrant 已經建立了一個稱為 vagrant
的使用者,並使用 ssh 金鑰讓您登入。 [1] vagrant 使用者可以在沒有密碼的情況下使用 sudo,而 Vagrant 使用它來控制機器管理。
您可以使用 vagrant halt
停止機器,並使用 vagrant destroy
完全銷毀機器。 vagrant up
指令會啟動現有的機器,或者在尚未建立機器的情況下建立並啟動機器。Vagrant 使用一個預設機器,有多種方法可以處理多個機器,但我尚未探索它們。
使用 Chef 準備
Vagrant 讓我有了一個裸機器,但這樣的機器需要使用軟體配置,這樣我才能用它做有用的事情。這個操作的重點是讓它盡可能自動化,因此合作者只需執行幾個指令,就可以讓 VM 準備就緒,而不需要尷尬的安裝說明。
執行此操作的方法之一可能是執行 VM 中的安裝指令碼,但通常更好的方法是使用專門用於配置機器軟體,例如 Puppet、Chef 或 Ansible。我選擇 Chef,不是因為任何詳細的評估,而是因為我認識在那裡工作的人,以防我需要一位有影響力的朋友。
不幸的是,Chef 的文件在這個時候沒有什麼幫助,因為它是為您管理數百台伺服器的情況而編寫的。關於只設定單一伺服器幾乎沒有文件,我必須搜尋一陣子才能找出我該做什麼。
Google 的關鍵字是 chef-solo,這是 Chef 的一種版本,用於處理這種單一伺服器情境。Vagrant 有必要的掛鉤可與 chef-solo 搭配使用,因此兩者可搭配得很好。 [2]
我用來設定 Vagrant VM 的資料夾包含兩個項目:Vagrantfile
和 cookbooks
,後者是一個目錄,其中包含 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」食譜來提供新機器與 chef-solo。
mfweb 食譜位於 cookbooks 資料夾中,展開後如下所示
cookbooks └── mfweb ├── files │ └── default │ └── home-web │ └── … ├── libraries │ └── helpers.rb └── recipes └── default.rb
在 Chef 中,食譜是一組提供資訊。各種資訊會出現在食譜中,但我需要三個主要部分
- 檔案:需要複製到 VM 的各種檔案和目錄
- 函式庫:食譜的輔助程式碼
- 食譜:指定 VM 設定的程式碼
由於我非常熟悉 ruby,因此 Chef(和 Vagrant)都使用 ruby 作為其程式語言,這很方便。Chef 食譜使用 ruby 內部 DSL,對我來說相當好用。
然而,在較不令人滿意的地方,Chef 的整體結構很複雜,超過我單一伺服器範例的需要。使用 Chef 最困難的部分是找出我實際需要了解的系統小部分。對 Chef 的主要受眾來說,複雜性並非不必要,但對我來說確實很棘手。
與許多設定工具一樣,Chef 盡可能以宣告方式運作。Chef 食譜並非設定指令碼,用於對各種指令進行排序,而是嘗試描述機器狀態。然後,Chef 執行時間會將此所需狀態與機器實際狀態進行比較,並執行將機器帶到所需狀態所需的任何動作。
例如,假設我們希望檔案「hello.txt」出現在 /home/vagrant
中。食譜檔案(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
告訴它執行。
配置的重要部分之一是載入軟體,而使用 Ubuntu 進行此動作的主要方法是透過其封裝系統。使用 Chef,你可以使用 package 指令安裝封裝
package 'nodejs'
由於 Chef 指令是 ruby,如果我願意,我也可以使用 ruby 的語言功能。因此,如果我有多個封裝要安裝,我喜歡使用它輕鬆定義和使用字串陣列的能力
%w[build-essential openssl libreadline6 libreadline6-dev].each {|p| package p}
建立一個新使用者
嘗試讓此 VM 運作的其中一個磨難是處理不同的 ruby 版本。vagrant 帳戶用於管理,而我擔心在其中擁有不同的 ruby 版本會導致配置產生複雜性。因此,我為程式設計工作建立了一個獨立的使用者。事後看來,我不確定這是否有幫助,我可能會在未來移除它以簡化 VM 的設定,但以下是我如何執行的步驟。
建立新使用者從在指令檔案中定義使用者開始。
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
的內容放入 VM 的家目錄。VM 上不在來源目錄中的任何檔案都不會受到影響(有一個選項可以清除這些檔案)。然後,我可以將 .bashrc
和其他方便的檔案放入來源目錄,並在每次配置時將它們複製到機器上。
這些步驟會建立使用者和家目錄,但不會提供我們任何登入的方式。使用與 vagrant 使用者相同的 ssh 機制是有道理的,因此複製 vagrant 使用者的 .ssh
目錄似乎是合理的。我考慮使用 Chef 的檔案資源(因為它是非安全的金鑰),但無論我使用哪種方法,都需要調整所有權和權限模式,所以我訴諸 Chef 執行 shell 指令的能力。
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 檔案都會自動需要並提供給食譜。
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
使用輔助工具而不是常數現在已成為我的習慣。我比較喜歡在函式中保留任何字串處理,而且比較喜歡使用函式而不是常數,因此我可以輕鬆地隨意用函式取代簡單的常數。將函式綑綁到物件中,讓我可以將狀態與函式一起保存在明確的命名空間中。我通常不喜歡「輔助工具」作為類別的名稱,因為它暗示的只是一個任意的函式和資料集合,但在這種情況下,它完美地描述了它的角色。
同步開發樹狀結構
為了能夠建構任何東西,我們需要將各種來源放入 VM。由於我將來源保存在 git 中,因此執行此操作的方法之一是使用 git 在 VM 中複製儲存庫。但是,儘管我想使用 VM 來執行建構,但我寧願在我的主機上進行所有編輯。幸運的是,Vagrant 可以輕鬆地在主機和 VM 之間共用目錄,只要您進行變更,就會同步它們。為此,您將宣告 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
,它會正常啟動機器。我可以透過先執行新的機器,並將同步資料夾組態註解掉,然後重新載入它,來解決這個問題。
HTML 輸出
建構的輸出是網站,因此將 apache 加入 VM 是有道理的,這樣我們才能看到結果。
default.rb
package "apache2" execute "set-html_dir" do command "rm -r /var/www/html; ln -s #{helper.html} /var/www/html" end
遺憾的是,我必須在此處使用執行資源。Chef 有連結資源來設定連結,但不會覆寫 apache 安裝建立的現有目錄項目。
有了它,我就可以將 VM 上的埠 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 版本之間切換。我比較喜歡 rbenv,因為我不喜歡 rvm 處理我的 shell 的方式(用 shell 函式取代 cd)。由於 VM 僅用於單一目的,因此有一個很好的論點,完全不使用切換器 - 我可以像在生產系統中那樣,將適當的 ruby 版本安裝為系統 ruby。但我決定使用 rbenv,因為這樣它會反映那些像我一樣在沒有 VM 的情況下直接在他們的機器上執行工具的系統。
我安裝 rbenv 及其相關的 ruby-build 的第一個方法是使用 Chef 食譜 [3]。但在與它們奮戰了幾個小時後,我無法讓它們正常工作。我無法找出如何將 rubies 安裝到 ~/rbenv
而不是 /usr/local
,而且我陷入了一個困境,我會安裝一個 gem,但它不會顯示在 gem list
中。所以我放棄了 Chef 食譜。
我的下一個嘗試是使用 Chef 執行資源,以便安裝可以在配置期間執行。但是,我在讓腳本使用正確版本執行的過程中遇到了麻煩。我無法讓執行命令使用環境來執行,因此它會選擇正確的 rbenv shim 來執行正確版本的 Ruby。因此,最後放棄在配置期間執行 Ruby 安裝,而是盡可能在配置期間執行,然後使用需要在 VM 內手動執行的引導腳本。
所有這些的第一步是使用 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
關於該片段的幾件事。首先,您會注意到我指定了一個特定標籤以進行簽出和使用。我這樣做是因為擁有 可重製建置 非常重要。這樣,如果遇到問題,我可以使用 Vagrant 設定的 git 歷程記錄來協助追蹤問題。其次,我在輔助物件上撰寫了另一個方法,用於 rbenv 安裝位置。
helper.rb
class MfCom::Helper def rbenv_home *args home('.rbenv', *args) end …
我也想安裝 ruby-builder,這是 rbenv 的姊妹專案,用於安裝新的 Ruby。我將其安裝到 rbenv 中的 plugins 目錄,以便可以使用 rbenv 的 install 命令。
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
要執行它,VM 的使用者需要登入網路帳戶並執行
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 食譜,執行選項似乎運作良好。Coffee 的版本是我目前在筆電上的版本,我應該考慮升級它。
值得嗎?
總的來說,這一切都比我預期的花費了更長的時間,而且耗費了大約一週的撰寫時間。如果它可以節省我的合作者未來的時間,或者這篇文章在執行類似操作時可以節省其他人的時間,那麼這將是值得的。如果我在開始之前就知道這篇文章的內容,我肯定會做得更快。
請告訴我,我這裡討論的任何事情是否錯誤。現在更新我的設定可能不值得,但我至少可以將一些警告和指向其他方法的指標放入這篇文章中。
進一步閱讀
Pete Hodgson 談論 專案儲存庫中單一「go」指令碼的價值 以設定您的開發環境。
致謝
Danilo Sato 在我嘗試執行所有這些時協助我解決了幾個問題。
腳註
1: 這是個不安全的金鑰,私鑰與 Vagrant 一起提供。對於僅可透過主機存取的簡單機器(如本例),這沒問題。
2: Chef 文件指出您應使用 chef-client 的本機模式,而非 chef-solo。然而,我找不到任何有關如何使用它的文件,而 chef-solo 似乎是 Vagrant 喜歡的,至少目前是這樣。
3: 這些是 chef-rbenv 和 chef-ruby_build
重大修訂
2014 年 9 月 4 日:首次發布