なぜローカル化する必要があるのか
従来のやり方: capistrano-rails
capistrano公式が公開しているcapistrano-rails (gem)にはassets:precompileを各Webサーバーで実行するタスクが備わっている。gemを入れた後でCapfile内でrequire 'capistrano/rails'
もしくはrequire 'capistrano/rails/assets'
とすると、deploy:assets:precompile
というタスクが生えてきてこの機能が使える。
しかし各Webサーバでコンパイルを実行するということは、Webサーバーにコンパイル処理を行うだけのスペックが求められることを意味する。
webpacker (webpack) メモリ食い過ぎ問題
Rails 6からはwebpackerが同梱されている。Rails 5以前からアップデートを行っている方々の中にはsprocketsとwebpackerを共存させている方もいるだろう。共存させている場合、生のwebpackではなくgemのwebpackerを利用しているのであれば、assets:precompileでsprocketsとwebpacker両方のビルドが実行されるはずである。
そのwebpackerが内部で使用するwebpackは、場合によってはべらぼうにメモリを食う。リッチなフロントエンドを実現するためのJSが増えれば増えるほど、使用メモリは増大する。さらにRollbarやbugsnagのようなエラー検知サービスなどで、圧縮(minify)済みのJSのコードから「どこでJSのエラーが発生したのか」を検知するためにはsource mapをアップロードする必要に迫られることもある。そのためにsource mapを出力しようとするとwebpackは更にメモリを食う。
Webサーバーの増強をすることでメモリ足りない問題は一応回避可能であるが、その分だけお金がかかる。普段のアクセス負荷はそこまででもないのにデプロイの時のためだけにサーバースペックを大幅に上げざるを得ないのはあまりに勿体ないので、assets:precompileをWebサーバー外で行うことで、この問題を回避できる。
もちろんwebpackerのビルドでメモリを軽減するためにSplitChunksPluginを有効化したりTree Shakingが効いているかを確認したりしてwebpackのビルド負荷そのものを下げる対策も必要であるが、この記事ではそこには触れない。
バージョン
- capistrano (3.15.0)
この記事の前提
- sprocketsとwebpackerが共存しており、assets:precompileを実行すると両方のビルドが走る環境である
- sprocketは
public/assets
にファイルを書き出す - webpackerは
public/packs
にファイルを書き出す - capistranoを実行するマシンで
tar
コマンドが使える(大抵使えるはず)
手順
Capfileの編集
もし、Capfileにcapistrano/rails
があったら、まずは3つのタスクに分解する
- require 'capistrano/rails'
+ require 'capistrano/bundler'
+ require 'capistrano/rails/assets'
+ require 'capistrano/rails/migrations'
deploy:assets:precompile
を実現しているのは、capistrano/rails/assets
なので、これを消す。
require 'capistrano/bundler'
- require 'capistrano/rails/assets'
require 'capistrano/rails/migrations'
タスクの定義
config/deploy.rbに追記する。大切なのはrun_locally
とon roles(:web)
を適宜使い分けること。
namespace :deploy do
# ......
# ...
# 1. capistranoを実行したマシン側でコンパイルする
task :compile_assets_locally do
run_locally do
with rails_env: fetch(:stage) do
execute 'bundle exec rails assets:precompile'
end
end
end
# 2. webpackerとsprocketsで生成したファイルをそれぞれzipする
task :zip_assets_locally do
run_locally do
execute 'tar -zcvf ./tmp/assets.tar.gz ./public/assets 1> /dev/null'
execute 'tar -zcvf ./tmp/packs.tar.gz ./public/packs 1> /dev/null'
end
end
# 3. zip後のファイルをupload!でWebサーバーに送り込む。
task :send_assets_zip do
on roles(:web) do |_host|
upload!('./tmp/assets.tar.gz', "#{release_path}/public/")
upload!('./tmp/packs.tar.gz', "#{release_path}/public/")
end
end
# 4. Webサーバー内でunzipする
task :unzip_assets do
on roles(:web) do |_host|
execute "cd #{release_path}; tar -zxvf #{release_path}/public/assets.tar.gz 1> /dev/null"
execute "cd #{release_path}; tar -zxvf #{release_path}/public/packs.tar.gz 1> /dev/null"
end
end
# ...
# ......
end
# どのタイミングでタスクが呼ばれるのかを記述する。
before 'deploy:updated', 'deploy:compile_assets_locally'
before 'deploy:updated', 'deploy:zip_assets_locally'
before 'deploy:updated', 'deploy:send_assets_zip'
before 'deploy:updated', 'deploy:unzip_assets'