S3にファイルをアップロードしようとしたらPermission deniedされた[Java]

お仕事でハマった件について、解消に至る流れを記載します。先に教訓を書いて、以降問題解決に至るまでのプロセスを記載します。 なお解決したのは主に同僚だった模様。

教訓

  • スタックトレースを出すべし
  • ファイルに書き出さなくていいならインメモリで扱うべし
    • 一度インスタンスにしたFileを読み出し(書き込み?)しようとすると、permission errorが発生するかも。

事象

Webアプリの管理ページからファイルをS3にアップロードしようとするとエラーが発生します。
エラーメッセージは以下のみ。
java.io.FileNotFoundException: ファイル名 (Permission denied)

対処

スタックトレースを出す

エラーメッセージだけだとAWS側の設定に問題があるのかサーバー側のコードに問題があるのか分からないので、詳細なスタックトレースがほしいところです。

ログの出力はlog4jのLoggerを使っていました。
log4jのerrorメソッドは引数が(Throwable e)だけだとスタックトレースが表示されませんが、(String message, Throwable t)だと表示されるようになります。

これをもとに、ソースを以下の通り書き換えました。

  • 修正前
// 中略
catch (Exception e) {
    logger.error(e);
}
  • 修正後
// 中略
catch (Exception e) {
    logger.error("An error occurred during rendering an administrator page.", e);
}

補足として、log4jjavadocを取り上げます。

Logger (Apache Log4j API 2.11.1 API)

以下のerror(String message, Throwable t)を用いることで、スタックトレースを出力できるようになります。

error(String message, Throwable t)
Logs a message at the ERROR level including the stack trace of the Throwable t passed as parameter.

一方、Throwable t のみを引数にしたメソッドはなく、修正前は、error(Object message)が該当していて、スタックトレースが出力されていなかったようです。

Permission denied エラー解決

スタックトレースを見ると、一番下に以下の記載がありました。*1

Caused by: java.io.FileNotFoundException: ファイル名(Permission denied)
    at java.io.FileOutputStream.open0(Native Method) ~[?:1.8.0_171]
    at java.io.FileOutputStream.open(FileOutputStream.java:270) ~[?:1.8.0_171]
    at java.io.FileOutputStream.<init>(FileOutputStream.java:213) ~[?:1.8.0_171]
    at java.io.FileOutputStream.<init>(FileOutputStream.java:162) ~[?:1.8.0_171]
    at fuga.hoge.S3FileStorage.upload(S3FileStorage.java:32) ~[***.jar:?]
    at fuga.hoge.BaseFileService.upload(BaseFileService.java:33) ~[***.jar:?]
    ... 89 more

S3FileStorage.javaが我々が作成したソースです。これを見ると、32行目は以下の行に相当します。

  • 修正前
@Override
public void upload(@NonNull String filename, @NonNull byte[] contents) throws IOException {
    File file = new File(filename);
    try (FileOutputStream fos = new FileOutputStream(file)) {  // ←ここ!!!!
        fos.write(contents);
        s3Client.putObject(Constants.S3_BUCKET_NAME, filename, file);
    }
}

try-with-resourceの冒頭で落ちている事がわかります。 作成したファイルインスタンスを操作できないのが原因とみられるので、ファイルではなくバッファー配列を作成しました。

  • 修正後
@Override
public void upload(@NonNull String filename, @NonNull byte[] contents) throws IOException {
    ObjectMetadata metadata = new ObjectMetadata();

    try (ByteArrayInputStream is = new ByteArrayInputStream(contents)) {
        s3Client.putObject(Constants.S3_BUCKET_NAME, filename, is, metadata);
    }
}

これで無事アップロードできるようになりました。
同僚曰く、File()はディスク上でファイルを作るからアクセスできないという問題が生じるんですよと言っていました。

振り返り

最初のエラーメッセージを見た時にAWSの問題かなと思ってうろたえていたのですが、冷静に振り返るとFileNotFoundExceptionなのでS3は関係ないんじゃないかと気づくべきでした。

もちろん、スタックトレースは出すべきで、それによって初めて原因箇所が特定できました。

そして最後の修正ですが、ファイルはS3にアップロードするのが目的でサーバーにとっておく必要がないので、そのような場合ではFileのようにディスクに書き込む方法ではなくインメモリで扱う方法がいいでしょう。

*1:JavaスタックトレースはCaused by hogehogeと続いていくので一番下から読んでいくと根本原因にたどり着けることが多い