AWS(S3)に画像をアップロードする

# はじめに
AWS（S3）に画像データを保存する必要性や手順をまとめたいと思います。
現状、herokuを用いてアプリのアップロードを行なっていますが、アップロードされた画像はアプリケーションのディレクトリに保存されていました。
しかし、ユーザーがアップロードした画像データは、アプリがデプロイまたは再起動（24時間に1回自動で行われる）される度に、消えてしまう仕様になっています。
そこで、ストレージサービス（画像等の膨大になるであろうデータを別途保管できる場所、サービスのこと）を利用することで、画像を長期的に保存することができるようになります。
ストレージサービスには様々な種類がありますが、今回はAWSが提供しているS3というサービスを使用します。

## S3（Amazon Simple Storage Service）とは
S3とは、AWSが提供するサービスの一つです。
（インターネット上にデータを保存する箱を借りられるサービスであるとイメージをすると良い。）
S3を利用することで、画像を保存したり、保存してある画像を取得したりすることができます。
S3に保存されたデータは実在の施設に分散して保管されるため、ネットワーク障害に強い仕様になっています。
また、使用した分だけ課金されるシステムであるため、保存できるデータ容量やファイル数の上限がなく使用することができます。
（個人制作のアプリケーションで保存する画像データの量であれば、無料でS3を利用することができます。）

## S3導入の流れ
AWSのサイト上で以下を実施します。
①S3で画像の保存先を作成する
その後、アプリケーションのソースコード上で、以下画像の保存先をS3に設定する処理を記述します。
②<ローカル環境>S3に保存する処理を記述する
③<本番環境>S3に保存する処理を記述する

## 1.S3で画像の保存先を作成する
### 1.AWSにルートユーザーでログインする
- 右上の「コンソールにサインイン」をクリック
- ルートユーザーのログイン画面に遷移します。
AWS登録時のルートユーザーのメールアドレスとパスワードを入力して、サインインしましょう。
二段階認証のトークン入力を求められるので、Authyアプリを起動して適切なトークン（数字6桁）を入力しましょう。
AWSサイト画面右上の表示によって、現在自分がどのユーザーでログインしているか確認できます。

## 2.バケット（S3での、自分用のデータを保存する場所）を作成する。
- 画面上部の検索フォームに「S3」と入力し、検索結果からS3のページにアクセス
- バケットを選択
- バケットを作成を選択
- 「バケット名」を入力し、リージョンが「アジアパシフィック（東京）」になっていることを確認。
バケットの名前はアクセスするときのURLに使用されるため、英数字で、まだ誰も付けたことがない名前を使う必要があります。名前が重複してしまう場合は、「付けたい名前 + 数値」を試してみることをおすすめします。数値部分を変えることで重複を避けやすくなります。例）「furima2020」など
- 画面を下にスクロールすると、アクセス許可の設定が表示されます。
まずは、「パブリックアクセスをすべてブロック」のチェックを外しましょう。
チェックを外すと、追加でチェックボックスが表示されます。
- ①「新しいパブリックバケットポリシーまたはアクセスポイントポリシーを介して付与されたバケットとオブジェクトへのパブリックアクセスをブロックする」
②「任意のパブリックバケットポリシーまたはアクセスポイントポリシーを介したバケットとオブジェクトへのパブリックアクセスとクロスアカウントアクセスをブロックする」
③「パブリックアクセスのブロックをすべてオフにすると、このバケットとバケット内のオブジェクトが公開される可能性があります。」
にチェックをつける
- オプション設定が表示されますが、何も入力せず、「バケットを作成」をクリックしましょう。

