【AWS S3 × Laravel】画像アップロード先をS3に変更した際に発生したエラーと解決までの道のり（nginxデバッグ対応）

#Laravelアプリケーションにて画像保存先をS3に変更した途端正しく表示されなくなった
目下Laravel7.x系でアプリケーションを作成しており、同アプリ内で画像をアップロードし表示するという機能を実装しておりました。

これまで開発環境ではローカルディスク内に保存しておりましたが、本番環境をAWSのEC2上でアプリケーションをデプロイすることから、画像保存先もS3に変更しようとしていたところエラーが発生してしまいまいした。

今回は私が遭遇したエラーと解決までの道のりを備忘も兼ねてまとめてみようと思います。

エラーは開発環境と本番環境それぞれで発生しました。
また本番環境ではnginx、Laravel双方でのデバッグ対応を初めて行ないました。

##実装内容
まずは今回実装した内容になります。（説明のため一部簡略化）
少し複雑な形ですが以下を行なっています。

* 投稿画像（ここではプロフィール画像）をストレージに保存（saveAvatarメソッド）
* ファイル名をDBに保存

画像アップロード機能に関しては他の記事にもたくさんあるので今回は割愛いたしますが、詳細は以下をご覧ください。

```php:ProfileController.php
<?php

namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\File;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Intervention\Image\Facades\Image;

class ProfileController extends Controller
{
     public function editProfile(Request $request)
     {
         if ($request->has('avatar')) {

            //saveAvatar()で投稿画像のファイル名をDBに保存
            $fileName = $this->saveAvatar($request->file('avatar'));
            $user->avatar_file_name = $fileName;
        }

         $user->save();
         return redirect()->back();
     }

      //アバター画像をリサイズして保存
      private function saveAvatar(UploadedFile $file): string
      {
          //makeTempPath()で一次保存用のファイルを生成
          $tempPath = $this->makeTempPath();
          
          //Intervention Imageを使用して、画像をリサイズ後、一時ファイルに保管
          Image::make($file)->fit(200, 200)->save($tempPath);

          //Storageファサードを使用して画像ファイルをディスク（s3を選択）にavatarsフォルダに保存
          $filePath = Storage::disk('s3')
              ->putFile('avatars', new File($tempPath));

          //保存された画像ファイル名を返す
          return basename($filePath);
      }

     //一時ファイル生成して保存パスを生成。
     private function makeTempPath(): string
     {
         //tmpに一時ファイルが生成され、そのファイルポインタを取得
         $tmp_fp = tmpfile();

         //ファイルのメタ情報を取得
         $meta   = stream_get_meta_data($tmp_fp);

         //メタ情報からURI(ファイルのパス)を取得
         return $meta["uri"];
     }
}
```

Blade側ではUrlメソッドを用いて画像ファイルのURLを取得して表示しています。

