Amazon EventBridge API Destinationを使って外部API呼び出しを非同期的かつレートリミット内に収まるように行う(Slackで実装)

この記事は[PTAのアドベントカレンダー](https://adventar.org/calendars/7539)24日目の記事です。

# 概要
- [EventBridgeのAPI Destination](https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-api-destinations.html)という機能を使うと、外部API呼び出しを非同期的に実行できる。
- APIのレートリミット内に収まるように自動的に調整してくれる。
- SlackのAPIで実験してみました。

# EventBridge API Destinationとは
EventBridge API Destinationは、EventBridgeのターゲットにHTTP APIの呼び出しを指定できる機能です。

以下の特徴があります。
1. 自動リトライ
1. DLQにも対応
1. 3種類の認証方式に対応
1. APIのレートリミット内に収まるように自動的に調整

# 実装
## 全体像

今回は実験のために以下を実装します。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/18aa3a7a-888e-7f21-dc8e-a60b6d4f0b76.png" alt="drawing" width="700"/>

実装・設定するものは以下です。

1. SlackのWebhook URLを取得
1. EventBridgeのAPI Destination・接続を作成
1. EventBridgeのイベントバスを作成
1. EventBridgeのルールを作成
1. EventBridgeのイベントを発行するコードを実装

## SlackのWebhook URLを取得
[こちら](https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8)を参考にSlackのWebhook URLを取得します。

## EventBridgeのAPI Destination・接続を作成
#### 1. 接続を作成

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/1d0ec8e9-3341-0954-710f-c607b1bdce6e.png" alt="drawing" width="200"/>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/4b677711-1d6d-04b2-e20e-2374b8d04c60.png" alt="drawing" width="700"/>

※今回はWebhook URLを使用するので認証は不要なのですが、必須なので適当に設定しました。ただ、本当はSlackアプリを作成してOAuth Tokenを取得し、認証を設定した方が良い気がします。

#### 2. API Destinationを作成
API Destination(API 送信先)を作成します。
- `API 送信先エンドポイント`の欄にWebhook URLを貼り付けてください。
- SlackのIncoming webhooks APIのレートリミットは1秒に1リクエストまでなので、`1秒あたりの呼び出しレート制限`の欄に1と入力します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/6eceff19-4ac4-4abe-db0b-925ecea1a921.png" alt="drawing" width="800"/>

## EventBridgeのイベントバスを作成

![スクリーンショット 2022-12-24 18.49.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/63f4cb53-5a27-95ce-9a14-3902ec2eb0eb.png)

## EventBridgeのルールを作成

1. 上記で作成したイベントバスを指定し、ルールを作成
![スクリーンショット 2022-12-24 18.50.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/945c718c-d752-7296-3289-5aea674b8930.png)

2. イベントパターンは以下のようにします。
![スクリーンショット 2022-12-24 18.54.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/12cd2534-8593-24a1-e3b9-ab0d4661b4d9.png)

3. ターゲットを設定します。

- `ターゲットタイプ`に`EventBridge APIの宛先`を指定し、先程作成したAPI送信先を指定してください。

![スクリーンショット 2022-12-24 19.03.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/c77124d5-7725-5d27-fa36-9d01168aa73b.png)

- `ターゲット入力を設定`で`一致したイベントの一部`を選択肢、`$.detail`と入力します。

![スクリーンショット 2022-12-24 19.03.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/f8408c06-6e01-4ee1-eb1e-771fb27d8dd7.png)

## EventBridgeのイベントを発行するコード
今回はGolangで実装しました。

```go
package main

import (
	"encoding/json"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/eventbridge"
)

type Payload struct {
	Text string `json:"text"`
}

func main() {
	payload := Payload{Text: "test-after"}
	p, err := json.Marshal(payload)
	if err != nil {
		panic(err)
	}

	sess, err := session.NewSession(&aws.Config{Region: aws.String("us-east-1")})
	if err != nil {
		panic(err)
	}

	e := eventbridge.New(sess)

	for i := 0; i < 200; i++ {
		_, err := e.PutEvents(&eventbridge.PutEventsInput{
			Entries: []*eventbridge.PutEventsRequestEntry{{
				EventBusName: aws.String("test"),
				Source:       aws.String("test.slack.post"),
				DetailType:   aws.String("detail"),
				Detail:       aws.String(string(p)),
			}},
		})
		if err != nil {
			panic(err)
		}
	}
}
```

# 実験
## Before(SlackのWebhook URLを直接呼び出す)
比較のため、EventBridge API Destinationを経由せずSlackのWebhook URLに直接リクエストを大量に送ってみます。

```go
package main

import (
	"encoding/json"
	"net/http"
	"net/url"
)

var hookURL = "" // NOTE: 今回は実験のためWebhook URLを変数に定義しましたが、危険なのでSecrets Managerなどに保管してください。

type Payload struct {
	Text string `json:"text"`
}

func main() {
	payload := Payload{Text: "test"}
	p, err := json.Marshal(payload)
	if err != nil {
		panic(err)
	}

	for i := 0; i < 200; i++ {
		resp, err := http.PostForm(hookURL, url.Values{"payload": {string(p)}})
		if err != nil {
			panic(err)
		}
		if resp.StatusCode != http.StatusOK {
			panic(resp.StatusCode)
		}

		err = resp.Body.Close()
		if err != nil {
			panic(err)
		}
	}
}
```

### 結果
レートリミットを超えるリクエストを大量に送信すると、以下のように警告文が表示された後しばらくして、429(Too Many Requests)レスポンスが返ってきます。[^1]

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/04d25665-d0ef-e86b-c82f-e1de5ef51f3f.png" alt="drawing" width="600"/>

```go
panic: 429
```

## After(EventBridge API Destinationを使って呼び出す)
### 結果
すべてのメッセージが投稿されました。
![スクリーンショット 2022-12-24 18.42.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/455240/ac86303d-fb67-9f80-30fd-30c3d3d63897.png)

# まとめ

- EventBridge API Destinationを使用することで、レートリミットを気にせずにAPIを呼び出せるようになりました。
- 厳しいレートリミットを設けているAPIを利用する際などに活躍しそうです。
- 一方で、ログが出力されない(自分が見つけられていないだけかも)ので、デバッグなどは難しくなる印象を受けました。

# 参考
- https://docs.aws.amazon.com/ja_jp/eventbridge/latest/userguide/eb-api-destinations.html
- https://slack.com/intl/ja-jp/help/articles/115005265063-Slack-%E3%81%A7%E3%81%AE-Incoming-Webhook-%E3%81%AE%E5%88%A9%E7%94%A8
- https://api.slack.com/docs/rate-limits

[^1]: SlackのIncoming webhooks APIではレートリミットを超えた場合に短期間バーストします。その際にこのメッセージが表示されるようです。