2022-12-17に更新

S3からアセットを配信する

完成後のアーキテクチャ

image

S3 バケットへのアクセス権を EC2 インスタンスに付与する

※ 事前に AWS CLI version 2 以降をインストールしておくこと。

  1. Amazon S3 へのアクセスを許可する AWS Identity and Access Management (IAM) プロファイルロールを作成します。

  2. IAM インスタンスプロファイルをインスタンスにアタッチします。

  3. S3 バケットのアクセス許可を検証します。

  4. EC2 インスタンスから Amazon S3 へのネットワーク接続を検証します。

  5. S3 バケットへのアクセスを検証します。

引用元:EC2 インスタンスに S3 バケットへのアクセス権を付与する | aws.amazon.com

以下はEC2にアタッチしたロールのポリシー例:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1664856193519",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:ListBucket",
                "s3:ListBucketVersions",
                "s3:PutObject"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:s3:::your-bucket-name",
                "arn:aws:s3:::your-bucket-name/*"
            ]
        }
    ]
}

以下はS3バケットポリシーの例:

{
    "Version": "2012-10-17",
    "Id": "Policy1665296051262",
    "Statement": [
        {
            "Sid": "PublicRead",
            "Effect": "Allow",
            "Principal": "*",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion"
            ],
            "Resource": "arn:aws:s3:::your-bucket-name/*"
        },
        {
            "Sid": "EC2ServiceRoleWrite",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::your-ec2-attached-iam-role"
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:ListBucket",
                "s3:ListBucketVersions",
                "s3:PutObject"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket-name",
                "arn:aws:s3:::your-bucket-name/*"
            ]
        }
    ]
}

バケットポリシーにはPrincipalにEC2インスタンスプロファイルのロールのみを指定している。
IAM ロールにおけるプリンシパルとは、「誰がこのロールを引き受けることができるか?」を指定するものだが、S3バケットポリシーにおけるプリンシパルとは、AWS公式ドキュメントのS3ユーザーガイドによれば以下のように説明されている:

リソースへのアクセスを許可または拒否するユーザー、アカウント、サービス、または他のエンティティを指定します。

また、同じくユーザーガイドは、インスタンスプロファイルは「IAM ロールを納めるコンテナであり、インスタンスの起動時に EC2 インスタンスにロール情報を渡す役割をしている」と説明している。

AWS権限周りの要件

  • S3バケットのバージョニングは有効
  • 「ACLは無効」かつ「バケット所有者の強制」を設定(AWSの推奨設定)
  • バケットポリシーは「匿名ユーザーへの読み取り専用アクセス許可」を付与
  • なぜなら、ブラウザからページにアクセスすると GET リクエストでS3バケットに置いてあるリソースファイルのURLを要求するから(後述するセクションで説明する今回のうっかりミスポイント)

asset_sync を導入する

以下導入の手順は AssetSync/asset_sync github リポジトリの README.md を参照する。

In your Gemfile:

gem "asset_sync"
gem "fog-aws"
$ bundle config set --local without 'test development'    # 既に設定してあればスキップ
$ bundle install
#config/environments/production.rb
config.action_controller.asset_host = "//#{Rails.application.credentials.aws[:fog_directory]}.s3.amazonaws.com"

ちなみにRubyでは二重引用符""の中で式展開が効き、一重引用符''の中では式展開が効かないので要注意。

$ RAILS_ENV=production bundle exec rails g asset_sync:install --provider=AWS
#=> create  config/initializers/asset_sync.rb

$ EDITOR=vim rails credentials:edit

#config/credentials.yml.enc
aws:
  access_key_id: 123
  secret_access_key: 345
  fog_directory: your-s3-bucket-name

In your config/initializers/asset_sync.rb:

if defined?(AssetSync)
  AssetSync.configure do |config|
    config.fog_provider = 'AWS'
    config.aws_access_key_id = Rails.application.credentials.aws[:access_key_id]
    config.aws_secret_access_key = Rails.application.credentials.aws[:secret_access_key]
    config.aws_session_token = ENV['AWS_SESSION_TOKEN'] if ENV.key?('AWS_SESSION_TOKEN')
    # Disable automatic run on precompile in order to attach to webpacker rake task
    config.run_on_precompile = false
    # Use IAM roles
    config.aws_iam_roles = true
    # Change canned ACL of uploaded object. Default is unset. Will override fog_public if set.
    # Choose from: private | public-read | public-read-write | aws-exec-read |
    #              authenticated-read | bucket-owner-read | bucket-owner-full-control 
    config.aws_acl = "bucket-owner-full-control"
    # Use http instead of https. Default should be "https" (at least for fog-aws)
    # config.fog_scheme = "http"
    config.fog_directory = Rails.application.credentials.aws[:fog_directory]
    # Increase upload performance by configuring your region
    config.fog_region = 'ap-northeast-1'
    # Set `public` option when uploading file depending on value,
    # Setting to "default" makes asset sync skip setting the option
    # Possible values: true, false, "default" (default: true)
    config.fog_public = true
  end
