asset_syncを利用してS3に静的アセットをアップロード、CloudFrontで高速配信を行う
❓ なぜこの記事を書いたか
Railsを使用したポートフォリオを作成していた時にasset_syncを利用して静的アセットを外部に配置し、CloudFrontより高速配信する構成にしました。
S3のバケットのブロックパブリックアクセスを全てブロックにした状態で構築したところ、asset_syncの設定で詰まってしまいました。
その際の備忘録としてまとめます。
🖥️ 環境
- Ruby 3.0.4
- Rails 6.1.4
- Bundler 2.2.33
- asset_sync 2.15.2
- fog-aws 3.14.0
👣 前提
- Ruby on Railsで作成されたWebアプリケーションが既にある
- AWS EC2で本番環境を構築している
- AWS Route 53により独自ドメインを取得している
🖊️ 本題
やりたいこと
Ruby on Railsで構築されたWebアプリケーションはアセットパイプラインによりCSSやJavaScriptなどの静的ファイルを圧縮し、連結して使用しています。
そのアセットのプリコンパイルは$ RAILS_ENV**=**production bin/rails assets:precompile
で提供され、/public/assets配下に出力されています。
また、Webpacker導入以降はwebpackによるビルドシステムもRailsに同梱されるようになったため、webpackによりコンパイルされたファイルは/public/packs配下に出力されています。
asset_syncはこれら/public/assetsや/public/packsに出力されたコンパイル済みのファイルをS3などのストレージサービスと共有することができます。
このS3に共有された静的アセットをCloudFrontを通じて公開し、Webアプリケーションに提供することが今回の目的です。
なお、webpackerのコンパイルタスクは assets:precompile
のrakeタスクに含まれますので、Capistranoなどによる自動デプロイでも assets:precompile
が走るタスクでは実行されているようです。
Webpackerは
assets:precompile
のrakeタスクにwebpacker:compile
タスクを追加するので、assets:precompile
を使う既存のデプロイパイプラインはすべて動作します。
CloudFrontをS3の全段に配置することでブラウザとCloudFront間でHTTPS通信を行うことができ、S3でホストしている静的アセットをセキュアに配信することができます。
手順
実装するために以下のような手順が必要になります。
- S3バケットの作成
- ACMによるSSLサーバ証明書の取得
- CloudFrontの設定
- Route 53でCloudFrontを指すレコードを定義
- asset_syncの導入
なお、1〜4までの内容は以下の記事でまとめています。
重複する内容が多いため今回は簡単にまとめています。
CloudFrontを利用してCarrierWaveでS3にアップロードした画像を表示する
S3バケットの作成
AWSのS3のコンソールより「バケットを作成」
以下のような設定を行います。
ここでのポイントはACL無効にし、ブロックパブリックアクセス設定を全てブロックに設定することです。
特にブロックパブリックアクセス設定についてはチェックを外すやり方もあるようですが、後に説明する”IAMユーザーに適切なポリシーを付与する”、”CloudFrontのS3バケットアクセスでOAIを使用する”ことで補うことができました。
ACMによるSSLサーバ証明書の取得
HTTPS通信を行うため、SSLサーバ証明書を取得します。
なお、CloudFrontに設定するSSLサーバ証明書はバージニア北部で作成したものしか使用できませんでしたので注意してください。
リージョンをバージニア北部に変更 > AWS Certificate Manager > 証明書 > 証明書をリクエスト
- 証明書タイプ→パブリック証明書をリクエスト
- ドメイン名 → ex) rails-test-static.example.com CloudFrontに設定したいドメイン名を記述
- DNSの検証を選択
検証を行ったのち、作成ステータスが「発行済み」変わったらOK
CloudFrontの設定
CloudFront > ディストリビューションを作成
-
オリジンドメインでS3で作成したバケットのドメイン名を指定
-
S3バケットアクセスS3側でブロックパブリックアクセスをすべてブロックに設定しているため、こちらでOAIを作成することでアクセスできるように設定
- 新しいOAIを作成→初回はS3のバケットポリシーにOAIを自動追加するため、「はい、バケットポリシーを自動で更新します」を選択
- 新しいOAIを作成→初回はS3のバケットポリシーにOAIを自動追加するため、「はい、バケットポリシーを自動で更新します」を選択
-
ビュワープロトコルポリシー→Redirect HTTP to HTTPS
ブラウザからCloudFrontへHTTPでアクセスされた時、HTTPSにリダイレクトするように設定
-
代替ドメイン名→CloudFrontに設定したいドメイン名を設定します。
-
カスタムSSL証明書→ACMで取得した証明書を選択
Route 53でCloudFrontを指すレコードを定義
CloudFrontのディストリビューションが作成できたら、DNSにレコードを登録します。
Route 53 > ホストゾーン > ドメイン名 > レコードを作成
シンプルルーティング > シンプルなレコードを定義
- 値/トラフィックのルーティング先で「CloudFrontディストリビューションへのエイリアス」> 作成したディストリビューションを選択
asset_syncの導入
IAMユーザーの作成
RailsアプリケーションからS3にアクセスするために新たにAWS IAMにてポリシーの作成を行います。
-
README.mdを参考にasset_sync用のポリシーを作成
IAM > ポリシー > ポリシーを作成
{
"Version": "2012-10-17",
"Statement": [
{
"Action": "s3:ListBucket",
"Effect": "Allow",
"Resource": "arn:aws:s3:::rails-test-static"
},
{
"Action": "s3:PutObject*",
"Effect": "Allow",
"Resource": "arn:aws:s3:::rails-test-static/*"
}
]
}
- IAM ユーザーの作成
IAM > ユーザー > ユーザーを作成
AWS認証タイプをアクセスキーとし、新たにIAMユーザーを作成します。
既存のポリシーを直接アタッチにて先ほど作成したポリシーを選択します。
作成されたアクセスキーIDとシークレットアクセスキーを使用しますので、csvファイルを忘れずにダウンロードしておいてください。
- credentials.yml.encで環境変数として設定
作成したIAMユーザーのアクセスキーIDとシークレットアクセスキーを登録します。
❯ EDITOR='vi' bin/rails credentials:edit
aws:
# S3のバケットの設定
s3_region: ap-northeast-1 # バケットを作成したリージョン名
s3_bucket: rails-test-static # バケット名
s3_host: https://rails-test-static.example.com # CloudFrontに設定したドメイン名
# IAMアクセス設定
access_key_id: xxx
secret_access_key: xxx
gemのインストール
gem "asset_sync"
gem "fog-aws"
$ bundle install
asset_syncの設定
- 設定ファイルを作成する
Rails のジェネレーターを利用してasset_sync用の設定ファイルを作成します。
S3を利用する場合は--provider=AWS
を指定してください。
$ bin/rails g asset_sync:install --provider=AWS
# asset_syncの設定ファイルが作成される
Running via Spring preloader in process 6259
create config/initializers/asset_sync.rb
- 設定を記述
こちらもREADME.mdを参考に設定していきます。
Webpackerを使用する場合もREADME.mdに設定方法が明示してありますので、それをもとに作成しました。
if defined?(AssetSync)
AssetSync.configure do |config|
config.enabled = false if Rails.env.development?
config.fog_public = false # S3バケットはブロックパブリックアクセスを全てブロックに設定しているため、こちらでも非公開を設定
# AWSの設定
config.fog_provider = 'AWS'
config.fog_region = Rails.application.credentials.dig(:aws, :s3_region)
config.fog_directory = Rails.application.credentials.dig(:aws, :s3_bucket)
config.aws_access_key_id = Rails.application.credentials.dig(:aws, :access_key_id)
config.aws_secret_access_key = Rails.application.credentials.dig(:aws, :secret_access_key)
# S3にある元のアセットファイルを保持する
config.existing_remote_files = 'keep'
config.aws_session_token = ENV['AWS_SESSION_TOKEN'] if ENV.key?('AWS_SESSION_TOKEN')
# webpackerに対応
config.run_on_precompile = false
config.add_local_file_paths do
public_root = Rails.root.join('public')
Dir.chdir(public_root) do
packs_dir = Webpacker.config.public_output_path.relative_path_from(public_root)
Dir[File.join(packs_dir, '/**/**')]
end
end
end
end
ここでの注意点はS3のブロックパブリックアクセスの設定に応じて fog_public = false
を設定することです。
この値はデフォルトで true
ですので、ここを false
にしないとコンパイル時に以下のようなエラーが発生します。
$ bin/rails assets:precompile RAILS_ENV=production
yarn install v1.22.19
[1/4] Resolving packages...
success Already up-to-date.
Done in 0.67s.
Everything's up-to-date. Nothing to do
rails aborted!
Excon::Error::BadRequest: Expected(200) <=> Actual(400 Bad Request)
excon.error.response
:body => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>AccessControlListNotSupported</Code><Message>The bucket does not allow ACLs</Message><RequestId>xxx</RequestId><HostId>xxx=</HostId></Error>"
:cookies => [
]
:headers => {
"Connection" => "close"
"Content-Type" => "application/xml"
"Date" => "Sun, 19 Jun 2022 02:19:22 GMT"
"Server" => "AmazonS3"
"x-amz-id-2" => "xxx"
"x-amz-request-id" => "xxx"
}
:host => "rails-test-static.s3.ap-northeast-1.amazonaws.com"
:local_address => "xx.xx.xx.xx"
:local_port => 49618
:path => "/packs/css/application-38b8d4a2.css"
:port => 443
:reason_phrase => "Bad Request"
:remote_ip => "xx.xx.xx.xx"
:status => 400
:status_line => "HTTP/1.1 400 Bad Request\r\n"
/app_name/lib/tasks/asset_sync.rake:3:in `block in <main>'
/app_name/bin/rails:5:in `<top (required)>'
/app_name/bin/spring:31:in `block in <top (required)>'
/app_name/bin/spring:28:in `<top (required)>'
- lib/tasks/asset_sync.rake
こちらもREADME.mdに従います。
webpacker:compile
の実行後にassets:symc
を実行させるようにtaskを追加しています。
Rake::Task#enhanceの理解についてはこちらを参考にさせていただきました。
if defined?(AssetSync)
Rake::Task['webpacker:compile'].enhance do
Rake::Task['assets:sync'].invoke
end
end
production環境のasset_hostにCloudFrontを設定する
config.asset_host = Rails.application.credentials.dig(:aws, :s3_host)
プリコンパイルを実行してみる
- production環境でプリコンパイルを実行
$ bin/rails assets:precompile RAILS_ENV=production
Capistranoによる自動デプロイを行なっている場合はこちらでもasset_syncは実行されます
$ bundle exec cap production deploy
- S3のバケットにassets/とpacks/が作成され、コンパイル後のファイルが格納されていることが確認できます。
本番環境でアクセスし、headタグ内のstylesheetやscriptタグのsrcがCloudFrontに指定したURLになっていればOKです。
同時にhttpsでアセットを取得できていることも確認できます。
📓 まとめ
Railsアプリケーションにおいてasset_syncを使用し、静的アセットをS3にアップロードすることができました。
また、S3の全段にCloudFrontを配置することでセキュア且つ高速に静的アセットをブラウザに届けることができます。
今回は特にasset_syncの fog_public = false
の設定で詰まってしまいました。
同様の構成で設定されている方の一助になれば幸いです。
☕ 参考にさせていただきました
先人の方々の知見に沢山助けられました。
本当にありがとうございました。
Discussion