独立して読んでいただける様書いていますが、S3+CloudFrontでWebフォントを利用する(FireFox対応)の続編的位置づけです。
heroku で運用していると CSS、JS、画像、WebフォントなどをCDNで配信したくなるので、そのやり方をまとめました。
実は heroku の仕様変更で構成を変えざるを得なかったためにやったんですが、結果的にいい形にまとまったと思います。
大いに参考にしたサイトがこちら。
CloudFront CDN with Rails on Heroku
こちらを読めばこの記事は読まなくてもいいかも。
Asset Sync をやめる。
Asset Sync は、heroku へのデプロイ時に実行される rake assets:precompile の際に、S3 などの外部ストレージに assets の中身をコピーしてくれる gem です。
元から使っていなかった方はこのステップは飛ばしてください。
構成変更前はこの gem を利用して Amazon S3 へ静的ファイルをコピーし、CloudFront へバケツリレー的に配信していました。
S3 を使わなくなるので、この gem もお役御免。
特に ON/OFF などの設定は存在しないので、ごく普通に gem から Asset Sync を削除します。
#gem "asset_sync"
CloudFront のオリジンサーバを heroku にする。
CloudFront はオリジンサーバ(ファイルの送信元をこう呼びます)を S3 以外にも指定できます。
文字のみですが、設定方法は以下のように。
AWS のマネージメントコンソールから CloudFront を開きます。
エッジサーバ(オリジンサーバからファイルを受け取って実際にユーザに配信するサーバをこう呼びます)の設定が一覧で並んでいるので、対象となるDistribution の「i」マークをクリック。
「Origins」タブを開くとオリジンサーバの一覧(たいていひとつしかありません)が表示されるので、変更対象のオリジンサーバにチェックを入れ、「EDIT」ボタンを押します。
今まで S3 をオリジンサーバにしていた場合、「Origin Domain Name」の項目には 「bucketname.s3.amazonaws.com」 といった S3 の URL が入っているはずです。
これを、「appname.herokuapp.com」という記述に変えます。appname は実際にオリジンサーバにすることになる、貴方が運用している heroku アプリのアプリ名が入ります。
右下の「Yes,Edit」ボタンを押すと画面表示がちょこっと変わります。
「Origin Protocol Policy」が「HTTP Only」になっていますので、必要に応じて「Match Viewer」に変えてください。
この設定はオリジン ⇔ エッジサーバ間を非 SSL で接続するか、それともエンドユーザ ⇒ エッジサーバのアクセスと同じプロトコルで接続するか(エンドユーザがエッジサーバへ SSL でアクセスしたら、オリジン ⇔ エッジサーバ間も SSL になる)の設定です。
heroku アプリを独自ドメインで運用していて、SSL を使えるように設定していない場合は「HTTP Only」である必要があります。
弊社の Job-Hub は SSL 対応していますので、ここでは「Match Viewer」にしてみました。
もう一度右下に「Yes,Edit」ボタンが現れますので、設定を反映させてください。
15分程度かかりますが、オリジンサーバの設定が heroku に変更されます。
新規に CDN の設定をする場合は、「Create Distribution」ボタンを押してCDNを新たに構築します。
オリジンサーバが S3 の例ですが、こちらを参考に構築してみてください。
あるいは heroku 公式の Using Amazon CloudFront CDN | heroku dev center を読んでみると、CDN を使用した時にどのようにアクセスが処理されるのか理解できるのでお勧めです。
ただし、 asset_host の項目は↓のように修正した方が良いです。
asset_host を修正する
config/environment/production.rb で、以下のように修正します。
config.action_controller.asset_host = "//#{ENV['CDN_ASSET_HOST']}" unless ENV['CDN_ASSET_HOST'].blank?
環境変数を heroku に設定
以下のコマンドで heroku へ環境変数を設定します。
heroku config:add CDN_ASSET_HOST=(CloudFrontのホスト名) -a appname
CloudFront のホスト名は AWS のマネージメントコンソールで確認しましょう。
hogehogefugafuga.cloudfront.net という形式になっています。
環境変数を使う必要がない方は、asset_host にそのまま CloudFront のホスト名を記載しても構いません。
config.action_controller.asset_host = '//hogehogefugafuga.cloudfront.net '
という風に。
ただしこの書き方、production のほかに staging を、とか考え始めると途端に面倒になって来るので、最初から環境変数バージョンでやっておくのを強くお勧めします。
Web フォントを使わない場合は、以上の手順で CDN を利用できるようになります。
デプロイして動かしてみましょう。
Webフォント対応を行う。
Chrome や Safari では問題ないのですが、IE や FireFox は CDN 経由で Webフォントを使用する場合、Same Origin Poricy が壁となって立ちはだかるため、CORS対応が必要になってきます。
S3 + CloudFront だった時は CORS 対応は S3 でやっていたんですが、オリジンサーバが heroku アプリになったので、CORS 対応も heroku アプリでやる必要が出てきました。
この対応を行えるそのものずばりな rack-cors という gem があるので、これを使用します。
gem 'rack-cors', require: 'rack/cors'
下記のコードを追加します。
# Allow font files to be loaded from anywhere (for loading webfonts in Firefox)
require 'rack/cors'
use Rack::Cors do
allow do
origins '*'
resource '/assets/icomoon/*', headers: :any, methods: :get
end
end
#- See more at: http://singlebrook.com/blog/cloudfront-cdn-with-rails-on-heroku#sthash.TTdcfp2x.dpuf
resource に指定するのはフォントファイルの URI です。
ソース上の物理的な位置とは違いますので、注意してください。
Web フォントを読み込む CSS はこんな感じです。
弊社では icomoon を使っているのでこんな感じですが、一番人気の Font-Awesome でもファイル名が違う程度で基本同じことになります。
@font-face {
font-family: 'icomoon';
src:url('<%= asset_path 'icomoon/icomoon.eot' %>');
src:url('<%= asset_path 'icomoon/icomoon.eot' %>?ie01');
src:url('<%= asset_path 'icomoon/icomoon.eot' %>?ie02');
src:url('<%= asset_path 'icomoon/icomoon.eot' %>?ie03');
src:url('<%= asset_path 'icomoon/icomoon.eot'%>?#iefix') format('embedded-opentype'),
url('<%= asset_path 'icomoon/icomoon.woff' %>') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?ie01') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?ie02') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?ie03') format('woff'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>') format('truetype'),
url('<%= asset_path 'icomoon/icomoon.svg' %>#icomoon') format('svg');
font-weight: normal;
font-style: normal;
}
/* Firefox hack start */
@-moz-document url-prefix('http://appname.herokuapp.com') {
@font-face {
font-family: 'icomoon';
src:url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox01') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox02') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox03') format('woff'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox01') format('truetype'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox02') format('truetype'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox03') format('truetype');
font-weight: normal;
font-style: normal;
}
}
@-moz-document url-prefix('https://appname.herokuapp.com') {
@font-face {
font-family: 'icomoon';
src:url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox_ssl01') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox_ssl02') format('woff'),
url('<%= asset_path 'icomoon/icomoon.woff' %>?firefox_ssl03') format('woff'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox_ssl01') format('truetype'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox_ssl02') format('truetype'),
url('<%= asset_path 'icomoon/icomoon.ttf' %>?firefox_ssl03') format('truetype');
font-weight: normal;
font-style: normal;
}
}
/* Firefox hack end */
これで完了。デプロイしたらテストしてみましょう。
ファイル名の MD5 ダイジェストあたりは当然環境によるので、適宜読み替えてください。
オリジンサーバ向け:
curl -I -H 'Origin: http://appname.herokuapp.com' http://appname.herokuapp.com/assets/icomoon/icomoon-0848150e94ea8b6258e58dc83da18e1c.woff?firefox_ssl01
エッジサーバ向け:
curl -I -H 'Origin: http://appname.herokuapp.com' http://hogehogefugafuga.cloudfront.net/assets/icomoon/icomoon-0848150e94ea8b6258e58dc83da18e1c.woff?firefox_ssl01
こんなレスポンスが返ってくれば成功です。
HTTP/1.1 200 OK
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: http://appname.herokuapp.com
Access-Control-Expose-Headers:
Access-Control-Max-Age: 1728000
Age: 0
Content-length: 28964
Content-Type: application/font-woff
Date: Mon, 07 Apr 2014 12:45:48 GMT
Last-Modified: Mon, 07 Apr 2014 11:46:50 GMT
Status: 200 OK
Vary: Origin
X-Content-Digest: 62969ddfd28bad52bf01817e0ec1e3fe84bd8e25
X-Rack-Cache: miss, store
Connection: keep-alive
特に重要なのが Access-Control-Allow-Origin: http://appname.herokuapp.com
の部分で、これがないとダメ。
オリジンサーバでもエッジサーバでもこうなるはずなので、なってなければ設定を見直しましょう。
特に、エッジサーバはただのリバースプロキシみたいなもので、オリジンサーバのレスポンスをそのままキャッシュしてエンドユーザに返すのが仕事なので、まずはオリジンサーバのレスポンスが正常になるよう設定を行う必要があります。
エッジサーバの結果は HTTP ヘッダごとキャッシュされてしまうので、設定修正後はこの辺りを参考にキャッシュクリアするか、?以降のクエリ文字列を毎回デタラメにしてテストを繰り返しましょう。
また、スキーマを含めて Origin: ヘッダの完全一致が必要なので、SSL と非 SSL 両方でサービスを提供しているなら、両方テストする必要があります。
他、CORSとは?とかこのような設定が必要な理由は前記事にまとめていますので、こちらを参照してください。
S3+CloudFrontでWebフォントを利用する(FireFox対応)
補足ですが、最初に書いた参照元にも紹介されている Font Assets も試したんですが、残念ながら動きませんでした。
なんとかできたかもしれませんが、割と限られた時間で対策を講じてたのでさっさと諦めて rack-cors を使っています。
こちらはすんなり導入できるとってもいい子でした。
さいごに。
今回こんなことをやったのは、heroku の user-env-compile が使えなくなったから。
告知はこちら。
App environment available in all builds
この機能は、heroku へのアプリケーションデプロイ時に、環境変数を読めるようにする、というもの。
heroku config:add
で追加する環境変数は、普通はデプロイ時に読めないんですね。
なので、デプロイ時に動作する Asset Sync などは、S3 の認証情報などを環境変数に設定できるように この機能を利用するのが定番化しています。
本番やステージング環境で異なるS3を使いたいし、そもそも認証情報なんてコードに埋め込みたくないですからね。
この機能はあくまで実験的なもので将来削除される可能性がある、というのは告知されていましたが、当たり前のように使われていたので正直痛いところです。
だからこそ削除に踏み切ったのかなあ。
代わりってことになっている ENV_DIR も、なんかちょっと違うような。
なお、user-env-compile がずっと非推奨だった理由はこの辺りから読み取れます。
Why Do Heroku Builds Run Without Config by Default?
デプロイが環境変数に依存するのは変(環境変数を更新したらデプロイし直しになってしまう)ってことらしいんですが。
サポートにも問い合わせてみたんですが、同様の返答がありまして。
実は私の壮大な勘違いで、user-env-compile がデフォルトで使用可能になったとかなら嬉しいんですが。
the environment variables aren't available on deploys anymore って言われたしなあ。
最後までお読みいただきありがとうございます。
なかなか短くまとめられず分かりにくい箇所がたくさんあると思いますが、ご指摘いただければ随時修正します。
質問やまさかりも大歓迎です。