end

Create lib/tasks/asset_sync.rake:

if defined?(AssetSync)
  Rake::Task['webpacker:compile'].enhance do
    Rake::Task["assets:sync"].invoke
  end
end

ちなみに webpacker で静的アセット・CSS・JSファイルをバンドルさせているので、AssetSync/asset_sync の README.md 該当部分に倣って設定を追記している。
また、config/webpacker.yml で以下のように webpack でバンドルさせているファイルのコンパイル後の出力先を public/assets/packs 以下に変更しているので、README.md からは一部記述を省略している(『asset_syncの設定を見直してデプロイ時間を7分半削減した話』を参照)

#config/webpacker.yml
public_output_path: assets/packs
# デフォルトの出力先は public/packs 配下になる

プリコンパイル後にS3バケットに期待するファイルパスでアセットがアップロードされていればOK。

$ bundle exec rails assets:precompile assets:clean RAILS_ENV=production

トラブルシューティング

S3の権限に地獄を見せられたので以下実際に遭遇したエラーとその対処法を記述する(初歩的なミスを許すな)

プリコンパイル実行時に AccessControlListNotSupported エラーコードを返す

オブジェクト所有権のバケット所有者強制設定を適用すると、ACL は無効になります。ACL の設定または ACL の更新の要求は 400 エラーで失敗し、AccessControlListNotSupported エラーコードを返します。ACL の読み取り要求は引き続きサポートされています。ACL の読み取りリクエストは、バケット所有者の完全制御を示すレスポンスを常に返します。PUT オペレーションでは、バケット所有者の完全制御 ACL を指定するか、ACL を指定しない必要があります。そうしないと、失敗します。
トラブルシューティング - Amazon Simple Storage Service | docs.aws.amazon.com

重要:バケットおよびオブジェクト ACL によるクロスアカウントアクセスを付与することは、S3 オブジェクトの所有権が [Bucket Owner Enforced] に設定されているバケットでは機能しません。ほとんどの場合、ACL はオブジェクトやバケットにアクセス権限を付与するために必要がありません。代わりに、AWS Identity Access and Management (IAM) 施策と S3 バケット施策を使用して、オブジェクトとバケットにアクセス許可を付与します。
bucket-owner-full-control ACL を Amazon S3 のオブジェクトに追加する | aws.amazon.com

要するに、以下というわけである:
* asset_sync がS3にプリコンパイル済みアセットファイルをアップロードする = S3 PUT リクエストを行う
* 「オブジェクト所有権のバケット所有者強制設定」を適用しているので、バケット所有者の完全制御 ACL を指定するか、ACL を指定しない必要がある
* ∴「 IAM ポリシーと S3 バケットポリシーの双方を使用して、オブジェクトとバケットにアクセス許可を付与する」必要がある

実際に行なったこと

今回は、バケット所有者の完全制御 ACL を指定した。

#config/initializers/asset_sync.rb
# Change canned ACL of uploaded object. Default is unset. Will override fog_public if set.
# Choose from: private | public-read | public-read-write | aws-exec-read |
#              authenticated-read | bucket-owner-read | bucket-owner-full-control 
config.aws_acl = "bucket-owner-full-control"

ちなみに、ACLを指定しないを選んだ場合、推測に留まるが(別の機会に検証する積もり)、以下のような設定でいけるのではないだろうか?
* デフォルトの config.aws_acl = nil のままにする
* IAM Policy でs3:PutObject を許可 & S3 Bucket Policy で 当該ロールのs3:PutObjectを許可する

ブラウザからアプリケーションのアドレスにアクセスすると Status Code: 403 Forbidden が返される

image
要はS3から配信しているアセットを取得できていない。

デフォルトでは、すべての Amazon S3 バケットとオブジェクトはプライベートです。リソース所有者およびバケットを作成した AWS アカウントのみが、バケットにアクセスできます。ただし、リソース所有者は、他のリソースとユーザーにアクセス許可を付与することを選択できます。これを行う 1 つの方法として、アクセスポリシーを記述します。
Amazon S3 バケットのアクセス許可 - AWS Config | docs.aws.amazon.com

「忘れるな、S3バケットとオブジェクトへのアクセスはデフォルトで全て拒否」
ブラウザからページにアクセスすると GET リクエストでS3バケットに置いてあるリソースファイルのURLを要求するので、S3バケットポリシーに「匿名ユーザーへの読み取り専用アクセス」許可を定義しなければいけなかったのは当たり前体操だった……

ツイッターでシェア
みんなに共有、忘れないようにメモ

光の勢力

Crieitは誰でも投稿できるサービスです。 是非記事の投稿をお願いします。どんな軽い内容でも投稿できます。

また、「こんな記事が読みたいけど見つからない!」という方は是非記事投稿リクエストボードへ!

有料記事を販売できるようになりました!

こじんまりと作業ログやメモ、進捗を書き残しておきたい方はボード機能をご利用ください。
ボードとは?

コメント