[AWS Step Functions] ステートマシンが無限ループして148ドルも課金が発生した話

雑なEventBridgeのイベントパターンを設定したことを後悔しています。
2022.04.27

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

懺悔します

こんにちは、のんピ(@non____97)です。

私はここに「ステートマシンを無限ループさせて148ドルも課金が発生してしまった」ことを懺悔します。

いきなりまとめ

  • 検証だからといって雑なEventBridgeのイベントパターンを設定するのはやめよう

何が起こったか

ステートマシンが無限ループして148ドルも課金が発生しました。

ここで、クラスメソッドメンバーズ向けポータルサイト「クラスメソッド メンバーズポータル」で、AWS利用料金を確認してみましょう。

まずは明細です。

利用料金の明細

$148.67のインパクトが凄まじいですね。

USE1-StateTransitionAPN1-StateTransitionについての課金が大半を占めています。AWS Step FunctionsのStandardワークフローではステートマシンの状態遷移によって課金が発生します。

今回はus-east-1ap-northeast-1合わせて5,946,978回の状態遷移が発生したことが分かります。

続いて日別の料金を確認します。

日別のご利用料金

4/25に爆発的な課金が発生しています。

料金の累積表示をすると、いかにとんでもないことが起きたのかがよく分かります。

累積ご利用料金

メンバーズポータルではサービス毎の利用料金の比率も確認できます。

サービス毎のご利用料金の比率

4月の料金のうち、AWS Step Functionsについての料金が占める割合は86.8%です。圧倒的過ぎて円グラフを見たときに思わずニヤけてしまいました。ニヤける場合じゃないんですけどね。はい。

年間の利用料金の推移も確認しましょう。

よくもまぁ...といった感じです。

なぜ起こったか

EventBridgeのイベントパターンが雑すぎたためです。

具体的には、イベントパターンをすべてのイベントにしてしまいました。

マネージメントコンソールですべてのイベントを選択すると、以下のような警告が表示されます。

すべてのイベント

イベントソースとして [すべてのイベント] を選択すると、EventBridge はこのイベントバスに届くすべてのイベントをこのルールに送信します。
これにより、ターゲット呼び出しの数が非常に多くなり、追加コストが発生する可能性があります。
さらに、ルールが繰り返しトリガーされる無限ループにつながるルールの作成が可能となっています。
これを防ぐには、トリガーされたアクションが同じルールを再トリガーしないようにルールを作成することをお勧めします。
例えば、サービスに対するあらゆる変更の後ではなく、そのサービスが不正状態にあることが判明した場合にのみ、ルールをトリガーできます。

それでは、私はなぜ、このような過ちをしでかしてしまったのでしょうか。

私は当時以下の記事の検証をしていました。

検証の内容は以下の通りです。

  • Lambda関数からグローバルエンドポイントにuuidと現在のタイムスタンプをPutEventsする
  • グローバルエンドポイントの状態に応じて、プライマリリージョン(us-east-1)、もしくはセカンダリリージョン(ap-northeast-1)のカスタムイベントバスとEventBridgeルールを介してステートマシンを実行する

構成図

上述の記事にも記載していますが、グローバルエンドポイントは2022/4/25時点ではカスタムイベントのみに対応しています。

そのため、「どうせカスタムイベントバスに送信するし、EventBridgeルールのイベントパターンは[すべてのイベント]で良いだろう」という判断をしました。

ここで私の運命は決まりました。

検証の環境は一部を除いてAWS CDKで定義しています。

本来、カスタムイベントバスに何かイベントがあった時は、以下のようにEventBridgeルールでイベントバスを指定する必要があります。

./lib/global-endpoints-stack.ts

// State Machine that is the target of the Event Bridge Rule
const stateMachine = new sfn.StateMachine(this, "StateMachine", {
  definition: new sfn.Pass(this, "Pass"),
});

// Event Bridge Rule for Global endpoints
new events.Rule(this, "GlobalEndpointsRule", {
  eventBus: globalEndpointsEventBus,
  eventPattern: {
    account: [Stack.of(this).account],
  },
  targets: [new targets.SfnStateMachine(stateMachine)],
});

しかし、私は何を思ったのかカスタムイベントバスの指定を忘れてしまいました。

./lib/global-endpoints-stack.ts

// State Machine that is the target of the Event Bridge Rule
const stateMachine = new sfn.StateMachine(this, "StateMachine", {
  definition: new sfn.Pass(this, "Pass"),
});

// Event Bridge Rule for Global endpoints
new events.Rule(this, "GlobalEndpointsRule", {
  eventPattern: {
    account: [Stack.of(this).account],
  },
  targets: [new targets.SfnStateMachine(stateMachine)],
});

これにより、「このAWSアカウントで何かイベントがあったらステートマシンを実行する」という無限ループウェルカムなEventBridgeルールが出来上がってしまいました。

結果として以下のような無限ループが発生しました。

  1. ステートマシンを実行する
  2. 「ステートマシンが実行された」というイベントがデフォルトのイベントバスに送信される
  3. EventBridgeルールに従ってステートマシンを実行する
  4. 「ステートマシンが実行された」というイベントがデフォルトのイベントバスに送信される
  5. EventBridgeルールに従ってステートマシンを実行する

もっと言うと、「ステートマシンの実行が成功/失敗した」というイベントもデフォルトのイベントバスに送信されるので、溢れんばかりのイベントがEventBridgeルールとステートマシンを酷使します。

また、悪いことは重なるもので、グローバルエンドポイントのイベントレプリケーションを有効化していました。そのため、セカンダリリージョンでも同様の無限ループが発生していました。

StartExecution APIConsumedCapacity(1 秒あたりのリクエストの数)の合計を確認すると、以下のように桁外れのリクエストが飛んでいることが分かります。

StartExecution APIのConsumedCapacityの合計

今回はたまたまステートマシンの実行履歴を見たら、実行回数が2000+となっていたので、気づけられました。

こちらのメトリクスについてCloudWatchアラームを作成して、Slack通知するようにしておくと異常状態に素早く気づけられそうです。

併せて定期的にAWSの利用料金を確認しておくと良いと考えます。

検証だからといって雑なEventBridgeのイベントパターンを設定するのはやめよう

ステートマシンが無限ループして148ドルも課金が発生した話を紹介しました。

ちなみに、無限ループが発生してから解消するまでの時間は1時間20分です。たった1時間20分で2万円近くの課金が発生するのです。皆さんは雑なEventBridgeのイベントパターンはやめましょう。

試しにCloudTrailで該当の時間のStartExecutionイベントをダウンロードしようとしたところ、ダウンロードに失敗しました。

CloudTrail

行き場の無いこの気持ちを発散するために、雑なイベントパターン設定するな高校校歌を作りました。

雑なイベントパターン設定するな高校校歌
作詞作曲 のんピ

雑なイベントパターン設定するな 雑なイベントパターン設定するな

「すべてのイベント」は絶対設定するな 気づいた時には手遅れ

無限ループ and 無限ループ

迫り来る課金の嵐 素早く対処しろ

ああ我ら 雑なイベントパターン設定するな高校

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!