【Rails】Heroku上のRailsアプリからAWS S3へ画像を保存する方法(Paperclip&Carrierwave)


#はじめに

Rails+Heroku+AWS S3+Paperclip or Carrierwaveで画像投稿機能を実装するというのはよくある組み合わせのようですが、いくつかハマった点があり、一連の流れをまとめておきます。
この記事の最後に、関連する記事まとめを載せていますのでご参考にどうぞ。

**Herokuでは画像保存ができない**
Heroku上にデプロイされたRailsアプリに画像投稿機能を実装してあるものの、Herokuでは画像保存ができないため、画像アップロードサーバーとしてAWSのS3を使うことにしました。

**※今回、開発の関係上、PaperclipとCarrierwaveを併用しています。**
この記事では、Paperclipの場合とCarrierwaveの場合とで分けて、手順を記載しています。

※AWS(Amazon Web Services) S3は登録から1年間は無料だそうです。
(詳しい制限などは公式HPをご覧ください。)
※Herokuへのデプロイは完了していることを前提としています。
※画像投稿機能は開発環境で実装が完了していることを前提としています。

##環境
Ruby 2.4.5
Ruby On Rails 4.2.8
mySQL（開発環境）
PostgreSQL(本番環境)
サーバー：Heroku　
画像保存先：AWS S3
Paperclip
Carrierwave

