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 プロジェクトとしてやっていければと思います。