Active storageを使ってS3(AWS)へのアップロードとHerokuデプロイ

この記事はプログラミング学習者がActive storage、S3(AWS)を使って本番環境(Heroku)にデプロイするまでに躓いたところを備忘録も兼ねてまとめたものです。
内容に間違いがありましたら、ご指摘頂けると助かります。

動作環境
Ruby: 3.2.1
Rails: 7.0.0
Postgresql: 14
Docker
PC: M2 Mac OS Sonoma 14.4.1

# 1, Active storageの導入
Active storageとはRails5.2から導入されたファイルアップロード機能です。
Active Storageを用いることで画像、PDF、動画をローカル環境だけでなくAmazon S3、Microsoft Azure Storage、Google Cloud Storageからなるクラウドストレージにもファイルをアップロードすることができます。
※今回はローカルとAmazon S3へのアップロードを目的として設定しました。

Active Storageを使用するために事前のインストール作業を進めていきます。
```
$ bin/rails active_storage:install
$ bin/rails db:migrate
```
※Docker内の為、docker compose exec web(アプリ名)を頭に付けて実行しました。
上記のコマンドを実行することで下記の３テーブルが作成されます。
・active_storage_blobs
アップロードされたファイルに関するメタ情報(データに関するデータ-作成日、更新日等)を保存する
・active_storage_attachements
主となるモデルとactive_storage_blobsテーブルの中間に入るテーブル
・active_storage_variant_records
生成されたバリアント(元の画像から派生・生成される画像-リサイズ等)に関するレコードを保存する

Active storageのサービスは`config/storage.yml`で宣言されています。
使用するサービスごとに名前や保管に関する詳細を設定します。
以下のコードは私が実際に使用したコードです。
```ruby:config/storage.yml
test:
  service: Disk
  root: <%= Rails.root.join("tmp/storage") %>

local:
  service: Disk
  root: <%= Rails.root.join("storage") %>

# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
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: ap-northeast-1
  bucket: my-rails-bucket-for-ec-site
```
`test`、`local`、`amazon`の３サービスでの保管場所などが記載されています。
使用するサービスは環境ごとに異なる可能性もある為、環境ごとに設定していきます。

今回は開発環境と本番環境で使用するサービスの設定を解説します。

開発環境の場合
```ruby:config/envoironments/development.rb
  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local
```
この設定により開発環境では`local`サービス下にデータを保管する設定となります。
`config/storage.yml`の内容を見ていただくとローカル環境(Railsアプリが配置されたディレクトリ配下のstorageディレクトリ)にファイルを保存する設定になっています。
※appディレクトリと同じ階層にあるstorageディレクトリです。

次に本番環境の場合
```ruby:config/environments/production.rb
# Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :amazon
```
この設定により本番環境では`amazon`サービス下にデータを保管する設定となります。
`config/storage.yml`にはAmazonのS3サービスに保管する設定がされています。
※`config/storay.yml`の`amazon`に関する設定は初期設定ではなく、既に本番環境用に変更した内容です。

`config/storage.yml`の`amazon`部分については初期状態が下記のようになっています。
```ruby:config/storage.yml
# Use bin/rails credentials:edit to set the AWS secrets (as # aws:access_key_id|secret_access_key)
# 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: us-east-1
#  bucket: your_own_bucket
```
上記のように最初はコメントアウトされていますので、使用する際はコメントアウトを解除してから必要な所を変更します。
初期設定でコメントアウトされているのはMicrosoft Azure StorageとGoogle Cloud Storageも同じです。

`amazon`サービスを使用する場合、gemのインストールが必要となります。
```ruby:Gemfile
gem 'aws-sdk-s3', require: false
```
`bundle install`でインストールします。
`require: false`は自動読み込みされないようにするためのオプションです。
アプリ起動時に読み込む必要がないので、`require: false`の設定をすることで起動時のメモリ消費を抑えてアプリの起動速度を改善することができます。

# ２、Railsのファイル内でコードを編集
Active storageの導入が終わったら、Rails内のコードをそれに対応した内容へ変更・追加します。
先ず、Active storageと結び付けたモデルのファイルに必要なコードを追加します。
```ruby:app/models/merchandise.rb
class Merchandise < ApplicationRecord
  has_one_attached :image
end
```
今回は一つのレコード(DBの)に一つの画像を添付する設定です。
サンプルコードの場合、`Merchandise`が対象のモデル名となります。
もし、一つのレコードに複数の画像を添付したい場合は書き方が以下のようになります。
```ruby:app/models/merchandise.rb
class Merchandise < ApplicationRecord
  has_many_attached :images
end
```

