S3×CloudFront を使用したActionTextの画像アップロード・配信方法

# はじめに

ActionTextから登録された画像や添付ファイルはデフォルトだとサーバー内のローカルディスクに保存されます。

大容量のファイルが溜まり続けた場合、サーバがいつかキャパオーバーになってしまう懸念があります。

そこで、ActionTextから投稿された画像の保存先を、ローカルディスクから`Amazon S3`に変更しストレージ容量を確保し、高速コンテンツ配信サービス（CDN)である`Amazon CloudFront`を使用して、配信を行う構成を実装してみたいと思います。

---

【環境】
・ AWS（EC2, S3, CloudFront, IAM etc.)
・ Ruby 2.7.3
・ Ruby on Rails 6.1.5

:::note warn
**ActionText**を使用するには、Railsバージョン６系以上が必要です。
:::

[Action Text の概要](https://railsguides.jp/action_text_overview.html)

![スクリーンショット 2022-05-11 6.38.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/817d8ea9-ce26-fe03-18f8-8ae8d207db4a.png)


# インフラ構成
ざっくり、全体のインフラ構成は↓のような感じで実現しました。

- アプリケーションは`EC2インスタンス(web01, web02)`内にデプロイし、可用性を高めるためマルチAZ構成
- `ActionText`からアップロードされた画像は、`VPCエンドポイント`を経由して、S3バケットにアップロード
- ユーザーは画像にアクセスする際に、`CloudFront`の`エッジロケーション`にアクセスする
- `CloudFront`はキャッシュとして画像を保持し、存在すれば、そのままユーザーにレスポンスを返し、なければS3に対して、取得リクエストを送る

![スクリーンショット 2022-05-09 11.21.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/b106d76a-abee-6c27-7cd5-b1d5bcdfc163.png)



# ■ アプリケーション側（Rails）の設定
### `Gemfile`
```ruby:Gemfile
#適用したい環境で囲んであげる
group :staging, :production do
  gem 'aws-sdk-s3', require: false
end
```
### `environments/環境.rb`
環境ファイルに保存先の情報を追記する

`development.rb`, `staging.rb`, `production.rb`など、各々、実現したい環境のファイルを変更する

```ruby:config/environments/環境.rb
#activestorageファイル保存
config.active_storage.service = :amazon

#複数環境で実現したい場合は、↓のようにキーの指定の仕方を一意になるようにしてあげれば良い
config.active_storage.service = :amazon_production
```

### `storage.yml`
ここに、ActionTextの画像格納先情報が記載されており、デフォルトだとローカルディスク内に保存するようになっている（コメントアウト箇所）

それではいつかサーバーがキャパオーバーになりかねないので、保存先をS3に変更する

また、ローカル環境や、EC2に`IAMロール or 認証情報`を設定しない場合は、`storage.yml`内にアクセスキーを指定する
（今回はEC2自体にIAMロールをアタッチするので、コメントアウト）

```yml:storage.yml
#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にアタッチ

![スクリーンショット 2022-05-09 14.25.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/669969/a12692f9-d4c1-8c2d-e456-a6c513bd4c93.png)

### EC2には認証情報ではなく、IAMロールを割り当てた方が良い
AWSのベストプラクティス的にもEC2内に直接、認証情報を記述するのは良くないとされているので、権限は基本的に最小権限のIAMロールで付与します。

`aws credentials`の認証情報とIAMロールで同じポリシーを設定している場合、認証情報の方が優先度が高いので、`aws cli`からの操作の時に予期せぬ挙動となる可能性がある。

https://dev.classmethod.jp/articles/tsnote-ec2-iamrole-instanceprofile-001/


## ▼S3の設定
### バケットの作成
ActionTextの画像ファイル格納用のバケットを作成。
バケット名は全世界で一意である必要があります。

バージョニングは各々の要件に合わせて`有効/無効`を設定してください

### Cross-Origin Resource Sharing (CORS)の記述
新コンソールからはjsonでの記述が必須となったので、以下の内容をバケット内のCORSに記述。
```json:CORS
[
    {
        "AllowedHeaders": [
            "*"
        ],
        "AllowedMethods": [
            "PUT",
            "POST",
            "DELETE"
        ],
        "AllowedOrigins": [
            "*"
        ],
        "ExposeHeaders": []
    }
]
```



### VPCエンドポイントを使用してS3にアクセスする
バケットを作成後、EC2→S3への通信を実現するために、以下記事を参考にVPCエンドポイントを構築。

VPCエンドポイントを使用することで、EC2→S3間の通信をインターネットにでることなく、セキュアに通信することができる。

https://agohack.com/file-transfer-ec2-to-s3-via-vpc-endpoints/#

このとき、VPCエンドポイントのポリシーを以下のように設定。

EC2インスタンスがVPCエンドポイント経由でS3バケットに対して行いたいポリシーを設定する。

```json:VPCエンドポイントポリシー
{
	"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にアクセスする設定にしたものの、本当にエンドポイント経由でアクセスされているか確認したかったので、検証してみました。

確認する方法は、以下の２種類がありますが、今回は、`CloudTrailのデータイベントの記録`を参照し確認します。

- S3のサーバーアクセスログ
- CloudTrailのデータイベントの記録

なお、事前に**CloudTrailのデータイベントログの取得を有効化**しておく必要があります。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/enable-cloudtrail-logging-for-s3.html

EC2内から`aws cli`でListイベントを呼び出します。
(VPCエンドポイントポリシーに`s3:List*`のアクションが必要です）
```sh:EC2
//バケット内のオブジェクトの一覧を表示
$ 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へのアクセスになってしまっているかと思います）

```json:cloudtrailデータイベントログ
{
  "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エンドポイントの料金
https://dev.classmethod.jp/articles/best-practice-when-sending-traffic-s3-dynamodb-in-the-same-region/

## ▼CloudFrontの設定
### ディストリビューションの作成
ディストリビューションの作成から、OAIの設定まで、↓記事を参考にすると良いと思います！

https://zenn.dev/ashisan/articles/571f2d1c0f2c36


### 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から配信できています！

```sh:URL
https://"CloudfrontのDomainName"/"オブジェクト名"
```

### オリジンアクセスアイデンティティ(OAI)とは
:::note info
S3バケットから直接ではなく、CloudFront経由でのみファイルにアクセスできるようにするための特別なユーザー
:::

これを画像を格納するバケットのバケットポリシーとして設定してあげるとCloudFront経由でのみに制限できる。
(ディストリビューションの作成画面からOAIの作成、バケットポリシーの自動変更ができます）



### CloudFrontアクセスログをS3バケットに格納
S3バケットはデフォルトだとACL無効化により、バケットへの書き込みが制限されている。

ACLを無効に設定したS3バケットは選択できないようになっているため、ログを取得するにはバケットのACLの設定を変更する必要があります。

https://dev.classmethod.jp/articles/cloudfront-access-log-dont-choose-no-acl-s3-bucket/


バケットに格納されたCloudFrontのログ確認方法は以下記事がすごく参考になりました。

https://dev.classmethod.jp/articles/cloudfront-access-log-survey-use-s3-select/


# さいごに
S3×CloudFrontという王道のアーキテクチャ構成実装をしてみましたが、ActionTextと併用されていることは記事としてもなかなか少なかったので、すごく良い経験になりました。

記載はしませんでしたが、この後他にも、S3ライフサイクルポリシーの設定や、S3へのアクセスログの取得なども行いました。

まだまだやり残したこととして↓のようなこともあるので、今後はこれらも挑戦していきたいです！

- Rails側でActionTextの元のイベントを削除したときに、S3にアップロードされた画像どうするか問題
- CloudFrontのキャッシュヒット率の向上


# 参考
- ActionText
  - [Action Text での Amazon S3 アップロード](https://www.bokukoko.info/entry/2020/02/24/204307)
  - [【更新：解決😆】【Rails】ActionTextでアップロードした画像の保存先がローカルから変更できない](https://qiita.com/whitia/items/56dfe162793cd38b341e)
- S3
  - [AWS S3へのアクセスがEndPoint経由で行われているか確認する](https://www.it-ouji.com/2021/06/23/aws-s3%E3%81%B8%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%8Cendpoint%E7%B5%8C%E7%94%B1%E3%81%A7%E8%A1%8C%E3%82%8F%E3%82%8C%E3%81%A6%E3%81%84%E3%82%8B%E3%81%8B%E7%A2%BA%E8%AA%8D%E3%81%99/)
  - [プライベートサブネットからAWSサービスエンドポイントのアクセス経路を調べてみた](https://dev.classmethod.jp/articles/access-priority-vpcendpoint-natgateway/)
- CloudFront
  - [いますぐ使う CloudFront](https://qiita.com/sasasin/items/0f0ec1a90af6295589f9)
  - [CloudFront 経由で S3 のファイルにアクセスする](https://qiita.com/NaokiIshimura/items/46994e67b712831c3016)