Herokuでデプロイしたアプリで投稿画像の保存先をS3に設定してみた。

##はじめに
[前回記事](https://qiita.com/President_Taka/items/aeb3570b42a6aeb5461f)でHerokuにデプロイしたアプリですが、投稿した画像が時間が経つと次のように表示されなくなります。
<img width="728" alt="スクリーンショット 2020-02-25 21.43.43.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438093/2bd8f622-f3b6-a64f-2f85-3aeff41e57f9.png">


##原因
git上で管理されている画像は表示されますが、ブラウザ上で投稿した画像については保存されないため、一定時間で表示されなくなってしまいます。

これは、実装した画像アップローダーは、開発環境で動かす分には問題はありませんが、本番環境には適していません。`app/uploaders/image_uploader.rb`の`storage :file`というコードによって、ローカルのファイルシステムに画像を保存するようになるためです。本番環境でも投稿した画像を保存するためには、ファイルシステムではなくクラウドストレージサービスに画像を保存する必要があります。

##対処法
今回AWS/S3のクラウドストレージサービスを利用します。なにぶん初心者のためエラーにぶつかる事が多く、スマートな処理ではないですがじぶんの備忘録として同じようなエラーに苦労される方の助けになればと思います。

※注意事項として、試行錯誤しながらやっと解決できた感じなので、決してスマートな解決方法ではないと思います。その点はご了承ください。そしてこうしたらスマートに解決できるよ！というご意見がございましたら是非コメントいただければ幸いです！

## 前提条件
・Railsアプリ作成済
・CarrierWaveとMiniMagickで画像投稿機能は実装済
・AWSアカウント登録済
・AmazonS3FullAccess権限を与えたIAMユーザーを作成済

##以下実装の流れ
###1.fogのインストール
本番環境でクラウドストレージに保存するために、fog gemを使います。

```Gemfile.
gem 'bootstrap', '~> 4.4.1'
gem 'jquery-rails'
gem 'devise', '~> 4.6.1'
gem 'carrierwave', '~> 1.0'
gem "mini_magick"
gem 'fog' 　　　　 <= 追加
```

```
$ bundle install path --vendor/bundle
```

###2.S3で保存先を準備
AWSでバケットの作成します。
<img width="1391" alt="スクリーンショット 2020-02-02 12.51.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438093/54897876-9cb4-1291-2b38-29281cf2108a.png">

オリジナルのバケット名を入力し、アクセス許可をパブリックに設定します。

<img width="1440" alt="スクリーンショット 2020-02-26 12.01.27.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438093/ae17658d-8016-5ba6-9509-c2403b2d83c7.png">


###3.設定
Herokuでの画像の保存先をAmazon S3に保存できるように設定します。
基本的な話ですが、アクセスキーIDとシークレットアクセスキーは直接入力厳禁です。これをするととんでもないことになります。ですので、必ず環境変数を設定します。私は身をもって[体験](https://qiita.com/President_Taka/items/12c05e64d1e4b50cd6ef)いたしました。

```config/initializers/carrierwave.rb

　〈中略〉　以下を追加
require 'carrierwave/storage/abstract'
require 'carrierwave/storage/file'
require 'carrierwave/storage/fog'

if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      :provider              => 'AWS',
      :region                => ENV['S3_REGION'],     
      :aws_access_key_id     => ENV['S3_ACCESS_KEY'],
      :aws_secret_access_key => ENV['S3_SECRET_KEY']
    }
    config.fog_directory     =  ENV['S3_BUCKET']
  end
end
```

```app/uploaders/image_uploader.rb
class ImageUploader < CarrierWave::Uploader::Base
  # Include RMagick or MiniMagick support:
  # include CarrierWave::RMagick
  include CarrierWave::MiniMagick

　# 以下を追加
 if Rails.env.production?
    storage :fog
  else
    storage :file
  end
```

###4.herokuへ環境変数登録
herokuに環境変数を登録（IAMでCSVダウンロードした中身通りに値を入力）

```
$ heroku config:set S3_ACCESS_KEY="Accessキーを入力"
Setting S3_ACCESS_KEY and restarting ⬢ [アプリ名]... done, v15
S3_ACCESS_KEY: Accessキー

$ heroku config:set S3_SECRET_KEY="Secretキーを入力"
Setting S3_SECRET_KEY and restarting ⬢ [アプリ名]... done, v16
S3_SECRET_KEY: Secretキー

$ heroku config:set S3_BUCKET="Bucket名を入力"
Setting S3_BUCKET and restarting ⬢ [アプリ名]... done, v17
S3_BUCKET: Bucket名

$ heroku config:set S3_REGION="Region名を入力"
Setting S3_REGION and restarting ⬢ [アプリ名]... done, v18
S3_REGION: Region名
```

以上でherokuへ再度pushして'heroku open'してみると……

<img width="581" alt="スクリーンショット 2020-02-02 14.43.49.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438093/a15131ae-5a2c-703a-8c12-9f0ef5ef75de.png">
上手くいっていないようです。ログを確認してみます。

```
$ heroku logs -t
2020-02-02T05:42:54.905758+00:00 app[web.1]: from /app/vendor/bundle/ruby/
2.6.0/gems/bootsnap-1.4.5/lib/bootsnap/load_path_cache/core_ext/kernel_require.
rb:22:in `block in require_with_bootsnap_lfi'
・
・
・
2020-02-02T05:43:15.180513+00:00 heroku[router]: at=error code=H10 
desc="App crashed" method=GET path="/" host=photo-ogiri.herokuapp.com 
request_id=ba2d6298-7c81-40a0-843d-02b9e2a700ef fwd="126.233.30.95" 
dyno= connect= service= status=503 bytes= protocol=https
```
アプリがクラッシュしたとの内容のみで修正が必要な箇所がわからない。

```
$ heroku run rails c
・
・
・
in `block in load_missing_constant': uninitialized constant CarrierWave::Storage::Fog (NameError)
```
見つけました。carrierwaveの部分でエラーが出ているようです。

###5.エラー対処
確認すると carrierwave.rb が config/initializers 配下になく、 vendor/bundle 配下にしかなかったため、 config/initializers 配下に新たに carrierwave.rb を作成し、再度、heroku pushすると別のエラーが発生しました。

```
$ git push heroku master
・
remote:        rake aborted!
remote:        NameError: uninitialized constant CarrierWave::Storage::Fog
・
・
remote:  !
remote:  !     Precompiling assets failed.
remote:  !
remote:  !     Push rejected, failed to compile Ruby app.
・
```

まずは開発環境でPrecompileするために、以下を実行し成功することを確認しました。

```
$ RAILS_ENV=development bundle exec rails assets:precompile
・
・
yarn install v1.21.1
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 0.11s.
```
続いて本番環境でPrecompileできるか試してみました。

```
$ RAILS_ENV=production bin/rails assets:precompile
rails aborted!
NameError: uninitialized constant CarrierWave::Uploader
```
carrierwaveの設定に修正が必要な様子。調べてみると[Railsガイド](https://railsguides.jp/5_2_release_notes.html#credential%E7%AE%A1%E7%90%86)ではRails5.2以降の変更点として次の記載がありましたので、carrierwave.rbを修正しました。
> config/credentials.yml.encファイルが追加され、productionアプリケーションの秘密情報（secret）をここに保存できるようになりました。これによって、外部サービスのあらゆる認証credentialを、config/master.keyファイルまたはRAILS_MASTER_KEY環境変数にあるキーで暗号化した形で直接リポジトリに保存できます。

```ruby:carrierwave.rb
require 'carrierwave/storage/fog'

  CarrierWave.configure do |config|
    if Rails.env.production?
    config.fog_provider = 'fog/aws'
    config.fog_credentials = {
      provider:              'AWS',
      aws_access_key_id:     Rails.application.credentials.aws[:access_key_id],
      aws_secret_access_key: Rails.application.credentials.aws[:secret_access_key],
      region:                'ap-northeast-1'
    }
    config.fog_directory  = 'S3バケット名'
    config.fog_public     = true
    config.fog_attributes = { cache_control: "public, max-age=#{365.days.to_i}" }
    end
  end
```
再度、本番環境でPrecompileできるか試してみました。

```
$ RAILS_ENV=production bundle exec rails assets:precompile
rails aborted!
ArgumentError: Missing `secret_key_base` for 'production' environment, set this string with `rails credentials:edit`
```
失敗しましたがエラーは変わりました。secret_key_baseあたりを確認します。

```
$ EDITOR=vim rails credentials:edit
aws:
   access_key_id: ~~~~~~~~~~~~~~~~~~~~~~~
   secret_access_key: ~~~~~~~~~~~~~~~~~~~
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
   secret_key_base: ~~~~~~~~~~~~~~~~~~~~~~~~~
```
値もきちんと入っていますが、rails cで1つずつキーを確認するとsecret_key_baseのみがnillでした。

```
$ bundle exec rails c
>Rails.application.credentials.secret_key_base
=> nil
>Rails.application.credentials.aws[:access_key_id]
=> ~~~~~~~~~~~~~~~~~
>Rails.application.credentials.aws[:secret_access_key]
=> ~~~~~~~~~~~~~~~~~
```
ここでかなり苦戦しましたが、結果インデントの位置がおかしかったため生じていたエラーでした。修正後のcredentials.yml.encファイルがこちら。secret_key_base:のインデントを左に寄せただけです。恐らく修正前のコードだとaws:配下のコードとみなされてしまって、secret_key_base:の値が読み取られなかったことが原因です。

```
$ EDITOR=vim rails credentials:edit
aws:
   access_key_id: ~~~~~~~~~~~~~~~~~~~~~~~
   secret_access_key: ~~~~~~~~~~~~~~~~~~~
# Used as the base secret for all MessageVerifiers in Rails, including the one protecting cookies.
secret_key_base: ~~~~~~~~~~~~~~~~~~~~~~~~~
```
無事プリコンパイル処理もとおりました。

```
$ RAILS_ENV=production bundle exec rails assets:precompile
yarn install v1.21.1
[1/4] 🔍  Resolving packages...
success Nothing to install.
success Saved lockfile.
✨  Done in 0.15s.
```

あとはherokuにmaster.keyをセットして完了です。master.key は git 管理していないので、heroku にはデプロイされていません。そのため、最初この状態でheroku pushすると`NoMethodError: undefined method '[]' for nil:NilClass`と怒られました。

```
$ heroku config:set RAILS_MASTER_KEY=`cat config/master.key`
$ git push heroku master
```
無事heroku上のアプリに画像が表示されるようになり、S3のバケットに画像が保存されていることも確認できました。

<img width="1440" alt="スクリーンショット 2020-02-26 12.17.52.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438093/9f6cd833-fae7-2b83-d761-0e393c264338.png">



##参考記事
https://railsguides.jp/asset_pipeline.html
https://github.com/carrierwaveuploader/carrierwave#using-amazon-s3
https://railsguides.jp/5_2_release_notes.html#credential%E7%AE%A1%E7%90%86
https://workabroad.jp/posts/2166