Elastic Beanstalkで作った環境でRailsを運用していたのですが、そのままだといろいろ不都合な事態が発生してしまうことに気づきました。そこで、アセットだけS3に展開する環境を作ることとしました。
そのままの設定で生じる問題点
もちろん、そのままの設定でも表示自体は正常に行われます。ただし、不都合な点が生じることになります。
まず、ローカルでアセットを生成する場合はtmp/
ディレクトリの中に一時ファイルが作られて、元ファイルが変わらなければ一時ファイルを再利用して生成するので、一部のファイルだけ更新した場合は比較的短時間で終了します。一方、Elastic Beanstalkの場合、デプロイごとにディレクトリが作り直されて一時ファイルが残らないので1、毎回最初からのプリコンパイルとなって、時間がかかってしまします。
さらに、デプロイ作業はサーバごとに進みますので、複数サーバで運用する場合にも各サーバでプリコンパイルが走ってしまい、処理能力のムダ使いとなってしまいます。
もっと言えば、静的ファイルを配信するだけならS3を使ったほうが効率的なので、本来ならEC2の資源を使うまでもありません。
assets_sync
というgem
ちょうどこのような環境に対応するためのgemとして、asset_sync
というものがあります(GitHub)。これは、プリコンパイルで生成したアセットを、別なクラウドサーバに送信しておくというものです。もちろんS3にも対応していますので、EB環境であれば比較的容易に導入できます。
まず、Gemfile
にgem 'asset_sync'
を追加してbundle install
を行い、そしてrails g asset_sync:install --provider=AWS
として初期化ファイルのひな形を生成します(全部環境変数で設定する方法もありますが、ほかの場所でfog
を使うと干渉して厄介だし、アセット用のバケツは別途で用意したほうがいいと思いますので、設定ファイルの構築をおすすめします)。
設定ファイルができあがったら、何点か書き換えないといけない点があります。
-
asset_sync
は本番環境時、そしてそこへデプロイするファイルのプリコンパイル時以外は不要ですので、それ以外の状況ではGemfile
の書き方によって読み込ませないという選択もありです。そうすると、AssetSync
が見つからなくなるので、外側のif
文が必要となります。 - アセットは専用のバケツに入れたほうがいいので、
config.fog_directory
は適宜設定しましょう。あと、東京リージョンの場合はその設定も必要です(ap-northeast-1
)。 - AWSのキーやシークレットは別な環境変数でも構いませんが、原則ソースには書かないほうがいいです。
dotenv-rails
を使って、ローカルでは.env
ファイル、EB上ではEBから環境変数を設定、というのが適当でしょう。
if defined?(AssetSync)
AssetSync.configure do |config|
config.fog_provider = 'AWS'
config.fog_directory = 'バケツの名前'
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
# Don't delete files from the store
# config.existing_remote_files = 'keep'
#
# Increase upload performance by configuring your region
config.fog_region = 'ap-northeast-1'
#
# Automatically replace files with their equivalent gzip compressed version
# config.gzip_compression = true
#
# Use the Rails generated 'manifest.yml' file to produce the list of files to
# upload instead of searching the assets directory.
# config.manifest = true
#
# Fail silently. Useful for environments such as Heroku
# config.fail_silently = true
end
end
そして、環境設定ファイルの方で
# 東京リージョンの場合
config.action_controller.asset_host = 'https://s3-ap-northeast-1.amazonaws.com/バケツ名/'
とすれば、S3から参照できるようになります。とはいえ、これだけではハッシュ付きリソースのハッシュを判別できず、ロードに失敗します。
プリコンパイル時に、public/assets/
直下に、マニフェストとなるJSONファイルが生成しています。これをGitリポジトリに追加してコミットしましょう。なお、それ以外のファイルは不要なので、以下のように.gitignore
へ設定しておくのがいいかもしれません。
/public/assets/
!/public/assets/*.json
もうひと手間加えて
このままの設定にしていると、JavaScriptやCSSは、そのまま配信されます。gzip圧縮をかけることで、
- エンドユーザーのダウンロード時間が節約できる
- サーバ側でも、Amazonの転送量課金を節約できる
と一石二鳥です。一度設定すればずっと節約できるので、設定しておきましょう。
設定だけでは動かない
ちょうどAssetSync
の設定にconfig.gzip_compression = true
という設定があるのですが、これだけでは何も変化しません。上のコメントにも書いてありますが、このオプションの動作は「クラウドにアップすべきファイルのgzip版があったときに、本来のファイルの代わりにアップロードする」という意味合いです。自分でgzipファイルを用意しなければ動きません。
もっとも、gzip圧縮したファイルを用意するのはそこまで手間ではありません。
#!/bin/bash
# 現実問題として、スペース入りのアセットを用意することはほぼ考えられないので、
# そういうイレギュラーな場合は考えていません
for FILE in $( find public/assets -type f \( -name '*.css' -or -name '*.js' \) )
do
gzip -c $FILE > $FILE.gz
done
そして、rake assets:sync
とすると、アセットの同期だけしてくれます。ただし、さらに注意点があって、gzip前のファイルを転送していると、すでにファイルがあると判断して圧縮バージョンはサーバに送られません2。そこで、config.run_on_precompile = false
としてassets:precompile
時の送信を止めた上で、以下のような手順でデプロイすることになります。
rake assets:precompile
- gzipファイルの生成
rake assets:sync
- マニフェストファイルのコミットへの追加
eb deploy
手動でするには重量級の手順となってしまいますので、何かしら自動運用させましょう。