次にデータを表示する`view`ファイルの設定です。
```ruby:app/views/merchandises/index.html.erb
<% @merchandises.each do |merchandise| %>
    <% if merchandise.image.attached? %>
        <%= image_tag merchandise.image %>
    <% end %>
<% end %>
```
`@merchandises`にはコントローラーで`Merchandise`モデルのDBデータを全て入れるように設定しています(`@Merchandise.all`)。
Each分でDBのレコード一つ一つに対してActive storageで画像が添付されているかどうかを`attached?`で判定しています。
画像が添付されていれば、`image_tag`に`merchandise.image`を渡すことで表示できます。
`merchandise.image`はレコードに画像を添付しているかどうかに関わらず存在するオブジェクトですが、`image_tag`を使う場合は実際に画像が添付されていないとエラーが発生しますので、`merchandise.image.attached?`で判定して実際に画像が添付されている時だけ`image_tag`で画像を表示するようにしています。


# 3, AWSでS3を使えるように登録
AWS内のS3サービスを使って画像をアップロードできる場所を作っていきます。
※ここでは既にAWSのアカウントを持っている前提で話を進めていきます。
アカウントを持っていない方は先に新規登録をお願いします。

- AWSにログイン → サービス(左上) → S3 を起動
![AWSスタート画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/18c4a90b-b7e7-1feb-d68c-bbc4144dcbb8.png)
![AWSでS3を選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/a7f576f3-2535-ca02-1995-2a47679293c7.png)

- バケットを作成する(右端)を選択
※バケットは画像などのファイルをアップロードできる場所
![S3を選択した画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/6a95656b-a9a8-6d1a-2ab5-7b74750f97d3.png)

- バケット名を決めて、ブロックパブリックアクセスの設定をOFFにする。
他の設定はデフォルトで問題無いです。
![バケット名の入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/6741cb94-efef-3d18-6223-47c549a55bb3.png)
![パブリックアクセスブロックを外す.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/3b17c893-08f2-c508-656a-486662593347.png)

- バケットポリシーで権限や公開設定のルールを設定します。
![バケット名をクリック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/cfadae69-fb95-5d38-d55d-06afb2713d35.png)
![アクセス許可をクリック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/491958af-9b80-cc97-6258-efdb8ec63e96.png)
![バケットポリシー編集ボタン.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/23117cfe-6b2c-d193-2f90-4f0aedcc830d.png)
![バケットポリシーの保存.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/37cd41ee-4a01-29e1-d008-f89d0b657b66.png)
```:bucket policy
{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "AllowPublicReader",
			"Effect": "Allow",
			"Principal": "*",
			"Action": "s3:GetObject",
			"Resource": "arn:aws:s3:::my-rails-bucket-for-ec-site/*"
		}
	]
}
```
|項目名|概要|
|:-:|:-:|
| Version|ポリシーの文法のバージョン|
|Statement|一つの枠組み。枠組みの開始を宣言するもの|
|Sid|任意で与える識別子。他のStatementと重複しなければ指定は無い|
|Effect|Statementの内容を許可(Allow) or 拒否(Deny)する|
|Principal|誰が使うかを指定。サンプルコードでは*(全てのユーザー)|
|Action|AWSサービスのうちで指定したものについて、何の操作を許可 or 拒否する。サンプルではS3サービスのリソース取得操作が対象|
|Resource|Statementに記載した内容をどのリソースに適用するか。サンプルでは`my-rails-bucket-for-ec-site`というバケットが対象|

- バケットの作成が終わった時点で`config/storage.yml`を変更する
```ruby:config/storage.yml
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
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: ap-northeast-1
  bucket: my-rails-bucket-for-ec-site
```
上記の内容は`region`と`bucket`を作成したバケットの詳細情報に従って変更しています。

