Step Functions から ECS Task を呼びだすときに、エラーハンドリングを実装してみた

# はじめに

システムの中で、1日1回シェルスクリプトを動かしてバッチ処理を行うときがあります。1台の EC2 上で完結するくらいの処理量であれば、それほど問題になることはありませんが、処理量が多くなってきたときに分散処理をしたくなります。1台の EC2 で完結していたときは、バッチファイルの中でエラーハンドリングを行っていましたが、分散処理をするときは他の仕組みが必要です。こういった時に、便利に活用できるのが、AWS Step Functions です。これは、ワークフローを組み立てて、実行が出来るため、分散されたバッチ処理環境の中でエラーのハンドリングなど、適切に管理ができます。

今回の記事では、バッチ処理のワークフロー管理として、Step Functions を動かしてみる記事になります。Step Functions から ECS Task を呼びだし、エラーハンドリングを行ってみましょう。



# 環境について

[以前の記事](https://qiita.com/sugimount-a/items/31d03c17cc2055502fca)で作成した Step Functions と ECS の環境を利用します。



# Step Functions の State Machine でエラーハンドリング

編集対象の State Machine を選択して、Edit を押します

![image-20220319200858041.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/ecc66cfc-307a-2e7c-9364-43dd9e9369e6.png)



Workflow Studio を選択して、グラフィカルな編集画面を開きます。

![image-20220319200933990.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8fdc452c-05f6-09c6-b6d8-05905572d1c8.png)



Workflow Studio の画面が開きました

![image-20220319201927170.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8d0291ab-1f4d-74e1-78ef-aa6a503c3e8b.png)



まず、正常終了時には Success のアクションにたどり着きたいので、アイコンを置きます

![image-20220319202049242.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/06ccd7f1-c70e-e853-5c87-e5434eeb2f42.png)



次に、ECS の RunTask でエラーになったときの動作を指定していきます。エラーハンドリングを行いたいアクションを選択して、Error handling の画面を開きます。

![image-20220319202341365.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/0b517b4d-3d75-10b4-7d18-882770a336fd.png)



この画面には、「**<u>Retry on errors</u>**」と「**<u>Catch errors</u>**」の2種類のハンドリングのタイプがあります。

- Retry on errors : エラーが発生したときに、そのアクションをリトライする。リトライを繰り返すと、指定した時間分の時間間隔が伸びていく。
- Catch errors : エラーが発生したときに、特定のアクションでリカバリーを行う。Retry on errors と Catch errors を両方指定した場合は、Retry on errors を優先で処理する。リトライしてもエラーが出続ける場合は、Catch errors の処理に進む。

次の URL に詳細が記載されてあります : https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html#error-handling-fallback-states



今回は、Catch errors の設定を進めていきましょう。Catch errors の個所で、Add new catcher を選択します。

![image-20220319203250669.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/9850a23a-e9cf-418e-648c-36dbdc80c025.png)



設定画面が表示されました。主な3つの設定項目があります。

- Errors : キャッチするエラーのタイプを指定。指定可能なエラーのタイプは[ドキュメント](https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html#error-handling-error-representation)に記載
- Fallback state : エラーをキャッチしたあとに、どのアクションに遷移するか指定
- ResultPath : 次のアクションに遷移する際に、エラーの詳細を次のアクションに渡すときの ResultPath を指定

![image-20220319203411893.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/737f614b-5c2c-fd15-c23b-1e9053fe19e2.png)



Errors には、`States.TaskFailed` を選択して、実行時のエラーを拾います。

![image-20220319204642279.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/903f1ac5-9180-9a9d-26a6-b49c198b6075.png)



Fallback state で `Add new state` を選択して、新たなアクションを指定できるようにします。

![image-20220319204722653.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/f2c78f2b-cfef-6ecd-6fe8-ccc028cadd93.png)



新たな枠が現れました

![image-20220319205218960.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/d3c1477e-5a08-aa8c-9915-1aaa58cb6ca3.png)



今回は、エラー時に Lambda を動かしてエラーの詳細情報を CloudWatch Logs に出力をしていきます。

![image-20220319205305660.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/67827ec6-626d-4517-535d-4a9a82954503.png)



実行する Lambda Function の ARN を指定します。Payload は、state input をそのまま渡すことにします。

![image-20220319205929604.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/dbb12323-b6e7-4264-ea3b-8781819f2928.png)



なお、この Lambda は、受け取った Event をそのまま `logger.info(json.dumps(event))` としており、CloudWatch Logs に出力する実装になっています。

```python
import json
from logging import getLogger, INFO

logger = getLogger(__name__)
logger.setLevel(INFO)

def lambda_handler(event, context):
    print("============ logger.info の出力 ============")
    logger.info(json.dumps(event))

    return {
        "statusCode": 200,
        "body": json.dumps({
            "message": "hello",
        }),
    }
```



右上の Apply and exit を選択します。

![image-20220319210013867.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/88fc4dbb-8878-c3e4-ea64-d0c4d9b603e3.png)



Save を押します

![image-20220319210042595.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/90cdc8c4-798c-8369-f89f-e405c60c7327.png)



Start execution を選択します

![image-20220319210133341.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/98607753-8139-ce6f-26bc-0d9eccd2c462.png)



State Machine が実行されます。

![image-20220319211723529.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/7783a533-fd46-b907-b14e-46266cece569.png)



ECS Task を実行の結果、エラーとなり Lambda が起動していることがわかります。

![image-20220319212317361.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/eedccd55-5e49-0d58-b18f-2bf7b1dc3015.png)



Step Functions から Lambda を起動するときの Payload を確認すると、Cause にエラーの詳細情報がわたっていますが、JSON を文字列として扱っていることがわかります。「CloudWatch Logs」のリンクをクリックして、CloudWatch 側で受け取ったログを確認します。

![image-20220319212554011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8832d34b-09ff-bdd1-9959-9ce871026112.png)



CloudWatch Logs 上でも、1個の文字列としてやってきました。

![image-20220319212915284.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/082e623e-0deb-232f-6540-08bab25a344a.png)



# エラーメッセージを整形

上記のように、一個の文字列でもエラーの詳細を確認できますが、検索など活用がしにくいため文字列から JSON に変換をしていきます。

State Machine の Workflow Studio を開きます。

![image-20220319213115000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/17608bba-3305-ba26-852a-d2f31f834855.png)



Lambda の Payload に渡す前に、JSON に変換を行いたいです。Lambda の前に Pass アクションを追加します。

![image-20220319213217928.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b0460c95-33ca-5a8e-c139-d73c9fe857fa.png)



Pass の Input から、次のように設定をいれて、JSON に変換を加えます。

![image-20220319213303525.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/e64bf0ee-bc81-fb35-501f-bb12eee370d2.png)

```
{
  "Cause.$": "States.StringToJson($.Cause)"
}
```



なお、この指定方法については 次の Document に記載があります。

URL : https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html#error-handling-fallback-states



Apply and exit を押します。

![image-20220319213511382.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/0176c8df-1907-e715-4d61-a8550b884648.png)



Save を押します

![image-20220319213541042.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/7018780a-53a1-c8a9-0e0e-9b4a27cc26d8.png)



Start Execution を選択して、実行確認してみましょう。

![image-20220319213611788.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/3baa03f2-00d0-07c0-27a8-5dd2181607fc.png)



実行が完了しました

![image-20220319213734321.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8035f8a7-751d-0b9f-802f-6b36cd4085e3.png)



Lambda を実行しているときの Payload が JSON に変換されていることがわかります。

![image-20220319213825692.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/5648c834-8fa7-239f-d83a-479b246fc241.png)



CloudWatch Logs 上でも、整形されていますね。

![image-20220319213913136.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fb0569ba-27e2-cf8e-56ae-bba342e4061b.png)



この Log の中には、`StoppedReason` の値もあり、停止した理由がわかります。

![image-20220319214002169.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/ae2f59f9-954e-87ea-c0c4-adce966f4fe0.png)



また、ExitCode も取得できています。

![image-20220319214059072.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6ab189ff-4036-2fe9-dc10-15f974c370dc.png)

# わかったこと

- ECS Task の ExitCode が 0 以外の場合は、Step Functions 上でエラーとして扱ってくれる

- ECS Task の実行がエラーになったときに、Cause にエラーの詳細メッセージが表示されるが、String 型となっており、後続の処理がやりにくい。Pass アクションを中継して、String を JSON に変換することで、後続の処理をやりやすくできる

  - ```
    {
      "Cause.$": "States.StringToJson($.Cause)"
    }
    ```





# 参考URL

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/concepts-error-handling.html