はじめに
ActionTextから登録された画像や添付ファイルはデフォルトだとサーバー内のローカルディスクに保存されます。
大容量のファイルが溜まり続けた場合、サーバがいつかキャパオーバーになってしまう懸念があります。
そこで、ActionTextから投稿された画像の保存先を、ローカルディスクからAmazon S3
に変更しストレージ容量を確保し、高速コンテンツ配信サービス(CDN)であるAmazon CloudFront
を使用して、配信を行う構成を実装してみたいと思います。
【環境】
・ AWS(EC2, S3, CloudFront, IAM etc.)
・ Ruby 2.7.3
・ Ruby on Rails 6.1.5
ActionTextを使用するには、Railsバージョン6系以上が必要です。
インフラ構成
ざっくり、全体のインフラ構成は↓のような感じで実現しました。
- アプリケーションは
EC2インスタンス(web01, web02)
内にデプロイし、可用性を高めるためマルチAZ構成 -
ActionText
からアップロードされた画像は、VPCエンドポイント
を経由して、S3バケットにアップロード - ユーザーは画像にアクセスする際に、
CloudFront
のエッジロケーション
にアクセスする -
CloudFront
はキャッシュとして画像を保持し、存在すれば、そのままユーザーにレスポンスを返し、なければS3に対して、取得リクエストを送る
■ アプリケーション側(Rails)の設定
Gemfile
#適用したい環境で囲んであげる
group :staging, :production do
gem 'aws-sdk-s3', require: false
end
environments/環境.rb
環境ファイルに保存先の情報を追記する
development.rb
, staging.rb
, production.rb
など、各々、実現したい環境のファイルを変更する
#activestorageファイル保存
config.active_storage.service = :amazon
#複数環境で実現したい場合は、↓のようにキーの指定の仕方を一意になるようにしてあげれば良い
config.active_storage.service = :amazon_production
storage.yml
ここに、ActionTextの画像格納先情報が記載されており、デフォルトだとローカルディスク内に保存するようになっている(コメントアウト箇所)
それではいつかサーバーがキャパオーバーになりかねないので、保存先をS3に変更する
また、ローカル環境や、EC2にIAMロール or 認証情報
を設定しない場合は、storage.yml
内にアクセスキーを指定する
(今回はEC2自体にIAMロールをアタッチするので、コメントアウト)
#local:
#service: Disk
#root: <%= Rails.root.join("storage") %>
amazon:
service: S3
#access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
#secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: リージョン名
bucket: バケット名
#複数環境で実現したい場合
amazon_production:
service: S3
region: リージョン名
bucket: バケット名
■ インフラ側の設定
▼準備
EC2にS3操作権限のあるIAMロールをアタッチ
- 適当な名前の
IAMロール
を作成 - IAMロールには
S3FullAccess
もしくは、書き込み権限のあるポリシーを付与 - 作成した
IAMロール
をアプリケーションをデプロイしたEC2にアタッチ
EC2には認証情報ではなく、IAMロールを割り当てた方が良い
AWSのベストプラクティス的にもEC2内に直接、認証情報を記述するのは良くないとされているので、権限は基本的に最小権限のIAMロールで付与します。
aws credentials
の認証情報とIAMロールで同じポリシーを設定している場合、認証情報の方が優先度が高いので、aws cli
からの操作の時に予期せぬ挙動となる可能性がある。
▼S3の設定
バケットの作成
ActionTextの画像ファイル格納用のバケットを作成。
バケット名は全世界で一意である必要があります。
バージョニングは各々の要件に合わせて有効/無効
を設定してください
Cross-Origin Resource Sharing (CORS)の記述
新コンソールからはjsonでの記述が必須となったので、以下の内容をバケット内のCORSに記述。
[
{
"AllowedHeaders": [
"*"
],
"AllowedMethods": [
"PUT",
"POST",
"DELETE"
],
"AllowedOrigins": [
"*"
],
"ExposeHeaders": []
}
]
VPCエンドポイントを使用してS3にアクセスする
バケットを作成後、EC2→S3への通信を実現するために、以下記事を参考にVPCエンドポイントを構築。
VPCエンドポイントを使用することで、EC2→S3間の通信をインターネットにでることなく、セキュアに通信することができる。
このとき、VPCエンドポイントのポリシーを以下のように設定。
EC2インスタンスがVPCエンドポイント経由でS3バケットに対して行いたいポリシーを設定する。
{
"Version": "2012-10-17",
"Id": "Policy1651800381415",
"Statement": [
{
"Sid": "Stmt1651800377576",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:List*"
],
"Resource": [
"arn:aws:s3:::バケット名",
"arn:aws:s3:::バケット名/*"
]
}
]
}
VPCエンドポイント経由で通信できているか確認
VPCにエンドポイントを作成して、S3にアクセスする設定にしたものの、本当にエンドポイント経由でアクセスされているか確認したかったので、検証してみました。
確認する方法は、以下の2種類がありますが、今回は、CloudTrailのデータイベントの記録
を参照し確認します。
- S3のサーバーアクセスログ
- CloudTrailのデータイベントの記録
なお、事前にCloudTrailのデータイベントログの取得を有効化しておく必要があります。
EC2内からaws cli
でListイベントを呼び出します。
(VPCエンドポイントポリシーにs3:List*
のアクションが必要です)
//バケット内のオブジェクトの一覧を表示
$ aws s3 ls バケット名
=> 2022-05-09 16:55:43 590866 test.png
ちょっと待つとS3内のCloudTrailのイベントが記録されたバケットにイベントログが入ります。
ListObjects
が呼び出されたログを確認・・・
sourceIPAddress": "EC2のプライベートIPアドレス"
となっていれば、EC2からVPCエンドポイントを使用して、S3にアクセスできています!
(※EC2のプライベートIPアドレス
ではなく、自分が接続しているネットワークのIPアドレスならNATゲートウェイ経由でインターネットを通過したS3へのアクセスになってしまっているかと思います)
{
"eventTime": "2022-05-10T02:00:16Z",
"eventSource": "s3.amazonaws.com",
"eventName": "ListObjects",
"awsRegion": "ap-northeast-1",
"sourceIPAddress": "EC2のプライベートIPアドレス",
"userAgent": "[aws-cli/2.4.7 Python/3.8.8 Linux/5.10.102-99.473.amzn2.x86_64 exe/x86_64.amzn.2 prompt/off command/s3.ls]",
"requestParameters": {
"list-type": "2",
"bucketName": "バケット名",
"encoding-type": "url",
"prefix": "",
"delimiter": "/",
"Host": "バケット名.s3.ap-northeast-1.amazonaws.com"
}
}
(※注意)VPCエンドポイントの料金
▼CloudFrontの設定
ディストリビューションの作成
ディストリビューションの作成から、OAIの設定まで、↓記事を参考にすると良いと思います!
CloudFrontから配信されているか確認
CloudFront がディストリビューションのドメイン名としてd111111XXXXXXX.cloudfront.net
を返した場合は、
S3バケット内またはHTTPサーバーのルートディレクトリに存在する foo.jpg
のURLはhttp://d111111XXXXXXX.cloudfront.net/foo.jpg
となります。
え、、、? どゆこと、、、?
つまり、バケット内にhoge.jpeg
というオブジェクトがCloudFrontから配信されているか確認したい場合、
ブラウザのURLでhttp://d111111XXXXXXX.cloudfront.net/hoge.jpg
を叩いて、アクセスできればCloudFrontから配信できています!
https://"CloudfrontのDomainName"/"オブジェクト名"
オリジンアクセスアイデンティティ(OAI)とは
S3バケットから直接ではなく、CloudFront経由でのみファイルにアクセスできるようにするための特別なユーザー
これを画像を格納するバケットのバケットポリシーとして設定してあげるとCloudFront経由でのみに制限できる。
(ディストリビューションの作成画面からOAIの作成、バケットポリシーの自動変更ができます)
CloudFrontアクセスログをS3バケットに格納
S3バケットはデフォルトだとACL無効化により、バケットへの書き込みが制限されている。
ACLを無効に設定したS3バケットは選択できないようになっているため、ログを取得するにはバケットのACLの設定を変更する必要があります。
バケットに格納されたCloudFrontのログ確認方法は以下記事がすごく参考になりました。
さいごに
S3×CloudFrontという王道のアーキテクチャ構成実装をしてみましたが、ActionTextと併用されていることは記事としてもなかなか少なかったので、すごく良い経験になりました。
記載はしませんでしたが、この後他にも、S3ライフサイクルポリシーの設定や、S3へのアクセスログの取得なども行いました。
まだまだやり残したこととして↓のようなこともあるので、今後はこれらも挑戦していきたいです!
- Rails側でActionTextの元のイベントを削除したときに、S3にアップロードされた画像どうするか問題
- CloudFrontのキャッシュヒット率の向上