## 3.バケットポリシーを設定して、セキュリティ対策を行う
- バケットに対して、どのようなユーザーが、どのような処理をできるか取り決めをするものです。この処理とはデータの保存やデータの読み取りなど。
- IAMユーザーの情報を取得する
画面上部の検索フォームに「IAM」と入力し、検索結果からIAMのページにアクセス
左のサイドバーから「ユーザー」をクリック。
IAMユーザーのページが表示されます。
任意のユーザー名をクリック。
- ユーザー概要のページに遷移します。
「ユーザーのARN」をコピーするために、コピーするボタンをクリックしましょう。
- バケットポリシーの設定
画面上部の検索フォームに「S3」と入力し、検索結果からS3のページにアクセス
バケットからバケット名をクリック
「アクセス許可」をクリック
バケットポリシー欄の「編集」をクリック
    
        # AWSのサイト上の「ポリシー」という入力欄に下記をコピペ
        {
           "Version": "2012-10-17",
           "Id": "Policy1544152951996",
           "Statement": [
               {
                   "Sid": "Stmt1544152948221",
                   "Effect": "Allow",
                   "Principal": {
                       "AWS": "①"
                   },
                   "Action": "s3:*",
                   "Resource": "arn:aws:s3:::②"
               }
           ]
        }

- 上記①,②の欄を下記の通り編集する
「①」の箇所に、先ほどコピーした「ユーザーのARN」を入力します。
「②」の箇所に、作成したバケット名を入力します。
変更を保存し、バケットページに戻れば問題ない

ここまででS3の作成が完了。
ここからは実際にS3に画像データを保存する仕様に変更する。

## 4.ローカル環境からS3に画像を保存
### ソースコード内で画像をS3に保存する処理を記述
- "aws-sdk-s3"をGemfileに追記

        # Gemfile最下層に追記
        gem "aws-sdk-s3", require: false
        % bundle install　を忘れないよう注意

- development.rbを編集

画像の保存先を指定します。
現状では画像の保存先が「local」に設定されており、これはアプリケーション内に画像を保存することを意味しています。
S3に保存されるように設定を変更しましょう。

まず、development.rbに記述している画像の保存先の設定を「:local」→「:amazon」に変更

    # config/environments/development.rb
    # ~省略~
    
    config.active_storage.service = :amazon
    
    # ~省略~

- storage.ymlにバケット名とリージョン名を記述

S3で使用するバケット名とリージョン名を記述します。
storage.ymlに以下のコードを追記。

    # config/storage.yml
    test:
     service: Disk
     root: <%= Rails.root.join("tmp/storage") %>
    
    local:
     service: Disk
     root: <%= Rails.root.join("storage") %>
    
    ＜追記箇所（ここから）＞
    amazon:
     service: S3
     region: ap-northeast-1
     bucket: 自身の「バケット名」を入力
    ＜追記箇所（ここまで）＞    

    # ~省略~

### アクセスキーの記述方法を考える
最後に、S3にアクセスするための認証情報を設定します。
しかし、秘密情報である「Access key ID」「Secret access key」を直接storage.ymlに記載してはいけません。
そこで、環境変数を設定して、セキュリティ対策する

### 環境変数を設定して、セキュリティ対策
「Access key ID」「Secret access key」をそれぞれの環境変数（箱）に入れてソースコードに記述することで、GitHubでソースコードが公開されても秘密情報が漏洩する心配がありません。

- csvファイルを開いてアクセスキーの値を確認
まずは、環境変数に代入する「Access key ID」「Secret access key」の値を調べます。
ダウンロード済みの「new_user_credentials.csv」というcsvファイルを開いておきましょう。
後ほどこのcsvファイルを使用します。

- 環境変数を設定するファイルを開く【MacOSがCatalina以降の場合】

        % vim ~/.zshrc

「i」を入力して、ファイルを編集モードにします。
ターミナル左下に「INSERT」という文字が表示されれば文字が入力できるようになっています。

環境変数の設定ファイルに、以下のコードを追加しましょう。
右辺の値には、先ほど開いた「new_user_credentials.csv」というcsvファイルに記載してある値をコピーして、それぞれ貼り付けます。

    # ターミナル（開発中のアプリで実行）
    export AWS_ACCESS_KEY_ID="CSVファイルのAccess key IDの値を貼り付け"
    export AWS_SECRET_ACCESS_KEY="CSVファイルのSecret access keyの値を貼り付け"

値を貼りつけたら「escキー」→「:wq」の順で入力し、環境変数の設定ファイルを保存しましょう。
vimが終了し、通常のターミナルの状態に戻ることを確認します。

