fluentdでS3に転送しているならば、無駄なGETリクエストに気をつけろ！

# 概要

FluentdでS3にログ転送している場合で、以下の条件に当てはまると無駄なS3のGETリクエストを消費することがあります

- `check_object`が`true`（指定なしはtrue）
- `s3_object_key_format`に`%{index}`を含めている  
  S3に出力される際に同名のファイルがある時に自動的に枝番を付与される  
![brunch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30522/9ebcb222-3ec0-0c89-fab0-c6244d728f6b.png)

S3のGETリクエスト単価(`$0.0037/10,000リクエスト`)は安いので、なかなか無駄に気づかないことが多い
私も気づくまでに1年かかりましたｗ
ただ、無駄なものは無駄なのできちんと設定して無駄を無くしましょう！

# 正しい設定

- `check_object`を**false**
- `%{index}`を利用せずに出力する**ファイル名が必ず一意**になるように`s3_object_key_format`を指定する  
  ファイル名に秒まで含めるのが良いでしょう  

※[fluent-plugin-s3のリファレンス](https://github.com/fluent/fluent-plugin-s3)
※時間フォーマットの詳しい設定は[こちら](https://docs.fluentd.org/configuration/format-section)を参照

```config:fluent.conf

<match pattern>
  @type s3

  path logs/
  
  # 存在確認を無効
  check_object false

  # hms_sliceは時間
  s3_object_key_format %{path}%{time_slice}_%{hms_slice}.log

  # 日時（2020-02-18）
  time_slice_format %Y-%m-%d

</match>
```

※なお、バージョンによっては副作用として上記の`s3_object_key_format`と`check_object`を併用するとfluentdログに`WARN`が記録されます

# なぜ

前述の条件によりS3に同一ファイル名が存在する場合は、自動的に枝番を付与してくれます
ただし、枝番を採番する際に都度GETリクエストを発行して枝番ごとにファイルの存在確認していく処理になっている

実際にCloudTrialログを添えて、流れを追っていきます

1. S3に`2020-02-19.log_0`と`2020-02-19.log_1`がある前提
2. さらに同名のログ（`2020-02-19.log`）をPUTする
3. `2020-02-19.log_0`をGETリクエスト(HeadObject)する
4. エラーがなかったので、存在すると判定し、`2020-02-19.log_1`でGETリクエストする

    ```json:CloudTrialログ
    {
      "eventVersion": "1.06",
      "eventTime": "2020-02-19T09:12:01Z",
      "eventSource": "s3.amazonaws.com",
      "eventName": "HeadObject",
      "awsRegion": "ap-northeast-1",
      "requestParameters": {
        "bucketName": "hogehoge",
        "Host": "hogehoge.s3.ap-northeast-1.amazonaws.com",
        "key": "logs/2020-02-19_0.log"
      },
      "responseElements": null,
      "readOnly": true,
      "resources": [
        {
          "type": "AWS::S3::Object",
          "ARN": "arn:aws:s3:::hogehoge/logs/2020-02-19_0.log"
        },
        {
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::hogehoge"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false
    }
    ```
5. 同様にエラーがなかったので、存在すると判定し、`2020-02-19.log_2`でGETリクエストする

    ```json:CloudTrialログ
    {
      "eventVersion": "1.06",
      "eventTime": "2020-02-19T09:12:01Z",
      "eventSource": "s3.amazonaws.com",
      "eventName": "HeadObject",
      "awsRegion": "ap-northeast-1",
      "requestParameters": {
        "bucketName": "hogehoge",
        "Host": "hogehoge.s3.ap-northeast-1.amazonaws.com",
        "key": "logs/2020-02-19_1.log"
      },
      "responseElements": null,
      "readOnly": true,
      "resources": [
        {
          "type": "AWS::S3::Object",
          "ARN": "arn:aws:s3:::hogehoge/logs/2020-02-19_1.log"
        },
        {
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::hogehoge"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false
    }
    ```
6. エラー（`"errorCode": "NoSuchKey"`）が発生したので、存在しないと判定し、`2020-02-19.log_2`でPUTリクエストする

    ```json:CloudTrialログ
    {
      "eventVersion": "1.06",
      "eventTime": "2020-02-19T09:12:02Z",
      "eventSource": "s3.amazonaws.com",
      "eventName": "HeadObject",
      "awsRegion": "ap-northeast-1",
      "errorCode": "NoSuchKey",
      "errorMessage": "The specified key does not exist.",
      "requestParameters": {
        "bucketName": "hogehoge",
        "Host": "hogehoge.s3.ap-northeast-1.amazonaws.com",
        "key": "logs/2020-02-19_2.log"
      },
      "responseElements": null,
      "readOnly": true,
      "resources": [
        {
          "type": "AWS::S3::Object",
          "ARN": "arn:aws:s3:::hogehoge/logs/2020-02-19_2.log"
        },
        {
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::hogehoge"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false
    },
    {
      "eventVersion": "1.06",
      "eventTime": "2020-02-19T09:12:02Z",
      "eventSource": "s3.amazonaws.com",
      "eventName": "PutObject",
      "awsRegion": "ap-northeast-1",
      "requestParameters": {
        "bucketName": "hogehoge",
        "Host": "hogehoge.s3.ap-northeast-1.amazonaws.com",
        "key": "logs/2020-02-19_2.log"
      },
      "readOnly": false,
      "resources": [
        {
          "type": "AWS::S3::Object",
          "ARN": "arn:aws:s3:::hogehoge/logs/2020-02-19_2.log"
        },
        {
          "type": "AWS::S3::Bucket",
          "ARN": "arn:aws:s3:::hogehoge"
        }
      ],
      "eventType": "AwsApiCall",
      "managementEvent": false
    }
    ```
という流れになっています

なので**枝番が増えれば増えるほどGETリクエスト数も`1+2+3+4+5･･･`と増えていきます**
ファイル名が被らなくても`check_object`が有効な場合は、必ず1回はGETリクエストが発生するので、falseを検討してもよいかもしれない
万が一に上書きの心配があるならば念のために有効のままにしておくのもいいかもしれない

ちなみに自社のS3の請求欄です
S3にはログしか保存していないのにもかかわらず、GETリクエストがPUTの25倍もある
幸い単価が安いので助かっています( ;∀;)

![s3_cost.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/30522/dc535832-d1b8-f87c-2684-b495abbc6924.png)

# 検証用

検証用にサンプルコードを作成したので、そちらで簡単に動作確認ができるかと思います
手順はReadmeを参照ください
https://github.com/comefigo/fluentd-s3

CloudTrial以外にもバケットのメトリックス（CloudWatchの有料メトリックス）でリクエスト数の確認もできますので、必要であればご確認ください