CloudFrontを利用してCarrierWaveでS3にアップロードした画像を表示する
❓ なぜこの記事を書いたか
Railsを使用したポートフォリオを作成していた時にCarrier Waveを利用してS3へ画像のアップロードする構成にしました。
本番環境をEC2で構築しS3の全段にCloud Frontを設置しようとしたところ、画像のパスが上手くCloud Frontに向かず少し詰まってしまいました。
その際の備忘録としてまとめます。
🖥️ 環境
- Ruby 3.0.4p208
- Rails 6.1.4
- Bundler 2.2.33
- CarrierWave 2.2.2
- fog-aws 3.14.0
👣 前提
- Railsアプリケーションを作成し、CarrierWaveを利用した画像投稿機能を実装している
- AWS EC2で本番環境を構築している
- AWS Route 53により独自ドメインを取得している
まだの場合は・・。
これらの参考資料が非常に参考になります!
🖊️ 本題
やりたいこと
まず今回やりたいことの全体像は以下のようなフローです。
- S3の全段にCloudFrontを配置し、画像を高速で配信できるようにする
- WebAppとS3の間にCloudFrontを設置することによりHTTPSによる通信を行う
そのために、以下のような手順で進めていきます。
- S3でバケットの作成
- EC2インスタンスにロールの設定
- CarrierWaveによるWebApp→S3へのアップロード
- ACMによるSSLサーバ証明書の取得
- CloudFrontの設定
- CarrierWaveによるWebApp→CloudFront→S3へのアップロード
1. S3バケットの作成
AWSのS3のコンソールより「バケットを作成」
以下のような設定を行います。
- バケット名→ ex) rails-test-static
- AWSリージョン→ ex) ap-northeast-1
- オブジェクト所有者 →ACL無効
- このバケットのブロックパブリックアクセス設定→全てブロックにチェック
- 残りはデフォルトの設定
ここでのポイントはACL無効にし、ブロックパブリックアクセス設定を全てブロックに設定することです。
特にブロックパブリックアクセス設定についてはチェックを外すやり方もあるようですが、後に説明する”EC2へのroleの設定”、”CloudFrontのS3バケットアクセスでOAIを使用する”ことで補うことができました。
ブロックパブリックアクセスについてより詳しくは・・・
言葉が異様に分かりにくいですが以下資料が参考になります!
2. EC2インスタンスにロールの設定
S3にアクセスするためにロールを作成
IAM > ロール > ロールを作成
エンティティタイプで「AWSのサービス」を選択し、ユースケースは「EC2」とします。
ポリシーを作成よりJSON形式でポリシーを定義します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "VisualEditor0",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:PutObjectAcl",
"s3:ListAllMyBuckets"
],
"Resource": "*"
}
]
}
ポリシー作成後、許可ポリシーとしてロールにアタッチします。
ロール名は自分が分かりやすい名前を設定してください。
EC2インスタンスにロールをアタッチ
EC2 > インスタンス > xxxxx(インスタンスID)
アクションより、セキュリティ→IAMロールを変更を選択します。
そこで先ほど作成したIAMロールをインスタンスにアタッチします。
この設定をした上で、CarrierWave設定の use_iam_profile: true
を記述すると、EC2インスタンス上からS3バケットへアクセスすることができるようになります。
3. CarrierWaveによるWebApp→S3へのアップロード
S3のバケットを作成しましたので、CarrierWaveを使用しRailsアプリケーションからS3へ画像をアップロードするように設定します。
Gemのインストール
CarrierWaveでS3に画像をアップロードするためにGemfileにfog-awsを追加します。
gem 'fog-aws'
$ bundle install
CarrierWaveの設定ファイルの作成
config/initializer/carrier_wave.rbを作成しCarrierWaveの設定を記述します。
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_public = false # リソースへの直接のアクセスを制限する
config.fog_credentials = {
provider: 'AWS',
region: Rails.application.credentials.dig(:aws, :s3_region),
use_iam_profile: true, # EC2インスタンスに設定したIAMロールから権限情報を取得する
}
config.fog_directory = Rails.application.credentials.dig(:aws, :s3_bucket)
end
end
S3バケットでパブリックブロックアクセスをブロックしているため、CarrierWaveの設定でも config.fog_public = false
で非公開に設定しています。
デフォルトは true
となっています。
ここの設定が矛盾していると画像アップロード時にエラーが出ます。
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>xxxxx=</HostId></Error>"
また、use_iam_profile: true
を設定することでインスタンスに設定したIAMロールからfog-awsが権限情報を取得してくれます。
もし別でIAMユーザーを作成し、Railsアプリケーションに設定する場合はこのように設定してください。
config.fog_credentials = {
provider: 'AWS',
region: Rails.application.credentials.dig(:aws, :s3_region),
aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
}
config/initializer/carrier_wave.rbで設定した環境変数をcredentialsに追加します。
$ EDITOR='vi' bin/rails credentials:edit
# credentials.yml.encを編集する
aws:
# S3の設定
s3_region: ap-northeast-1 # バケットを作成したリージョン名
s3_bucket: rails-test-static # バケット名
# use_iam_profileを使用しない場合はIAMユーザー情報も設定する
# access_key_id: xxx
# secret_access_key: xxx
Uploaderの設定
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
# production環境でS3にアップロードする
if Rails.env.production?
storage :fog
else
storage :file
end
~~
4. ACMによるSSLサーバ証明書の取得
SSLサーバ証明書を取得するにあたり、Route53で独自ドメインを取得している前提で進めます。
それではCloudFrontへHTTPS接続するためSSLサーバ証明書を取得します。
リージョンをバージニア北部に変更
AWS Certificate Manager > 証明書 > 証明書をリクエスト
- 証明書タイプ→パブリック証明書をリクエスト
- ドメイン名 → ex) rails-test-static.example.com CloudFrontに設定したいドメイン名を記述
- DNSの検証を選択
Route 53でドメインを取得している場合、簡単にDNSの検証を行うことができます。
リクエストを作成した証明書のステータス > Route 53でレコードを作成
ステータスが「発行済み」に変わったらOKです。
5. CloudFrontの設定
AWSではCloudFrontについて以下のような説明がされています。
Amazon CloudFront は、高いパフォーマンス、セキュリティ、デベロッパーの利便性のために構築されたコンテンツ配信ネットワーク (CDN) サービスです。
S3の全段にCloudFrontを配置するように設定することでS3に保存した画像ファイルを高速で配信することができるようになります。
また、WebブラウザとCloudFront間でHTTPS通信を行うことで簡単にセキュアな通信を実現することができます。
CloudFront > ディストリビューションを作成
-
オリジンドメイン→S3で作成したバケットのドメイン名を指定します
-
S3バケットアクセス
S3側でブロックパブリックアクセスをすべてブロックに設定しているため、こちらでOAIを作成することでアクセスできるように設定します。- OAIを使用しますを選択→新しいOAIを作成をクリックします
- 初回はS3のバケットポリシーにOAIを自動追加するため、「はい、バケットポリシーを自動で更新します」を選択します
-
ビュワープロトコルポリシー→Redirect HTTP to HTTPS
ブラウザからCloudFrontへHTTPでアクセスされた時、HTTPSにリダイレクトするように設定します。
-
代替ドメイン名→CloudFrontに設定したいドメイン名を設定します。
-
カスタムSSL証明書→先ほどACMで取得した証明書を選択します
注意)証明書はバージニア北部に存在するものしか選択できません
-
デフォルトルートオブジェクト→index.html
通常使用する場合は設定する必要はないと思いますが、今回は後にCloudFrontへのアクセステストを行うためファイル名を設定します。
ここにはhttps://CloudFrontURL/
にアクセスした際のデフォルトのファイルを指定します。
ここを設定しない状態でhttps://CloudFrontURL/
にアクセスした場合、以下のようなエラーが発生します。
-
S3のバケットポリシーからCloudFrontからのアクセス設定が追加されていることが確認できます。
DNSへCloudFrontへのレコードを登録する
CloudFrontのディストリビューションが作成できたら、DNSにレコードを登録します。
Routes 53 > ホストゾーン > ドメイン名 > レコードを作成 よりACMでSSLサーバ証明書を取得した際のドメイン名を定義します。
ここで値/トラフィックのルーティング先で「CloudFrontディストリビューションへのエイリアス」から、作成したディストリビューションを選択します。
接続テスト
AWSで各サービスの設定が完了しましたので接続テストを行ってみます。
- EC2インスタンスにSSH接続しS3へファイルをアップロードします
$ vim index.html
<html><body>This is test page. Here is my S3 bucket</body><html>
# index.htmlをS3にアップロード
$ aws s3 cp index.html s3://rails-test-static
upload: ./index.html to s3://rails-test-static/index.html
- 独自ドメインでCloudFrontにアクセスし、index.htmlファイルがHTTPS接続で取得できているかを確認します
6. CarrierWaveによるWebApp→CloudFront→S3へのアップロード
環境変数の追加
CloudFrontでディストリビューションを作成しましたので、アセットのhostとして設定していきます。
$ EDITOR='vi' bin/rails credentials:edit
# credentials.yml.encを編集する
aws:
# S3の設定
s3_region: ap-northeast-1
s3_bucket: rails-test-static
s3_host: https://rails-test-static.example.com # CloudFrontに設定したドメイン名を追加
CarrierWaveの設定を修正
config/initializers/carrier_wave.rbに asset_host
を追加します。
if Rails.env.production?
CarrierWave.configure do |config|
config.fog_public = false
config.fog_credentials = {
provider: 'AWS',
region: Rails.application.credentials.dig(:aws, :s3_region),
use_iam_profile: true,
}
config.fog_directory = Rails.application.credentials.dig(:aws, :s3_bucket)
config.asset_host = Rails.application.credentials.dig(:aws, :s3_host) # 追加
end
end
Uploaderの設定を修正する
私はここでだいぶ詰まってしまったのですが、そのままでは画像のURLを表示するurlメソッドはCloudFrontのURLを返してくれません。
例えば、userモデルに紐つくimageリソースのurlは以下のようなパスで指定されます。
このままではCloudFrontを介してリソースを取得することができませんでした。
irb(main):005:0> user.image.url
=> "/uploads/user/image/1/user_1.jpg"
そのためこのようにurlメソッドをオーバーライドして設定します。
class ImageUploader < CarrierWave::Uploader::Base
def initialize(*)
super
# production環境ではインスタンス変数(asset_host)にDNSサーバーを指定する
self.asset_host = Rails.application.credentials.dig(:aws, :s3_host)
end
~~
# urlで取得できる画像ファイルのパスを変更
def url
return "#{asset_host}/#{store_dir}/#{identifier}?updatedAt=#{model.updated_at.to_i}" if path.present?
super
end
end
再度画像のURLを確認してみます。
irb(main):003:0> user.image.url
=> "https://rails-test-static.example.com/uploads/user/image/1/user_1.jpg?updatedAt=1655298375"
最終的にS3の利用をproduction環境のみで適用するため以下のような設定となりました。
class ImageUploader < CarrierWave::Uploader::Base
include CarrierWave::MiniMagick
if Rails.env.production?
storage :fog
else
storage :file
end
def initialize(*)
super
return unless Rails.env.production?
self.asset_host = Rails.application.credentials.dig(:aws, :s3_host)
end
~~
def url
if path.present?
# 保存先がローカルの場合
return "#{super}?updatedAt=#{model.updated_at.to_i}" if Rails.env.development? || Rails.env.test?
# 保存先がS3の場合
return "#{asset_host}/#{store_dir}/#{identifier}?updatedAt=#{model.updated_at.to_i}"
end
super
end
end
CloudFrontを介して画像ファイルが配信されているか確認
実際にproduction環境で画像を投稿してみます。
投稿された画像のURLに設定したCloudFrontのURLが含まれていたら成功です!!
※httpsで接続されていることも確認できます。
📓 まとめ
CarrierWaveを利用してS3に画像を保存し、その画像をCloudFrontを介して取得することができました。
特にCarrierWaveのUploader設定ではCloudFrontを利用するためにURLの変更が必要となります。
ここでだいぶ詰まってしまいましたので、同じように悩んでいる方の一助になれば幸いです。
☕ 参考にさせていただきました
先人の方々の知見に沢山助けられました。
本当にありがとうございました。
Discussion