Railsチュートリアル 第13章 ユーザーのマイクロポスト - 本番環境での画像アップロード

# 本番環境での画像アップロードに必要な技術

本番環境に画像をアップロードするためには、以下のような技術が必要となります。

* AWS IAMにおけるグループおよびユーザーの設定
* AWS S3バケットの新規作成およびアクセス権設定
* 本番環境に適用するRails設定の記述
* Herokuの環境変数設定

特に未経験の人にとっては、相応に高度で総合力が試されるパートではないかと思われます。

# 本番環境での画像アップロードに必要な設定

現在までに実装した画像アップローダーは、ローカルのファイルシステムに画像を保存するようになっているため、本番環境での使用には適していません。「ローカルのファイルシステムに画像を保存する」という動作は、`app/uploaders/picture_uploader.rb`内の以下の記述に由来します。

```ruby:app/uploaders/picture_uploader.rb（抜粋）
# Choose what kind of storage to use for this uploader:
storage :file
# storage :fog
```

「本番環境では、ローカルのファイルシステムとは別のクラウドストレージに画像を保存する」という設定にしたい場合、以下のように、「本番環境では`fog` gemを使用する。それ以外ではローカルのファイルシステムに画像を保存する。」という記述を行います。

```diff:
  class PictureUploader < CarrierWave::Uploader::Base
    # ...略

    # Choose what kind of storage to use for this uploader:
-   storage :file
-   # storage :fog
+   if Rails.env.production?
+     storage :fog
+   else
+     storage :file
+   end

    # ...略
  end
```

`Rails.env.production?`は、本番環境であれば`true`、それ以外の環境（開発環境・テスト環境等）では`false`を返します[^テスト環境では]。

[^テスト環境では]: 「テスト環境では`true`、それ以外の環境では`false`を返す」という動作をする`Rails.env.test?`は、Railsチュートリアルの第13章中、「演習 - 画像のリサイズ」で用いましたね。

# Amazon Web Servicesの設定

* マイクロポストの画像を保存するためのAWS S3バケットを新規に作成する
* AWSのリソースに対してプログラムによるアクセスが可能なユーザーを新規に作成する
    * 当該ユーザーは、AWSマネジメントコンソールにはアクセスしない
    * 別途当該S3バケットへのアクセス権を持つグループを割り当てる
* 上記ユーザーに割り当てるためのグループを新規に作成する
    * 当該S3バケットへのフルアクセスを許可する
    * S3バケットの新規作成・削除は明示的に拒否する

以上の条件を前提として、以下の情報を参考にして設定しました。