- AmazonでIAMユーザーを作成後、S3でアクセスキーを作成する
IAM(Identified and Access Management：アイアム)ユーザーとはAWSのサービスで認証と認可の設定を行うことができるサービスです。
![IAMユーザーを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/040b162b-873e-8c1f-1c20-fadd640451ff.png)
![ユーザー詳細.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/f4762dda-f274-25e8-2be6-038e23f71b0d.png)
![ユーザー作成ボタンを押す.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/f9806f9a-99b4-57b1-cd0a-9d96985dda38.png)
![ユーザー名の入力.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/d88856a4-204e-a948-c52b-43feee46f843.png)
許可のオプションはポリシーを直接アタッチするを選択
許可ポリシーとして`AmazonS3FullAccess`を選択
![S3fullaccessを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/a7a280c7-ed39-8d30-64bf-c8d3934f18a0.png)
![ユーザーの作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/afc77248-8188-ca9e-948e-9dcbd3bdbc45.png)

- 次にアクセスキーを作成します。
- ![作成したユーザーを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/9b016d40-b781-4011-82c3-498efd302eef.png)
![アクセスキー作成を選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/9d895167-3209-da99-0f6f-736b53e4ed31.png)
![ローカルコードを選択.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/a877b67e-7373-f0a7-64c0-085ad2143cee.png)
![アクセスキーを作成完了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/378e1e4f-8359-fe18-8b98-a7d23e899751.png)
必ずシークレットアクセスキーをメモなどで保管しましょう。
csvファイルとしてダウンロードすることもできます。
![アクセスキーの表示.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3508918/7c85079d-de8f-55dc-c6c3-e5658e61ba14.png)

# 4, Rails側で認証設定を行う
先ほど作成したアクセスキーを使って認証情報を追加していきます。
- バケットの作成が終わった時点で`config/storage.yml`を変更する
```ruby:config/storage.yml
# Use bin/rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key)
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: ap-northeast-1
  bucket: my-rails-bucket-for-ec-site
```
`config/storage.yml`でaccess_key_idとsecret_access_keyの部分はここに直接入力するとセキュリティ上第三者に漏洩する可能性がある為、Credentialsを使って入力します。
外部APIのキー情報やアクセストークンなどの機密情報を外部に漏らしてしまうと悪用される可能性があります。RailsにはCredentialsという機密情報を守る機構がありますので、重要な情報は暗号化して保護することができます。
`<%= Rails.application.credentials.dig...`の部分はCredentialsに入力されたデータを読み込んで処理するようになっていますので変更せずにそのままいきます。
ちなみにCredentialsの内容は通常`config/credentials.yml.enc`の中に保存されていますが、直接見ても暗号化されているので中身を正しく見ることはできません。

- CredentialsのデータをAWSで得たアクセスキーに書き換える。
コンソールで以下のコードを入力することでCredentialsの内容を書き換えできます。
```
$ EDITOR="vim" bin/rails credentials:edit
```
上記のコードではvimを使ってCredentiasを編集するという内容を意味しています。
vimの部分は他のエディターでも対応可です。
VisualStudioCode = code
Atom = atom
vi = vi
VisualStudioCodeとAtomの場合、入力内容が若干変わります。
```
EDITOR="code --wait" bin/rails credentials:edit
EDITOR="atom --wait" bin/rails credentials:edit
```

私の場合、Dockerコンテナ内の起動しているweb(サービス)に対して同じ処理をしていましたので、docker用のコマンドを交えて実行しました。
```
docker compose exec -e EDITOR="vim" web bin/rails credentilas:edit
```
`-e EDITOR="vim"`で`-e`をつけるのは環境変数の指定なのでオプションとして必要になるからです。
ちなみに最初に上記コマンドを実行した時は以下のエラーが発生してCredentialsの編集ができませんでした。
```
$ docker compose exec -e EDITOR="vim" web bin/rails credentilas:edit
Adding config/master.key to store the encryption key:xxxxxxxxxxxxxx

Save this in a password manageer your team can access.

If you lose the key, no one, including you, can access anything encrypted with it.

    create config/master.key

Couldn't decrypt config/credentials.yml.enc. Perhaps you passed the wrong key ?

```
内容としては
暗号化キー xxxxxxxxxxxxxxx を保管する為に`config/master.key`を追加しました。
チーム全員がアクセスできる場所にこのkeyを保管してください。
このkeyを無くしたらあなたも含めて誰もこのkeyで暗号化されたデータにアクセスできません。
`config/master.key`を作成しました。
`config/credentials.yml.enc`を復号出来ませんでした。
あなたが間違ったkeyを通した可能性があります。

