devひよこのあしあと

いつでもひよっこな気持ちで学びと挑戦を

AssetSyncでAzure BlobにRailsの静的ファイルを自動的に保存する

AzureでRailsアプリを運用されている皆様に朗報です。

アセットプリコンパイルで自動的にクラウドストレージに静的ファイルをアップロードしてくれるAssetSync gemがAzure Blobにも対応しました!! AssetSync PR#363

とりあえず結論

Gemfileに下記を追加

gem 'asset_sync', '~> 2.4.0'
gem 'fog-azure-rm'
$ bundle install
$ bundle exec rails g asset_sync:install --use-yml --provider=AzureRM

上記のコマンドで生成されたconfig/asset_sync.ymlを下記のように編集

defaults: &defaults
  fog_provider:               AzureRM
  azure_storage_account_name: ストレージアカウント名
  azure_storage_access_key:   ストレージアクセスキー
  fog_directory:              コンテナー名
  fog_region:                 リージョン # 西日本ならjapanwest
  existing_remote_files:      keep

development:
  <<: *defaults
  enabled: false

test:
  <<: *defaults
  enabled: false

production:
  <<: *defaults
  enabled: true

そしてアセットプリコンパイルの実行です。

$ RAILS_ENV=production bundle exec rake assets:precompile

あとは、アップロードしてある静的ファイルの方を参照させるようにconfig/environments/production.rbを設定します。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
  end
  ...
end

解説

それぞれ設定内容などについて解説していきます。

Gemfile

gem 'asset_sync', '~> 2.4.0'
gem 'fog-azure-rm'
gem 'azure-core', '~> 0.1.14', require: false # ← なくてもいいです

AzureBlob対応したAssetSync gemは2.4.0になるので2.4.0以降を指定します。 fog-azure-rm gemはAssetSyncがAzureBlobを操作するために必要となります。

azure-core gemはfog-azure-rmをインストールすれば依存関係で自動的にインストールされるので明示的にGemfileに記述する必要はありませんが、0.1.14以降を利用することを明示したいために記述することをオススメします。

というのも、azure-core gemはアセットプリコンパイルされた静的ファイルたちをAzure Blobへアップロードするために利用されていますが、0.1.14より前のバージョンではAzure側から接断されてしまったときのリトライ処理がうまく動作せずに失敗してしまうバグがあるためです。 Azure Core PR#52 Azure Core PR#55

たぶん0.1.14をGemfileで明示しなくても bundle update azure-core するだけでもよいはず。

AzureSyncの設定

AzureSyncを設定する方法には以下の3種類があります。

  1. config/initializers/asset_sync.rb
  2. config/asset_sync.yml
  3. 環境変数

3つ目の環境変数は、1つ目2つ目の設定ファイルが存在しなかった場合に参照されます。

1つ目と2つ目の設定ファイルはいずれか一方でもいいですし、両方を併用することもできます。 併用した場合は、initializerの方が先に読み込まれ、yamlの値で上書きされる、という順番で評価されます。

yamlの方でもERB形式のロジックを記述できるので充分な表現力がありますが、一部の設定項目(アップロード対象ファイルを追加する設定やログ出力設定)はinitializerの方でしか設定できませんので、それを使いたい場合はinitializerを使うか併用するとよいと思います。

設定例

私が開発・メンテしているRailsアプリでは下記のような内容で併用するようにしました。

yamlの方には認証情報などのクレデンシャルなものを記述し、ansibleやcapistranoなどのデプロイツールを使ってステージング毎に違う設定ファイルを配置する想定です。

config/initializers/asset_sync.rb

if defined?(AssetSync)
  AssetSync.configure do |config|
    # Don't delete files from the store
    config.existing_remote_files = "keep"

    # Use the Rails generated 'manifest.yml' file to produce the list of files to
    # upload instead of searching the assets directory.
    config.manifest = true

    # Log silently. Default is `true`. But you can set it to false if more logging message are preferred.
    # Logging messages are sent to `STDOUT` when `log_silently` is falsy
    config.log_silently = false
  end
end

config/asset_sync.yml

