Homebrew vs Boxen を比較して、brewproj に着手
前の投稿で書いたとおり、連休中、開発環境を整理しながら、同僚の開発環境を構築している Boxen から Homebrew へ移行できないかと、技術検証していました。
結論、それぞれ Pros. / Cons. があり、まだ Homebrew で構築するには足りないものがあるなぁ、と思った次第です。
Package 管理は Brewfile だけでやるのが楽。
例えば、Boxen で VMWare Fusion と tree をインストールしたい場合、
Puppetfile
にgithub "vmware_fusion","1.1.0"
を追加modules/people/manifests/$USER.pp
に以下を追加:include vmware_fusion
package { 'tree': ; }
の2工程を踏み、boxen
スクリプトを実行します。
script/boxen
さらに VMWare Fusion がアップデートした場合には、Puppetfile
を更新します。
それをせずに、手動で VMWare Fusion をアップデートし、boxen
スクリプトを再度実行した場合、init.pp に宣言されているバージョンにデグレードしてしまいます。
それに対して、Homebrew は Brewfile に以下の4行を書き、
update upgrade install tree cask install vmware-fusion
brew bundle
コマンドを実行するだけで、常に最新のソフトウェアをインストールしてくれます。
brew bundle
定義ファイルとバージョンの管理は Homebrew に軍配があがります。
Ruby vs Puppet
Boxen は Puppet です。Ruby と違って、システムの自動管理の目的にできたものなので、プログラミング言語としての機能はそこまで高度ではありません。
ライブラリを Ruby 書いて、拡張していくことができます。
Puppet の定義ファイルはシンプルに書けるのですが、構築で躓くと、結局 Puppet の Ruby のソースを読まざるを得なくなります。
これを扱う同僚のほとんどがインフラではなく、アプリケーションエンジニアなので、Puppet を学習してもらうのは、多少コストが高いです。
Homebrew の Formula も DSLできれいな定義ファイルが書けるので、特に Puppet が優位でもないと思います。
# 前に Chef でプログラマティックに recipe を書きすぎて注意されたので、Ruby 乱用厳禁ですが。
では、Boxen はオワコンでおk?
いいえ。
Boxen はバイナリをキャッシュする
Boxen は、sync
コマンドで Homebrew の Celler 配下、rbenv でインストールした Ruby をそれぞれ tarball にしてアップロードし、次にセットアップする人は、ビルドする必要がありません。
https://github.com/boxen/our-boxen/blob/master/script/sync
Ruby, Homebrew は Boxen が独自に拡張している機能です: rubybuild.rb, boxen-bottle-hook
NodeJS は nodenv に元々その機能があるみたいです: nodenv-install。
Project で開発環境構築
この機能が一番大きくて、Project Manifests を書いたら、ソースコードをチェックアウトし、依存しているモジュール、DB 設定など、諸々面倒を見てくれます。
class projects::trollin { include icu4c include phantomjs boxen::project { 'trollin': dotenv => true, elasticsearch => true, mysql => true, nginx => true, redis => true, ruby => '1.9.3', source => 'boxen/trollin' } }
ということで、Homebrew 拡張に着手します。
前途の様に Boxen にも良い所があり、とはいえ、Puppet が面倒臭い、設定が複雑など、超えられない壁があるので、Homebrew を拡張して、それらの足りない機能を補うモジュールを開発しようと思います。
ProjectFormula
まずは、Project Manifests と同じ様なことをできるようにしました。
1. 個人、組織で Tap を作成する。
- リポジトリを作成。 例:
kaizenplatform/homebrew-kaizenplatform
- 基礎クラス
ProjectFormula
:lib/project_formula.rb
- プロジェクト Formula
こんな感じで Formula を書くと、
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib') require "project_formula" class Planbcd < ProjectFormula homepage "https://github.com/kaizenplatform/planbcd/" head "git@github.com:kaizenplatform/planbcd.git", :using => :git depends_on "rbenv" depends_on "ruby-build" depends_on 'readline' depends_on 'openssl' depends_on "mysql" depends_on "imagemagick" depends_on "phantomjs" def install backup_existing copy_cached_download create_database_yml rbenv_install install_bundler start_mysql rake 'db:create' rake 'db:create', 'RAILS_ENV=test' rake 'db:migrate' rake 'db:migrate', 'RAILS_ENV=test' end end
- 依存モジュールをインストール (Homebrew標準機能)
- プロジェクトコードのチェックアウト
config/database.yml
をインストール.ruby-version
に入っている Ruby を rbenv でインストール- Bundler のインストール、
bundle install
の実行 - MySQL 起動
- Rake タスク: DB 作成+マイグレーション
を行ってくれます。
lib/project_formula.rb:
require "formula" class ProjectFormula < Formula def start_mysql system %Q{ln -sfv /usr/local/opt/mysql/*.plist ~/Library/LaunchAgents} system %Q{launchctl load ~/Library/LaunchAgents/homebrew.mxcl.mysql.plist} end def rake cmd, env = '' bundle_exec "rake #{cmd}", env end def bundle_exec cmd, env = '' rbenv_exec "#{env} bundle exec #{cmd}" end def install_bundler rbenv_exec 'gem install bundler && rbenv rehash && bundle install' end def rbenv_exec cmd system %Q{cd #{install_dir} && eval "$(rbenv init -)" && #{cmd.strip}} end def rbenv_install system %Q{rbenv install --skip-existing #{ruby_version}} if ruby_version end def nodenv_install system %Q{nodenv install --skip-existing #{node_version}} if node_version end def ruby_version f = "#{install_dir}/.ruby-version" @ruby_version ||= IO.read(f).strip if File.exists? f end def node_version f = "#{install_dir}/.node-version" @node_version ||= IO.read(f).strip if File.exists? f end def create_database_yml File.open(File.join(install_dir, 'config', 'database.yml'), 'w') {|f| f.write database_yml } end def copy_cached_download FileUtils::mkdir_p src_dir FileUtils::cp_r cached_download, install_dir end def backup_existing if File.directory?(install_dir) File.rename install_dir, "#{install_dir}-org-#{Time.now.strftime '%Y%m%d%H%M%S'}" end end def install_dir File.join src_dir, name end def src_dir ENV['HOMEBREW_PROJECT_SRC_DIR'] || File.join(ENV['HOME'], 'src') end def database_yml <<EOM; development: adapter: mysql2 encoding: utf8 reconnect: false database: #{name}_development pool: 15 username: root password: host: 127.0.0.1 test: adapter: mysql2 encoding: utf8 reconnect: false database: #{name}_test pool: 15 username: root password: host: 127.0.0.1 EOM end end
2. brew tap
コマンドで Tap をインストール。
brew tap kaizenplatform/kaizenplatform
3. brew install
でプロジェクト環境構築
brew install planbcd
使ってみていい感じだったので、Homebrew Cask みたいにしたい。
上記で、必要最低限の環境構築はできました。
さらにこれをワークフロー化するために、Homebrew Cask みたいにサブコマンドを作って、サクサク Formula を作成できるようにしたいです。
# Create new project brew proj create my-project # Edit project brew proj edit my-project # Start, restart, stop project service brew proj start my-project brew proj restart my-project brew proj stop my-project
とりあえず、組織とリポジトリだけ作りました。 brewproj/homebrew-proj
ちゃんと OSS プロジェクトとしてやっていければと思います。