これは`config/master.key`が無い状態で`bin/rails credentials:edit`コマンドを実行した時に出るメッセージで`config/master.key`が生成される時に発生します。

また、２回目に同じコマンドを実行したら次は以下のエラー文が出ました。
```
$ docker compose exec -e EDITOR="vim" web bin/rails credentilas:edit

ActiveSupport::MessageEncryptor::InvalidMessage(以下省略)
```
内容としては先のコマンドで作成された`config/master.key`が既存の`config/credentials.yml.erc`に対して使えない為、エラーが出ているようです。
ここでは`config/credentials.yml.erc`を一度別名に変えてから(存在しないように仕向けてから)もう一度同じコマンドを実行しました。

３回目はdockerコンテナ内部にvimがインストールされていなかったので、編集することができませんでした。
コマンドを実行しても`File encryped and saved.`とだけ出力されてきました。
`docker compose exec web apt-get install vim`をした後にもう一度トライすると編集することができました。
```
$ docker compose exec -e EDITOR="vim" web bin/rails credentials:edit
File encrypted and saved.
以下はvimコマンドにより表示された内容です。
# aws:
#   access_key_id: 123
#   secret_access_key: 345
# Used as the base secret for all MessageVerfiers in Rails, including the one protecting cookies.
secret_key_base:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
ここで`aws`, `access_key_id`, `secret_access_key`の三行について行頭の`#`を外してコメントアウトを解除します。
`access_key_id`, `secret_access_key`についてはAWSで取得したアクセスキーをそれぞれ入力します。
入力した内容は下記のコマンドで見ることもできます。
```
$ docker compose exec -e EDITOR="vim" web bin/rails credentials:show
```
credntialsを編集する時には`edit`だった所を`show`に変更したコマンドです。
これで保存先をAmazon S3に変更する一連の作業が完了しました。

# 4, Herokuへアップロード
最終段階としてHerokuへ設定した内容をPushして反映させます。
```
$ heroku buildpacks:add -i 1 https://github.com/heroku/heroku-buildpack-activestorage-preview
$ git add .
$ git commit -m "任意(S3サービスを本番環境に加えた内容)"
$ git push heroku index_and_detail(私の場合):main #ブランチを切っていました。
```

Credentialsの内容をHeroku側で復号できるように`config/master.key`の内容を環境変数として用意する必要があります。
`config/master.key`はRailsのファイル内に含まれていますが、.gitignoreの対象になっており、GithubやHerokuのようなGitリポジトリをそのままデプロイする本番環境の場合は別途用意しておく必要があります。
```
$ heroku config:set RAILS_MASTER_KEY=`config/master.key`の内容を貼り付け
$ heroku run rails db:migrate
$ heroku open
```
私の場合、この時点では作成しているアプリを通して画像データをアップロードする機構をまだ組み込んでいませんでしたので、seedを使って本番環境に画像データをアップロードしました。

```ruby:db/seed.rb
merchandise6 = Merchandise.create!(
  name: 'サワラ',
  amount: 1500,
  description: '800g, 40cm',
  stock: 1,
  how_to_cook: '刺身(炙り)'
)
merchandise6.image.attach(io: File.open(Rails.root.join('app/assets/images/spanish_mackerel.JPG')),
                          filename: 'spanish_mackerel.JPG')
```
下記のコメントで作成している`db/seed.rb`のデータをHerokuへ取り込みます。
```
$ heroku run rake db:seed
```
結果、取り込んだ画像データをHeroku側へ表示させることができました！

参考リンク
https://railsguides.jp/active_storage_overview.html
https://zenn.dev/farstep/books/7f169cdc597ada/viewer/9b2cc7
https://qiita.com/takehanKosuke/items/79a66751fe95010ea5ee
https://toshpit.com/rails-activestorage/
https://zenn.dev/redheadchloe/articles/e924ab767b40d5
https://qiita.com/kawa3401/items/c982ccebacf2faeae48d
https://qiita.com/hmmrjn/items/7cc5e5348755c517458a
https://qiita.com/hmmrjn/items/479c9e9ce82771f1b6d7
https://qiita.com/zenfumi/items/4a7cbab59f0f7ede0d6e
https://qiita.com/show1107/items/5dbdac581d561909cf84