- sourceコマンド入力し、先ほど設定した環境変数を使えるようにします。
	
        % source ~/.zshrc
        ※ターミナルを2つ以上開いている場合は、すべてのタブでsourceコマンドを実行

- storage.ymlを編集

        # config/storage.yml
        test:
         service: Disk
         root: <%= Rails.root.join("tmp/storage") %>
        
        local:
         service: Disk
         root: <%= Rails.root.join("storage") %>
        
        amazon:
         service: S3
         region: ap-northeast-1
         bucket:  （自身のバケット名が記載されている状態です）
         ＜追記箇所（ここから）＞
         access_key_id: <%= ENV['AWS_ACCESS_KEY_ID'] %>
         secret_access_key:  <%= ENV['AWS_SECRET_ACCESS_KEY'] %>
         ＜追記箇所（ここまで）＞
        
        # ~省略~

これで安全にソースコード内で安全に秘密情報を記述することができました。

しかし、うっかりミスで環境変数を使用し忘れてしまい、誤って秘密情報をGitHubにpushしてしまう可能性もあります。
誤操作で秘密情報をpushしないよう対策しましょう。

### git-secretsを設定して、セキュリティ対策
git-secretsというツールを使用して、AWSの秘密情報を誤ってGitHubにpushしないよう設定

<strong>git-secrets</strong>とは、AWSが公開しているツールです。
commitしようとしたコードをチェックし、パスワードだと推定されるような文字列が含まれている場合は、警告を出して処理が中断してくれる機能です。

- ターミナルから、Homebrewを経由してgit-secretsを導入します。

        # ターミナル（ホームディレクトリで実行）
        % cd ~/ #ホームディレクトリに移動
        % brew install git-secrets

これで、有効化を行なったリポジトリでgit-secretsを使用する準備ができました。

- git-secretsの条件を設定
<strong>このgit-secretsの条件設定の作業は、開発中アプリケーションのディレクトリで実行します。</strong>

- どのようなコードのcommitを防ぐのかを設定していきましょう。
「Access key ID」「Secret access key」など、アップロードしたくないAWS関連の秘密情報を一括で設定します。
下記のコマンドを実行してください。

        # ターミナル（開発中のアプリで実行）
        % git secrets --register-aws --global

