🌥️

CloudFrontを利用してCarrierWaveでS3にアップロードした画像を表示する

2022/06/18に公開

❓ なぜこの記事を書いたか

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により独自ドメインを取得している
まだの場合は・・。

これらの参考資料が非常に参考になります!

https://pikawaka.com/rails/carrierwave

https://qiita.com/take18k_tech/items/5710ad9d00ea4c13ce36

🖊️ 本題

やりたいこと

まず今回やりたいことの全体像は以下のようなフローです。

  • S3の全段にCloudFrontを配置し、画像を高速で配信できるようにする
  • WebAppとS3の間にCloudFrontを設置することによりHTTPSによる通信を行う

そのために、以下のような手順で進めていきます。

  1. S3でバケットの作成
  2. EC2インスタンスにロールの設定
  3. CarrierWaveによるWebApp→S3へのアップロード
  4. ACMによるSSLサーバ証明書の取得
  5. CloudFrontの設定
  6. CarrierWaveによるWebApp→CloudFront→S3へのアップロード

1. S3バケットの作成

AWSのS3のコンソールより「バケットを作成」
以下のような設定を行います。

  • バケット名→ ex) rails-test-static
  • AWSリージョン→ ex) ap-northeast-1
  • オブジェクト所有者 →ACL無効
  • このバケットのブロックパブリックアクセス設定→全てブロックにチェック
  • 残りはデフォルトの設定

ここでのポイントはACL無効にし、ブロックパブリックアクセス設定を全てブロックに設定することです。
特にブロックパブリックアクセス設定についてはチェックを外すやり方もあるようですが、後に説明する”EC2へのroleの設定”、”CloudFrontのS3バケットアクセスでOAIを使用する”ことで補うことができました。

ブロックパブリックアクセスについてより詳しくは・・・

言葉が異様に分かりにくいですが以下資料が参考になります!

https://zenn.dev/ymasutani/articles/019959e7c990b1

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を追加します。

Gemfile
gem 'fog-aws'
$ bundle install

CarrierWaveの設定ファイルの作成

config/initializer/carrier_wave.rbを作成しCarrierWaveの設定を記述します。

config/initializer/carrier_wave.rb
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/initializer/carrier_wave.rb
    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の設定

app/uploaders/image_uploader.rb
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) サービスです。

https://aws.amazon.com/jp/cloudfront/

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 を追加します。

config/initializers/carrier_wave.rb
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メソッドをオーバーライドして設定します。

app/uploaders/image_uploader.rb
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環境のみで適用するため以下のような設定となりました。

app/uploaders/image_uploader.rb
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の変更が必要となります。
ここでだいぶ詰まってしまいましたので、同じように悩んでいる方の一助になれば幸いです。

☕ 参考にさせていただきました

先人の方々の知見に沢山助けられました。
本当にありがとうございました。

https://pikawaka.com/rails/carrierwave

https://zenn.dev/ymasutani/articles/019959e7c990b1

https://qiita.com/pocari/items/acdb45dbec3eb0e0656e

https://qiita.com/take18k_tech/items/5710ad9d00ea4c13ce36

https://zenn.dev/junara/articles/1058a853a4f792

https://saitoxu.io/2017/09/carrierwave-with-cloudfront

Discussion