Ruby
Rails
capistrano

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

More than 1 year has passed since last update.

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

その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に

require 'capistrano/rails/assets'

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

  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が何をしているかというと、以下を見るとわかります。

  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に書かれています。以下に、一部抜粋します。

  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を実行するだけです。

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

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

    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 の実装を見てみます。

    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に存在しないファイルをアップロードしていることが見てとれます。

    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)により、決めることができます。

# 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