defaults: &defaults
  fog_provider:               AzureRM
  azure_storage_account_name: ストレージアカウント名
  azure_storage_access_key:   ストレージアクセスキー
  fog_directory:              Blobコンテナー名
  fog_region:                 japanwest

development:
  <<: *defaults
  enabled: false

test:
  <<: *defaults
  enabled: false

production:
  <<: *defaults
  enabled: <%= $STAGE != "DEV" %> # ← DEV/STAGING/PRODUCTIONと開発工程毎に分かれている定数値でAssetSyncの利用を切り分け

アップロードした静的ファイルを参照させる設定

Railsのビューで生成されるlinkタグやimgタグのURLをアップロードしたAzure側のホストに向けるためにはconfig/environments/production.rbを下記のように設定します。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
  end
  ...
end

上記はAzureBlobに対して匿名のHTTPアクセスを許可させている場合の設定です。AzureCDNを使う場合は.blob.core.windows.net の部分を .azureedge.net としてください。(もちろん、Azure側は適切に設定してくださいね。)

なお、linkタグやimgタグのURLを全部Azureに向ける場合は上記の設定でOKですが、一部のJSやCSS、画像ファイルをRailsコントローラーやnginxなどのフロントWebサーバーに処理させたい場合には下記のようにProcオブジェクトで条件分岐させることができます。Procを評価した結果nilが返った場合は外部ホストを参照しないURLが生成されます。

Rails.application.configure do
  ...
  if defined?(AssetSync) && AssetSync.enabled?
    config.action_controller.asset_host = Proc.new { |src, request|
        if src =~ /^\/assets/
          "//#{AssetSync.config.azure_storage_account_name}.blob.core.windows.net/#{AssetSync.config.fog_directory}"
        end
      }
  end
  ...
end

おまけ

上記までで基本的にはAssetSyncでAzureBlobに静的ファイルを保存させることができます。

以下はハマった箇所についての補足です。

CSSファイルから外部ホスト参照している場合のキャッシュ回避

dt.menu-tree-open {
    background-image: url(<%= asset_path("menu_tree/minus.png") %>);
}

上記のようにCSSファイル内で画像ファイルを指定している場合に前述の設定で外部ホストを参照するようにできますが、設定を変更して別のホストにしたり、一時的に外部ホストを使わないようにしたという場合にアセットプリコンパイルを実行してもキャッシュが効いてしまって再作成されずに古い設定のまま外部ホストを参照してしまいました。

キャッシュを削除してからプリコンパイルを実行すればもちろん解決するのですが、そのために全てのキャッシュを削除してプリコンパイルをやり直すとちょっと時間がかかりすぎてしまうのであまりやりたくないです。

そこで、画像ファイルを利用しているCSSファイルをxxx.cssからxxx.css.erbに名称変更して下記の内容を加えます。

/* 画像ファイルパスのキャッシュ回避 */
<% depend_on Rails.root.join("config/asset_sync.yml") %>
<% depend_on Rails.root.join("config/environments/production.rb") %>

<% asset_host = Rails.app_class.config.action_controller.asset_host %>
<% if asset_host.is_a?(Proc) %>
dummy_tag {
  background-image: url(<%= asset_host.call(Rails.app_class.config.assets.prefix) %>) ;
}
<% else %>
dummy_tag {
  background-image: url(<%= asset_host %>) ;
}
<% end %>

dt.menu-tree-open {
    background-image: url(<%= asset_path("menu_tree/minus.png") %>);
}

depend_onで指定したファイルが更新されているとプリコンパイルで再作成する対象となります。また、dummy_tagというところに外部参照ホストを書き出すようにすることでプリコンパイル結果が変わるので無事にAssetSync関係の変更が反映されるようになります。

おわりに

AssetSyncでAzureBlobへ静的ファイルを自動的にアップロードするやり方を紹介しました。

もともとAssetSyncはAzureに対応していなかったのですが、プルリクを送ってみたら採用してもらえました。はじめてのプルリクでドキドキしていましたがピカチュウっぽい人が丁寧に対応してくれて助かりました。自分の変更が取り込まれて世界中の人に使ってもらえるっていいですね〜。