capistrano-rails + asset_sync + S3で構成されたasset配信の仕組みを解明する


[asset_sync](https://github.com/AssetSync/asset_sync)を使って、RailsのassetsをS3から配信するパターンはほぼ定石となっている感があり、現在携わっているサービスも例外に漏れずこのパターンを採用しています。


その[asset_sync](https://github.com/AssetSync/asset_sync)ですが、導入がカンタンであまり迷うことがないので、勢いブラックボックスになりがちなのかなと思ってます。（少なくとも自分は詳しく知りませんでした^^;）


今回、assetsの配信周りで発生したトラブルを契機に、capistrano-railsとasset_syncについて調べたので、備忘録としてまとめておきます。

なお、運用環境は以下の通りです。

- sprockets (3.6.3)
- capistrano (3.4.1)
- capistrano-rails (1.1.7)
- asset_sync (1.0.0)
- fog (1.38.0)


## capistrano-rails

Capistrano v3向けのRails固有タスクを追加するためのgemです。

Capfileに

```ruby
require 'capistrano/rails/assets'
```

と書くだけで、updated hook に deploy:compile_assets 等のタスクが登録されます。

```ruby
  after 'deploy:updated', 'deploy:compile_assets'
  after 'deploy:updated', 'deploy:cleanup_assets'
  after 'deploy:updated', 'deploy:normalize_assets'
  after 'deploy:reverted', 'deploy:rollback_assets'
```
https://github.com/capistrano/rails/blob/v1.2.3/lib/capistrano/tasks/assets.rake#L59-L62 



では、deploy:compile_assetsが何をしているかというと、以下を見るとわかります。

```ruby
  namespace :assets do
    task :precompile do
      on release_roles(fetch(:assets_roles)) do
        within release_path do
          with rails_env: fetch(:rails_env) do
            execute :rake, "assets:precompile"
          end
        end
      end
    end
  end
```
https://github.com/capistrano/rails/blob/v1.2.3/lib/capistrano/tasks/assets.rake#L64-L73



## rake assets:precompile

rake assets:precompileで実行しているタスクの詳細を知りたい場合は、

```
bundle exec rake assets:precompile --trace
```

を実行すればokです。


```
$ bundle exec rake assets:precompile --trace
** Invoke assets:precompile (first_time)
** Invoke assets:environment (first_time)
** Execute assets:environment
** Invoke environment (first_time)
** Execute environment
** Execute assets:precompile
** Invoke assets:sync (first_time)
** Invoke assets:environment
** Execute assets:sync
```

assets:precompile後にassets:syncを実行していることがわかります。

どういった仕組みで実行しているかは、asset_syncのREADMEに書かれています。以下に、一部抜粋します。

```ruby
  if Rake::Task.task_defined?("assets:precompile:nondigest")
    Rake::Task["assets:precompile:nondigest"].enhance do
      Rake::Task["assets:sync"].invoke if defined?(AssetSync) && AssetSync.config.run_on_precompile
    end
  else
    Rake::Task["assets:precompile"].enhance do
      Rake::Task["assets:sync"].invoke if defined?(AssetSync) && AssetSync.config.run_on_precompile
    end
  end
```

Rake::Task#enhance により、assets:precompile => assets:sync という順序でタスクが実行されるようになっています。



## asset_sync

assets:syncタスクはシンプルでAssetSync.syncを実行するだけです。

```ruby
  namespace :assets do
    desc "Synchronize assets to S3"
    task :sync => :environment do
      AssetSync.sync
    end
  end
```


コードを追うと、AssetSync::Storage#syncに辿り着きます。これがS3への同期処理のコア部分です。

```ruby
    def sync
      # fixes: https://github.com/rumblelabs/asset_sync/issues/19
      log "AssetSync: Syncing."
      upload_files
      delete_extra_remote_files unless keep_existing_remote_files?
      log "AssetSync: Done."
    end
```
https://github.com/AssetSync/asset_sync/blob/v1.0.0/lib/asset_sync/storage.rb#L230-L236


さらに、upload_files と delete_extra_remote_files の実装を見てみます。

```ruby
    def upload_files
      # get a fresh list of remote files
      remote_files = ignore_existing_remote_files? ? [] : get_remote_files
      # fixes: https://github.com/rumblelabs/asset_sync/issues/19
      local_files_to_upload = local_files - ignored_files - remote_files + always_upload_files
      local_files_to_upload = (local_files_to_upload + get_non_fingerprinted(local_files_to_upload)).uniq
      ...
```

upload_filesメソッドではlocal_files と remote_files の差分をチェックして、remoteに存在しないファイルをアップロードしていることが見てとれます。


```ruby
    def delete_extra_remote_files
      log "Fetching files to flag for delete"
      remote_files = get_remote_files
      # fixes: https://github.com/rumblelabs/asset_sync/issues/19
      from_remote_files_to_delete = remote_files - local_files - ignored_files - always_upload_files
```

delete_extra_remote_filesでも差分をチェックしていますが、こちらはlocalに存在しないファイルを削除していることがわかります。

なお、delete_extra_remote_filesを実行するかは設定（config.existing_remote_files）により、決めることができます。

```ruby
# AssetSync::Config
    def existing_remote_files?
      ['keep', 'ignore'].include?(self.existing_remote_files)
    end
```


## まとめ

だいぶ長くなったのでまとめます。

 - capistrano-railsはdeploy:compile_assets等のcapタスクを追加するgem
 - asset_syncはrake assets:precompileからinvokeされる
 - asset_syncはlocalとremoteのファイル差分を計算し、よしなに同期してくれる


## References

 - [Rails で静的ファイルを撒く時に考えること](http://blog.livedoor.jp/sonots/archives/41721638.html)
 - [Rake で任意のタスクの前後に別のタスクを実行する](https://www.hsbt.org/diary/20120210.html)