**※Paperclipのサポートが終了するらしい**
今後は、Paperclipではなく、ActiveStorageの利用を推奨されています。
[Deprecating Paperclip(thoughtbot)](https://thoughtbot.com/blog/closing-the-trombone)

#手順

1. AWSでユーザー登録
2. AWS S3でバケット作成&シークレットキー等取得
3. Gem取得
4. モデル等の編集
4. 環境設定(シークレットキー等)

##1. AWSに登録
AWSのサイトからユーザー登録。クレジットカードの登録と電話番号orSMS認証があります。

####ここで一つ落とし穴
[Ruby on Rails ~herokuにアップしたRailsアプリで、Amazon S3(AWS)に画像を保存する（コードメモ）](https://qiita.com/wtb114/items/500bb7b0ed6a14b8ef8b)
こちらの記事のとおり、「セキュリティ認証」のタブからアクセスキーを取得したのですが、こちらではうまくいかず、IAMというものの登録が必要でした。

「セキュリティ認証」でも「IAM」の場合でも、取得されるものは「アクセスID」と「シークレットアクセスキー」という名前で同じなのですが、キーの中身は違いました。AWSの仕様が変わったのでしょうか。

これに長い間ハマったので気をつけてください。。。

**以下アクセスキーID取得までの流れ**

１. AWSコンソールにログイン（アカウント作成後）
2. サービス(左上タブ)から、セキュリティ,ID,およびコンプライアンスの「IAM」をクリック
3. ユーザー(左タブ)をクリック
4. ユーザーを追加
5. 適当なユーザー名を入力し、AWSアクセスの種類を選択から「プログラムによるアクセス」にチェック
6. 次のステップで、「既存のポリシーを直接アタッチ」「AmazonS3FullAccess」にチェック
7. ユーザー作成完了後、アクセスキーの作成からアクセスキー IDとシークレットアクセスキーを取得
(あとで利用するので厳密に保管)

※こちらの記事が画像付きで分かりやすいです。
[Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する](https://qiita.com/junara/items/1899f23c091bcee3b058#aws)

##2. バケット作成


1. サービス（左上タブ）→S3
2. バケットを作成
(バケット名は、誰とも被らないものでなければいけません。また、「.」を入れると思わぬエラーが起こる可能性があるという記事もあるので、避けるのが無難。)

3.　リージョンは今回は、Tokyoを選択


#3. Paperclipの場合

モデルの作成など、ローカル上ではアップロード機能ができている前提とします。
今回は、ユーザー編集画面でアバター画像を投稿できる機能を実装しています。

##3. 1. Gemfileの編集
```ruby:Gemfile.rb
  gem 'paperclip'
  gem 'aws-sdk', '< 2.0'
  gem 'aws-sdk-s3'
```
aws-sdkのみでやったあと、aws-sdk-s3も追加してくださいというエラーがでたので追加。


```
  $bundle install
```

##3. 2. User.rbの編集
User.rbに本番環境の時はS3に保存し開発環境ではローカルに保存するよう指定。
AWSのアクセスキーなどは、Githubに載せるとまずいので環境変数として設定します。
s3_host_nameは、リージョンの設定によって変わります。以下はTokyoの場合。

```ruby:user.rb
#AWS-S3の設定
  if Rails.env.production?
    S3_CREDENTIALS={access_key_id: ENV['AWS_S3_ACCESS_ID'], secret_access_key: ENV['AWS_S3_ACCESS_KEY'], bucket: ENV['AWS_S3_BUCKET_NAME'], s3_host_name: "s3-ap-northeast-1.amazonaws.com", s3_region: ENV['AWS_REGION']}
  end

#avaterの保存先設定
  if Rails.env.production?
    has_attached_file :avatar, storage: :s3, s3_credentials: S3_CREDENTIALS,
    styles: { medium: "300x300", thumb: "100x100" }, :default_url => 'missing_:style.png',
    :convert_options => {:all  => "-gravity Center -crop 150x150+0+0"}, path:":attachment/:id/:style.:extension"
  else
    has_attached_file :avatar, styles: { medium: "300x300", thumb: "100x100" }, :default_url => 'missing_:style.png',
    :convert_options => {:all  => "-gravity Center -crop 150x150+0+0"}
  end

  validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/
```

これで、Paperclipで投稿したアバター画像は、AWS S3に保存されます。


##3. 3. 環境変数の設定
Herokuの場合、これらの環境変数をHeroku上で入力する必要があります。
コマンドでも操作できますが、今回はHerokuのサイト上で設定してみます。

1. Herokuにログインし、自分のAppを選択。
2. Settingsから「Config Vars」の「Reveal Config Vars」を選択
3. 「KEY」と「VALUE」にそれぞれ入力（例:KEY[AWS_S3_ACCESS_ID], VALUE[123456......])

これでHeroku上での環境変数が設定できます。

以下、本番環境がHerokuの場合は必要ありませんが、```dotenv-rails```を利用して環境変数を管理する場合。GithubにRailsアプリをアップするものの他の人には見られないようにする方法です。

```ruby:gemfile.rb
  gem 'dotenv-rails' 
```

```
  $bundle install
```


AWSのIAMで取得したアクセスIDとアクセスキー、bucketname、Regionを.envに入力。


```ruby:.env
AWS_S3_ACCESS_ID='*************'
AWS_S3_ACCESS_KEY='**************'
AWS_S3_BUCKET_NAME='*************'
AWS_REGION='ap-northeast-1' #東京の場合

```

```ruby:gitignore
  #Ignore .env
/.env
```

##3. 4. アバター画像の表示

```erb:users/edit.html.erb
<%= image_tag(@user.avatar(:medium), :size => "70x70", :alt => 'hoge') %>
```


#4. Carrierwaveの場合

Imageモデルという新しいモデルを作成し、それとUserモデルを紐づける(Association)ことで、Userが一意のImageを投稿できる機能を実装しました。

こちらもPaperclipの場合と同じく、AWS S3に画像を保存するという流れで実装していきます。

##4. 1. Gemfileの編集

```ruby:Gemfile.rb
   gem 'carrierwave'
   gem 'fog'
```

####fog-awsについて

```ruby:terminal
Thank you for installing fog!

IMPORTANT NOTICE:
If there's a metagem available for your cloud provider, e.g. `fog-aws`,
you should be using it instead of requiring the full fog collection to avoid
unnecessary dependencies.

'fog' should be required explicitly only if:
- The provider you use doesn't yet have a metagem available.
- You require Ruby 1.9.3 support.
```


```fog``` を```bundle install```後、このようなNOTICEが出たため、```fog-aws```にして試してみたのですが、エラーでうまくいきませんでした。
fog-awsでエラーが起こるという記事がいくつかStackoverflowで見受けられました。
結果、```fog```で進めました。

##4. 2. Image_uploaderの編集
ここで重要なのは、 ```if Rails.env.development?```の部分です。環境によってストレージを分ける処理をしています。　


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

  # Choose what kind of storage to use for this uploader:
  if Rails.env.development?
    storage :file
  elsif Rails.env.test?
    storage :file
  else
    storage :fog
  end

  # Override the directory where uploaded files will be stored.
  # This is a sensible default for uploaders that are meant to be mounted:
  def store_dir
    "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}"
  end

  # Provide a default URL as a default if there hasn't been a file uploaded:
  # def default_url(*args)
  #   # For Rails 3.1+ asset pipeline compatibility:
  #   # ActionController::Base.helpers.asset_path("fallback/" + [version_name, "default.png"].compact.join('_'))
  #
  #   "/images/fallback/" + [version_name, "default.png"].compact.join('_')
  # end
  def default_url
    "default.jpg"
  end

  # Process files as they are uploaded:
  # process scale: [200, 300]
  #
  # def scale(width, height)
  #   # do something
  # end

  # Create different versions of your uploaded files:
  # version :thumb do
  #   process resize_to_fit: [50, 50]
  # end

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w(jpg png)
  end

  # Override the filename of the uploaded files:
  # Avoid using model.id or version_name here, see uploader/store.rb for details.
  def filename
     super.chomp(File.extname(super)) + '.jpg' if original_filename.present?
  end
end

```
##4. 3. Carrierwave.rbの作成
carrierwave.rbを/initializers以下に作成。
AWSで取得したシークレットキー等を環境変数として設定。

```ruby:config/initializers/carrierwave.rb
   if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
        :provider              => 'AWS',
        :aws_access_key_id     => ENV['AWS_S3_ACCESS_ID'],
        :aws_secret_access_key => ENV['AWS_S3_ACCESS_KEY'],
        :region                => ENV['AWS_REGION'],
    }

    config.fog_directory = ENV['AWS_S3_BUCKET_NAME']
    config.cache_storage = :fog
  end
end
```
##4. 4. 環境変数の設定
**herokuでの環境変数の設定の仕方は同じですので、この記事中にある「Paperclipの場合」の該当部を参考にしてください。**




##4. 5. 画像の表示

```erb:users/edit.html.erb
<%= image_tag(@image.image , :class => "hogehoge") %>
```

####image.rb
参考までに

```ruby:models/image.rb
   class Image < ActiveRecord::Base
  mount_uploader :image, ImageUploader
    #association
  belongs_to :user
end
```
#まとめ
PaperclipとCarrierwaveを併用すると何か予期せぬ動作をするのかと思っておりましたが、特に問題はなさそうでした。
AWSの「セキュリティ認証情報」ではなく、IAMのキーを使うことと、fog-awsがエラーになることでハマりましたが。

ただ、Rails5以降の画像投稿機能については、ActiveStorageで実装した方がよさそうです。
[Rails: PaperclipからActiveStorageへの移行ガイド by thoughtbot（翻訳）](https://techracho.bpsinc.jp/hachi8833/2018_05_21/56668)


#参考URL

以下、関連する記事まとめです。AWSの仕様も頻繁に変わるみたいなので、比較的新しい記事を参考にしたほうがいいかもしれません。

[Ruby on Rails ~herokuにアップしたRailsアプリで、Amazon S3(AWS)に画像を保存する（コードメモ）](https://qiita.com/wtb114/items/500bb7b0ed6a14b8ef8b)
[Railsでcarrierwaveを使ってAWS S3に画像をアップロードする手順を画像付きで説明する](https://qiita.com/junara/items/1899f23c091bcee3b058#aws)
[Rails+carrierwave+heroku+AWS S3で画像アップロードさせるときにハマったこと](http://euglena1215.hatenablog.jp/entry/2017/01/07/134802)
[RailsでPaperclipを使ってAmazon S3に画像を保存する](https://www.aguuu.com/archives/2012/12/paperclip/)
[【Rails】AWS S3を使ってHerokuで画像を投稿できるようにする方法](https://qiita.com/hmmrjn/items/479c9e9ce82771f1b6d7)