実際にどのような設定がされているか確認しましょう。
下記のコマンドを実行してください。

    # ターミナル（開発中のアプリで実行）
    % git secrets --list
    
    # ⬇️上記コマンドを実施すると、以下の内容が表示されます(秘密情報のパターンを正規表現で記載している)
    secrets.providers git secrets --aws-provider
    secrets.patterns [A-Z0-9]{20}
    secrets.patterns ("|')?(AWS|aws|Aws)?_?(SECRET|secret|Secret)?_?(ACCESS|access|Access)?_?(KEY|key|Key)("|')?\s*(:|=>|=)\s*("|')?[A-Za-z0-9/\+=]{40}("|')?
    secrets.patterns ("|')?(AWS|aws|Aws)?_?(ACCOUNT|account|Account)_?(ID|id|Id)?("|')?\s*(:|=>|=)\s*("|')?[0-9]{4}\-?[0-9]{4}\-?[0-9]{4}("|')?
    secrets.allowed AKIAIOSFODNN7EXAMPLE
    secrets.allowed wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY

上記のコマンドを実行すると、現在のgit-secretsの設定が表示されます。
様々な正規表現を用いて、AWSで作成される秘密情報のパターンが条件に設定されていることが分かります。
これで、「git secrets --install」を行なったリポジトリでは「git commit」コマンドを実行した際にAWSの秘密情報を含んでいないかチェックされるようになりました。

### GitHub Desktopからgit-secretsを利用できるようにする
GitHub Desktopを使用してcommitなどソースコード管理を行なっている場合は、また追加で設定が必要になります。
まず、GitHub Desktopがアプリケーションのディレクトリに存在しているか確認しましょう。
アプリケーションに内に存在していることを前提で設定を進めていくためです。
もし他の場所に存在している場合はアプリケーション内に移動しましょう。

次に、以下のコマンドを実行してGitHub Desktopにgit-secretsを適用

    # ターミナル
    % sudo cp /usr/local/bin/git-secrets /Applications/GitHub\ Desktop.app/Contents/Resources/app/git/bin/git-secrets

sudoコマンドを実行した際にパスワードの入力が必要となる場合があります。
このパスワードとはご自身のPCにログインする際のパスワードです。

上記コマンドで「No such file or directory」のエラーがでる場合は、GitHub Desktopのバージョンが古い可能性があります。以下のコマンドを実行しましょう。

    # ターミナル
    % sudo cp /usr/local/bin/git-secrets /Applications/GitHub\ Desktop.app/Contents/Resources/git/bin/git-secrets

### 今後作成する全てのリポジトリに、git-secretsが適用されるようにしましょう
ここまでの設定では、今後作成するリポジトリにはgit-secretsが適用されません。
特段の理由がなければ、以下のコマンドを実行して自動で適用されるようにしましょう。

    # ターミナル
    % git secrets --install ~/.git-templates/git-secrets
    % git config --global init.templatedir '~/.git-templates/git-secrets'

これで、今後作成する他のアプリケーションにも、git-secretsが適用されるようになりました。

### ローカル環境で画像を保存できるか確認
まずローカル環境で画像投稿を行いましょう。

- IAMユーザーでログインしましょう
AWSのサイトにIAMユーザーでログインしましょう。
「ご自身のIAMユーザー名_credentials.csv」を、Numbersというアプリケーションで開きます。
開いたcsvファイルに記載されている「Console login link」という欄のURLをクリックして、IAMユーザーのログイン画面にアクセスしましょう。
MFAコードの入力画面に遷移します。
これは前章で二段階認証を設定したためです。
スマートフォンでAuthyのアプリケーションを開き、IAMユーザーのアカウントを開きましょう。
表示されている6桁の数字を、「MFAコード」に入力してください。
入力できたら「送信」をクリックしましょう。

- S3のバケットのページを開きましょう
S3のバケットにファイルが保存されているか確認します。
まず、以下の手順でバケットのページに遷移しましょう。
1.AWSのサイト画面上部の検索フォームから「S3」を入力
2.検索結果から「S3」を選択
3.「バケット名」をクリック
次に、以下の手順で投稿した画像が保存されているか確かめます。
1.一度リロードして情報を最新の状態にする
2.「オブジェクト」という項目の中に投稿した画像があるか確認する
3.【確認方法①】 最終更新日時が投稿した時間と一致しているか
4.【確認方法②】 オブジェクトをクリックし詳細ページを開く。「開く」をクリックし、表示された画像と投稿した画像が一致しているか

画像が上手く投稿できない場合、以下を確認しましょう
もし画像がS3に保存できないまたはエラーが出た場合、以下のリストを確認しましょう。

- <strong>【gemのインストール】</strong>
"aws-sdk-s3"のgemをインストールできているか
gemのインストール後にrails sの再起動をしているか

- <strong>【development.rbの記述】</strong>
「:local」→「:amazon」に変更できているか

- <strong>【storage.ymlの記述】</strong>
「自身のバケット名」を正しく入力できているか。カリキュラムの見本例のバケット名にしていないか。
「Access key ID」「Secret access key」を正しく入力できているか。コピーし間違えていないか。逆になっていないか。
全角スペースやタブを入力していないか。余計な文字が含まれていないか。「:」の後ろに半角スペースがあるか。

- <strong>【環境変数の設定】</strong>
環境変数の設定後にsourceコマンドを入力したか。rails sしているターミナルのタブ又はウィンドウでも入力したか。
「Access key ID」「Secret access key」を正しく設定できているか。コピーし間違えていないか。逆になっていないか。
上記が問題ない場合は、前章のバケットポリシーの内容も確認しましょう。

- <strong>【バケットポリシーの記述】</strong>
「ユーザーARN」を記述できているか
「作成したバケット名」を記述できているか

## 本番環境からS3に画像を保存
- production.rbを編集しましょう
ローカル環境での設定と同様に、画像の保存先を指定します。
現状では画像の保存先がローカルに設定されているため、S3に保存されるように設定を変更しましょう。
まずproduction.rbに記述している画像の保存先の設定を「:local」→「:amazon」に変更します。
        
        # config/environments/production.rb
        # ~省略~
        
        config.active_storage.service = :amazon
        
        # ~省略~

### Heroku上でも環境変数を使えるようにしよう
Herokuで環境変数を扱うには、「heroku config:setコマンド」を打つ必要があります。
開発中のアプリケーションのディレクトリで、以下2つのコマンドを実行しましょう。
右辺にはcsvファイルの「Access key ID」「Secret access key」の値をコピーして貼り付けてください。

    # ターミナル（開発中のアプリで実行）
    % heroku config:set AWS_ACCESS_KEY_ID="CSVファイルのAccess key IDの値を貼り付け"
    
    # ターミナル（開発中のアプリで実行）
    % heroku config:set AWS_SECRET_ACCESS_KEY="CSVファイルのSecret access keyの値を貼り付け"

境変数が正しく設定できているかを確認するために、下記のコマンドを入力してください。

    # ターミナル（開発中のアプリで実行）
    % heroku config

### 編集内容をHerokuに反映
ローカル環境で編集した内容をHerokuに反映させた上で、本番環境の挙動を確認します。
編集内容をコミットしましょう
ここまで編集出来たらコミットをしましょう。

    # ターミナル（開発中のアプリで実行）
    % git push heroku master

### 本番環境で画像を保存できるか確認
ここまでで、本番環境での設定とセキュリティ対策ができました。
それでは、実際に保存したファイルがS3に保存されるか確認しましょう。

本番環境にアクセスするためのWeb URLを取得しましょう。
以下のコマンドを実行することで、Herokuにデプロイされたアプリケーションの情報を見ることができます。

    # ターミナル（開発中のアプリで実行）
    % heroku apps:info

実行結果から「Web URL」の欄を確認しましょう。
Web URLにアクセスし、本番環境で画像の投稿を行いましょう。

### 画像保存されているか確認
- IAMユーザーでログインしましょう
まず、IAMユーザーにログインしましょう。
ローカル環境での動作確認時と同様に、以下の手順で実施しましょう。

1.「ご自身のIAMユーザー名_credentials.csv」を開く
2.「Console login link」という欄のURLをクリックして、IAMユーザーのログイン画面にアクセス
3.「User name」「Password」を入力し、「サインイン」をクリック
4.Authyのアプリケーションを開き、トークンを表示
5.表示されている6桁の数字を、「MFAコード」に入力
6.S3のバケットのページを開きましょう

- S3のバケットにファイルが保存されているか確認します。
まず、以下の手順でバケットのページに遷移しましょう。

1.AWSのサイト画面上部の検索フォームから「S3」を入力
2.検索結果から「S3」を選択
3.「バケット名」をクリック
4.画像が保存されているか確認しましょう

次に、以下の手順で投稿した画像が保存されているか確かめます。
1.一度リロードして情報を最新の状態にする
2.「オブジェクト」という項目の中に投稿した画像があるか確認する
3.【確認方法①】 最終更新日時が投稿した時間と一致しているか
4.【確認方法②】 オブジェクトをクリックし詳細ページを開く。「開く」をクリックし、投稿した画像が表示されるかどうか

### 画像が上手く投稿できない場合、以下を確認しましょう
もし画像が投稿できないまたはエラーが出た場合、以下のリストを確認しましょう。

- 「Access key ID」「Secret access key」を正しく設定できているか。コピーし間違えていないか。逆になっていないか。
- 環境変数を設定する前にHerokuにpushしてしまっていないか。
- ブランチを切って作業していないか。その場合はmasterブランチにマージし忘れていないか。

S3導入前に投稿してた画像が表示エラーになる場合があります。
画像の保存先の設定を変更することにより、画像の参照先がアプリケーション内からS3へ置き換わります。したがって、S3導入前に投稿した画像が表示できなくなります。
以下のコマンドを実行し、一度データベースをリセットしてください。
この際、データベースに保存されていたデータは全て消えるため注意しましょう。

    # ターミナル（開発中のアプリで実行）
    % heroku run DISABLE_DATABASE_ENVIRONMENT_CHECK=1 rails db:drop db:create db:migrate