>[Laravel7.x :ファイルのURL](https://readouble.com/laravel/7.x/ja/filesystem.html)
>urlメソッドを使用し、特定のファイルのURLを取得できます。s3ドライバーを使用している場合は、完全修飾リモートURLが返されます。

```php:plofile.blade.php
<img src="{{ Storage::disk('s3')->url("avatars/$user->avatar_file_name")}}">
```
##エラー内容（ローカル開発環境）
###【事象】
画像投稿後に画像表示ページにアクセスすると正しく表示されない。
以下の画像の左側のように画像が読み込まれない状況。
![スクリーンショット 2021-08-17 10.29.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/096e0588-d3a3-ab25-5152-bfc6c9b5a239.png)
###【考えたこと】
エラー解決にあたり手当たり次第探し出すのではなくまずは仮説を持って目星をつけるようにしました。※私自身まだ学習中の身であるため、そもそもの目星の立て方がおかしいという点はご容赦ください・・・。

**①Debugbar/laravel.logに何かしらエラーが吐き出されているか？**
この場合であればcontroller側に何かしらの設計ミスがある可能性があります。

　→エラー表示なし

**②S3側には画像ファイルが保存されているか？**
保存されていなければドライバー設定をするfilesystems.phpの記述漏れ

　→S3側には正しく保存されている！！（マネジメントコンソールで確認）
![スクリーンショット 2021-08-17 10.31.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/94b35406-ed3a-dbc6-4bcb-aa7685a30ecd.png)

**③View側の記述は正しいか？**
①と②の可能性が消えたのでおそらくview(Blade)側の記述ミスだとあたりをつけました。
###【結果】
ものすごく初歩的なミスでした。。
仮説通り、view側で画像を読み込む記述が間違っていたようです。
具体的にはimgタグ内のsrc属性に記述していた画像ファイルパスのURIの指定がローカルストレージのままでした。

```php:plofile.blade.php
//誤り
<img src="/storage/item-images/{{$item->image_file_name}}">

//正解
<img src="{{ Storage::disk('s3')->url("avatars/$user->avatar_file_name")}}">

```
##エラー内容（本番環境）
###【事象】
まず前提ですがAWSのEC2上にLaravelアプリケーションを動かしていました。
・AWS（EC2）
・Nginx

今回発生した事象としては上述の開発環境の時とは少し毛色が違い以下の内容でした。
・ログイン/ログアウト画面等、画像が関わる処理以外のページは正常に作動する
・画像の取得や投稿を行うメソッド（GET/POST）を実行すると以下の404ページが表示される。
![スクリーンショット 2021-08-21 15.05.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/a9f0ae7f-3d55-4075-b7ca-b4b36811b02e.png)
###【考えたこと】
開発環境でのデバッグ対応と同様にまずは考えられうる可能性を紐解いていきました。今回初めて本番環境でのデバッグ対応だったので『ググって解決できそうな内容を手あたり次第コマンドを打つ』ことだけは絶対にしないようにしました。根拠があるか、影響はなさそうかを少しずつ検証しながら進めていきました。

**①本番環境における.envファイル記載漏れはないか？**
当初は一番最初にこれが候補として上がり自分の中でも90%近い確度で主要因と思っていました。背景としては本番環境での.envファイルにS3にアクセスするため環境変数の設定を記述し忘れていたからです。

無事.envファイル更新してキャッシュもクリアしたので表示できる！！！と思っていた矢先でした。

→404 Not found 

なぜ・・・・。。

**②nginxのログに何かヒントはあるか？**
本番環境側のwebサーバー、もしくはLaravelに何かしらのエラーが発生していると予想しました。EC2にアクセスしてターミナルで以下コマンドを叩き、まずはnginx側のログファイルに何かヒントが隠されていないか確認してみることとしました

```
sudo nano /var/log/nginx/error.log
```

結果は・・・。

```
2021/08/17 06:02:54 [error] 22609#0: *20 open() "/home/xxx/xxx/xxx/public/50x.html" failed (2: No such file or directory), client: xxx.xx.xxx.xxx, server: _, request: "POST /xxxx/xxxx HTTP/1.1", upstr$
```

こちらも手あたり次第ググって貼り付けまくるのではなく可能な限り読み解いて目星をつけます。

初めてのnginxでのエラーログで完全に意味がわからない状態でしたが、恐れず少しずつわかる範囲で仮説を立ててみました。

```
open() "/home/xxx/xxx/xxx/public/50x.html" failed
```

```
No such file or directory
```

```
client: xxx.xx.xxx.xxx, server: _, request: "POST /xxxx/xxxx HTTP/1.1", upstr$
```

何となくですがこう読みときました。

①50x.htmlというファイルを開こうとしたら失敗した。
②そんなファイルやディレクトリは存在しないよ。
③xxxxのクライアントからHTTPのPOST形式でリクエスト出した。

そして、さらにここから読み解いた一つの仮説はこうです。

**上記で表示された404エラー（リンク先のページや、検索結果を開いた先にページが存在しない場合に表示される）というのは画像ファイルが存在しないのではなく『50x.html』が存在しないのではないか？？**

更新日が古かったのですがこちらの記事を拝見したところ以下の部分でnginx側で発生したエラーページの制御を行なっているようでした。

>[Nginxの設定で押さえておきたいポイント]
(https://www.wakuwakubank.com/posts/611-web-server-nginx/)

```/etc/nginx/nginx.conf
error_page 404 /404.html;
location = /40x.html {
  }

error_page 500 502 503 504 /50x.html;
location = /50x.html {
  }
```

当該ファイルに記載されている内容を把握し切るのは現段階での知識レベルでは困難でしたが分かったことがありました。
・500エラーが発生したためnginx側で50x.htmlを表示しようとしたが見つからなかった
・上記を受けて404ページを表示した

恐らくですが以下の流れと推測しました。
①ユーザー（今回は私）がブラウザで画像を表示or投稿(get/post)を行う
↓
②EC2上のwebサーバー（nginx）が受け取り、アプリケーション側（PHP/Laravel）に処理をリクエスト
↓
③アプリケーション側が結果をwebサーバー側に返す
↓
④webサーバーがユーザーに結果を返す

つまり、②と③の間で処理がうまくいかなかった結果、nginxには500エラーが返された可能性が高いです。となると真犯人はアプリケーション側にありそうとその段階で予想しました。

**③本番環境のLaravelのエラーログを確認**

```
[2021-08-17 15:02:20] production.ERROR: GD Library extension not available with this PHP installation. {"userId":1,"exception":"[object] (Intervention\\Image\\Exception\\NotSupportedException(code: 0): GD Library extension not available with this PHP installation. at /home/webapp/matching-doula/matching-doula/vendor/intervention/image/src/Intervention/Image/Gd/Driver.php:19)
[stacktrace]
```

ここで初めて今までで一番具体的な『GD Library』というワードが登場しました。
以下のようにPHPでは画像処理等を行うには『GD Library』が必要であり、本番環境でのPHPでは当該ライブラリの拡張機能が利用できない状況のようでした。

>イメージ関数を使用するには、PHP を GD ライブラリとともにコンパイルしなければなりません。 使用したいイメージ形式によっては、GD と PHP 以外に他のライブラリも必要となる可能性があります。
[PHP公式ドキュメント:GD]
(https://www.php.net/manual/ja/intro.image.php)

>GDとはPHPで、画像の操作や作成を行うためのライブラリ。
[[PHP] AWS EC2(Amazon Linux)にGDをインストールする]
(https://agohack.com/php-gd-aws-install/)

>＊同じ問題に遭遇した方へyum list | grep gdなどでパッケージ名を確認し、自分のversionにあったパッケージをinstallしましょう。
[GD Library extension not available with this PHP installationエラー]
(https://teratail.com/questions/152973)

###【結果】
PHPで画像の作成や修正を行うためにはGDというライブラリが必要であり、本番環境でのPHPのバージョンではGDライブラリの拡張機能が利用しきれないようでした。

対応としては以下コマンドで現在ダウンロードされているPHPとインストール可能なパッケージ確認を確認します。

```
sudo yum list | grep gd
```

結果：

```
php-gd.x86_64v 7.4.21-1.amzn2 amzn2extra-php7.4
```

GDパッケージの再インストール：

```
sudo yum install php-gd.x86_64
```

成功！！！！！！！！！！！！！！
![スクリーンショット 2021-08-21 16.36.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1824740/c68e5b2c-4964-73fd-8afa-d1ee0902494a.png)

#まとめ
長文駄文大変失礼いたしました。
Laravel側の設定を少し修正するだけでS3を利用っできると伺っていましたが、本番環境でのエラーはかなり複雑なものとなってしまいました。

また、本番環境側でのデバッグではwebサーバー（nginx）、アプリケーション（PHP-FPM/Laravel/PHP）等の仕組みや役割を意識しながら対応していく必要性も実感いたしました。

自分が不慣れな技術や仕組みのデバッグでも可能な限り課題を細分化し、わかる範囲で仮説を立てつつ対応していくことが解決までの最短で進めることが重要ですね。

引き続き頑張っていきます。