* [初めてのAWS (Amazon Web Service)](https://qiita.com/terappy/items/1e10bb85505acac5c282)
* [AWSで請求アラートを設定する - Qiita](https://qiita.com/Soh1121/items/fbd9e13cd050bcefbe55)
* [特定S3バケットに対してのみアクセスを許可したい – サーバーワークスエンジニアブログ](http://blog.serverworks.co.jp/tech/2018/04/23/post-63971/)
* [エラー&lt;Message&gt;Access Denied&lt;/Message&gt; 〜Rails + Carrierwave + HerokuでAWS S3に画像を保存〜 - Qiita](https://qiita.com/yukiyukimiyaiwa/items/a9e0103c8342b81f6ac1)

特に、一番下に書かれた情報については見落としがちかと思います。

なお、Secretキーの内容を閲覧できるのは、Accessキーの新規作成時のみです。Accessキーに対応するSecretキーがわからない場合は、Accessキーそのものを新規作成する必要があります。

## IAMにて「AWSマネジメントコンソールにはアクセスせず、プログラムによるアクセスのみを許可する」というユーザーを作成するには

GUIで行う場合、ユーザー作成の最初の画面で「プログラムによるアクセス」のみにチェックを入れます。「AWS マネジメントコンソールへのアクセス」にはチェックを入れません。

<img width="750" alt="2020-01-19 142328.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/d6e2d404-dbd4-fdbb-add8-f13e050ac180.png">

## S3バケットへのパブリックアクセスを部分的に許可する

今回開発中のアプリケーションにおいて、S3バケットへの画像の保存を正常に行うには、以下2つの設定を行う必要があります。

* S3バケットの「アクセス権限 - ブロックパブリックアクセス (バケット設定)」の設定を変更する
* S3バケットの「アクセス権限 - バケットポリシー」の設定を変更する

### S3バケットの「アクセス権限 - ブロックパブリックアクセス (バケット設定)」の設定を変更する

* 「新規のパブリックバケットポリシーまたはアクセスポイントポリシーを介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする」のチェックボックスは「オン」
* 上記以外のチェックボックスはオフ

<img width="1122" alt="2020-01-19 143557.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/323778/db51ef87-f754-6af4-4ddf-26b81546c5f7.png">

### S3バケットの「アクセス権限 - バケットポリシー」の設定を変更する

「アクセス権限 - バケットポリシー」のJSONエディタに、以下のJSONを入力していきます。

```json:
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::[12桁のアカウントID]:user/[今回作成したユーザー名]"
            },
            "Action": "*",
            "Resource": "arn:aws:s3:::[バケット名]/*"
        }
    ]
}
```

# CarrierWaveを通してS3を使うようにする

S3アカウントの作成と設定が終わったら、CarrierWaveに与える追加の設定を記述していきます。その内容は以下のようになります。

```ruby:config/initializers/carrier_wave.rb
if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      # Amazon S3用の設定
      :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
```

ポイントは以下です。

* S3バケットへのアクセスに必要な情報は、Herokuの環境変数によって与えるようにしている
    * 「機密情報はソースコードにベタ書きしてはいけない」というのは大原則である
    * Herokuの環境変数は、手動で設定する必要がある
* `CarrierWave.configure`の用法は、以前の演習で作成した`config/initializers/skip_image_resizing.rb`と同様である
    * 対象環境はtestではなくproduction
    * `config.fog_credentials`の設定内容は「ハッシュの配列」として与える

なお、「GitHubのパブリックリポジトリにAWSのアクセスキーをアップロードしてしまった場合に何が起こるか」については、以下のようなリソースに詳しく書かれています。本当に怖いですね。

* [AWSで不正利用され80000ドルの請求が来た話 - Qiita](https://qiita.com/koyama9876/items/add70cba3cccdb7fa995)
* [初心者がAWSでミスって不正利用されて$6,000請求、泣きそうになったお話。 - Qiita](https://qiita.com/mochizukikotaro/items/a0e98ff0063a77e7b694)
* [GitHub に AWS キーペアを上げると抜かれるってほんと？？？試してみよー！ - Qiita](https://qiita.com/saitotak/items/813ac6c2057ac64d5fef)
    * 「GitHubのパブリックリポジトリにAWSのアクセスキーをアップロードしたら抜かれるのか」という実験。結果は「抜かれるまでの時間はわずか13分だった」というものであったそうです。

## Herokuの環境変数に、S3バケットにアクセスするために必要な情報を追加する

Herokuの環境変数を設定するコマンドは、`heroku config:set`となります。以下のように使用します。

```
heroku config:set [環境変数名]="[値]"
```

S3バケットにアクセスするために必要な情報のうち、セキュリティ確保の観点からソースコードに記述しない項目は以下の4つです。これらをHerokuの環境変数として与える必要があります。

* S3バケットのAccessキー
* S3バケットのSecretキー
* S3バケットのID
* S3バケットが属するリージョンのコード
    * 東京リージョンであればap-northeast-1となります

```
>>> heroku config:set S3_ACCESS_KEY="Accessキー"
>>> heroku config:set S3_SECRET_KEY="Secretキー"
>>> heroku config:set S3_BUCKET="バケットID"
>>> heroku config:set S3_REGION="リージョン"
```

## 画像を保存するディレクトリを、Gitによる管理対象から除外する

画像をはじめとするバイナリファイルは、一般に、Gitほかバージョン管理システムによるバージョン管理の対象とはなりません。そのため、バイナリファイルを保存するディレクトリは、バージョン管理の対象から除外する必要があります。

「Git管理下にあるディレクトリに格納されている特定リソースのみを、Gitによる管理対象から除外したい」という場合、一般に、「当該リソースについて、Gitリポジトリのルートディレクトリからのパスを`.gitignore`ファイルに記述する」という方法により実現できます。

画像を保存するディレクトリは、Gitリポジトリのルートディレクトリを`/`とすると、`/public/uploads`となります。

結果、`.gitignore`に追加する内容は以下のようになります。

```diff:.gitignore
  # See https://help.github.com/articles/ignoring-files for more about ignoring files.
  #
  # If you find yourself ignoring temporary files generated by your text editor
  # or operating system, you probably want to add a global ignore instead:
  #   git config --global core.excludesfile '~/.gitignore_global'

  # ...略
+
+ # Ignore updated images
+ /public/uploads
```

## ここまでの変更をHerokuにデプロイする

ソースコードに対する変更は、当然ながらデプロイしなければHeroku（に限らず本番環境全般）に反映されません。

## masterブランチの変更内容をHerokuにプッシュする

まずは、masterブランチの内容をHerokuにpushしていきます。

```
>>> git push heroku
```

masterブランチの内容をHerokuにpushすると、裏で自動的にBundlerが動きます。`pg`や`fog`、ならびにそれらが必要とするgemが次々にインストールされる様子がコンソール出力からも見て取れます。

### データベースをリセットする

続いて、Heroku上のRDBの内容を一度完全にリセットします。

```
>>> heroku pg:reset DATABASE
```

なお、この操作を行うにあたっては、追加の確認が求められます。「データベースのリセット」というのは、影響範囲が極めて大きい破壊的変更であるためです。

```
>>> heroku pg:reset DATABASE
 ▸    WARNING: Destructive action
 ▸    postgresql-cylindrical-32893 will lose all of its data
 ▸    
 ▸    To proceed, type [アプリ名] or re-run this command with --confirm [アプリ名]

> [アプリ名]
Resetting postgresql-cylindrical-32893... done
```

## データベースの構造と内容の再構築

まずはデータベースの構造の再構築からです。Herokuにおけるデータベースの構造の再構築は、`heroku run rails db:migrate`コマンドで行います。

```
>>> heroku run rails db:migrate
Running rails db:migrate on ⬢ [アプリ名]... up, run.8443 (Free)
...略
== 20190928080951 CreateUsers: migrating ======================================
== 20190928080951 CreateUsers: migrated (0.0093s) =============================
== 20191010034159 AddIndexToUsersEmail: migrating =============================
== 20191010034159 AddIndexToUsersEmail: migrated (0.0070s) ====================
== 20191013040411 AddPasswordDigestToUsers: migrating =========================
== 20191013040411 AddPasswordDigestToUsers: migrated (0.0024s) ================
== 20191104221611 AddRememberDigestToUsers: migrating =========================
== 20191104221611 AddRememberDigestToUsers: migrated (0.0028s) ================
== 20191128032931 AddAdminToUsers: migrating ==================================
== 20191128032931 AddAdminToUsers: migrated (0.0099s) =========================
== 20191202093532 AddActivationToUsers: migrating =============================
== 20191202093532 AddActivationToUsers: migrated (0.0089s) ====================
== 20191211225559 AddResetToUsers: migrating ==================================
== 20191211225559 AddResetToUsers: migrated (0.0048s) =========================
== 20191218224953 CreateMicroposts: migrating =================================
== 20191218224953 CreateMicroposts: migrated (0.0330s) ========================
== 20200105225338 AddPictureToMicroposts: migrated (0.0019s) ==================
...略
```

マイグレーションの進捗に、サンプルアプリケーションに積み上げてきた機能とその実装履歴が見えます。積み上げてきたものの歴史を感じられていいですよね。

続いてはデータベースの内容の再構築です。こちらは`heroku run rails db:seed`コマンドで行います。

AWSの設定情報が正しく与えられていれば、画像つきのマイクロポストを正常に投稿できます。その際、Herokuのログには以下のような記録が残ります。

```
2020-01-18T13:31:34.647241+00:00 app[web.1]: I, [2020-01-18T13:31:34.647131 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Started POST "/microposts" for 1.33.232.123 at 2020-01-18 13:31:34 +0000
2020-01-18T13:31:34.648257+00:00 app[web.1]: I, [2020-01-18T13:31:34.648156 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Processing by MicropostsController#create as HTML
2020-01-18T13:31:34.648445+00:00 app[web.1]: I, [2020-01-18T13:31:34.648361 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   Parameters: {"utf8"=>"✓", "authenticity_token"=>"MILJ4TEbgpIItHsVQbg+f09zcrOzVHCT8rTACTQesXuw/smIusnvBoM9eP3LgfNFC8nK3X72awHdZPIwpYhM5A==", "micropost"=>{"content"=>"LGTM!", "picture"=>#<ActionDispatch::Http::UploadedFile:0x00007f94603397e8 @tempfile=#<Tempfile:/tmp/RackMultipart20200118-4-jt3qxq.png>, @original_filename="lgtm3.png", @content_type="image/png", @headers="Content-Disposition: form-data; name=\"micropost[picture]\"; filename=\"lgtm3.png\"\r\nContent-Type: image/png\r\n">}, "commit"=>"Post"}
2020-01-18T13:31:34.652670+00:00 app[web.1]: D, [2020-01-18T13:31:34.652572 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   User Load (1.9ms)  SELECT  "users".* FROM "users" WHERE "users"."id" = $1 LIMIT $2  [["id", 1], ["LIMIT", 1]]
2020-01-18T13:31:35.092221+00:00 app[web.1]: D, [2020-01-18T13:31:35.092040 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]    (11.6ms)  BEGIN
2020-01-18T13:31:35.095644+00:00 app[web.1]: D, [2020-01-18T13:31:35.095539 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]   SQL (1.2ms)  INSERT INTO "microposts" ("content", "user_id", "created_at", "updated_at", "picture") VALUES ($1, $2, $3, $4, $5) RETURNING "id"  [["content", "LGTM!"], ["user_id", 1], ["created_at", "2020-01-18 13:31:35.093179"], ["updated_at", "2020-01-18 13:31:35.093179"], ["picture", "lgtm3.png"]]
2020-01-18T13:31:36.478723+00:00 heroku[router]: at=info method=POST path="/microposts" host=warm-woodland-62915.herokuapp.com request_id=5d55adcb-c098-456a-8352-89e9afd326a6 fwd="1.33.232.123" dyno=web.1 connect=0ms service=3010ms status=302 bytes=1017 protocol=https
2020-01-18T13:31:36.473495+00:00 app[web.1]: D, [2020-01-18T13:31:36.473384 #4] DEBUG -- : [5d55adcb-c098-456a-8352-89e9afd326a6]    (1.7ms)  COMMIT
2020-01-18T13:31:36.474219+00:00 app[web.1]: I, [2020-01-18T13:31:36.473995 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Redirected to https://warm-woodland-62915.herokuapp.com/
2020-01-18T13:31:36.474222+00:00 app[web.1]: I, [2020-01-18T13:31:36.474157 #4]  INFO -- : [5d55adcb-c098-456a-8352-89e9afd326a6] Completed 302 Found in 1826ms (ActiveRecord: 16.4ms)
```

AWS S3を使っているからといって、Heroku側のログには特段変わった内容のログが記録されるわけではないといえそうです。