capistranoのassets:precompileをローカル側で実行し、各Webサーバーにはコンパイル済みファイルを配る

# なぜローカル化する必要があるのか

## 従来のやり方: capistrano-rails

[capistrano](https://github.com/capistrano/capistrano)公式が公開している[capistrano-rails (gem)](https://github.com/capistrano/rails)には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つのタスクに分解する

```diff_ruby:Capfile
- require 'capistrano/rails'
+ require 'capistrano/bundler'
+ require 'capistrano/rails/assets'
+ require 'capistrano/rails/migrations'
```

`deploy:assets:precompile`を実現しているのは、`capistrano/rails/assets`なので、これを**消す**。

```diff_ruby:Capfile
  require 'capistrano/bundler'
- require 'capistrano/rails/assets'
  require 'capistrano/rails/migrations'
```

## タスクの定義

config/deploy.rbに追記する。大切なのは`run_locally`と`on roles(:web)`を適宜使い分けること。

```rb:config/deploy.rb
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'
```

# 参考

- https://github.com/capistrano/rails
- https://qiita.com/ymiyamae/items/17035ffe9d3f5cf20ef0