LambdaでAWS Fault Injection Service アクションを深掘りしてみた

# はじめに

昨年末、FISのアップデートに関する投稿をしました。前回の投稿では追加されたLambda用アクション3つのうち、1つだけを試していました。
今回は残り2つのアクションを試しつつ、アクションを組み合わせて、シナリオ化も試してみます。

前回の投稿は[こちら](https://qiita.com/sh_fk2/items/0def60a9813737fa4452)

https://qiita.com/sh_fk2/items/0def60a9813737fa4452

### AWS Fault Injection Service (AWS FIS)とは
　FISは、AWS上で稼働するアプリケーションの回復力を高めるためのマネージドサービスです。カオスエンジニアリングの概念に基づき、システムに意図的に障害を発生させることで、その挙動や耐性を事前に確認することができます。

https://docs.aws.amazon.com/ja_jp/fis/latest/userguide/what-is.html

## 事前準備
障害を注入するLambda関数に対して前回同様に設定を追加して、ターゲットとして登録しておきます。
（[IAMポリシーのアタッチ](https://qiita.com/sh_fk2/items/0def60a9813737fa4452#lambda%E7%94%A8iam%E3%83%9D%E3%83%AA%E3%82%B7%E3%83%BC%E4%BD%9C%E6%88%90iam%E3%83%AD%E3%83%BC%E3%83%AB%E3%81%AB%E3%82%A2%E3%82%BF%E3%83%83%E3%83%81)、[Lambda関数のレイヤー、環境変数の追加](https://qiita.com/sh_fk2/items/0def60a9813737fa4452#lambda%E3%83%AC%E3%82%A4%E3%83%A4%E3%83%BC%E7%92%B0%E5%A2%83%E5%A4%89%E6%95%B0%E8%BF%BD%E5%8A%A0)の実施をお忘れなく）

私は、前回とは異なる2つのLambda関数に追加しておきました。

それではアクションを追加していきます。


## aws:lambda:invocation-error を試してみる
前回作成した実験テンプレートを更新して、アクションを追加していきます。

### aws:lambda:invocation-error

このアクションでLambda関数の実行エラーを返せるようになります。
| パラメータ                 | 説明                                                                        |
|:-------------------------|:----------------------------------------------------------------------------|
| Duration                 | 障害が継続する時間の長さ                                                   |
| Invocation percentage     | 関数呼び出し時に障害を発生させる割合                                              |
| Prevent execution | 値がオフの場合、Lambda関数を実行して、かつエラーを返す |

https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html#invocation-error

「Prevent execution」のパラメータですが、処理としては実行できているのに、エラーが返ってくるという形が実現できます。これは更新系のトランザクションにおいて実際に本番環境で起きると結構厄介なパターンですね。こういったパターンをコードの修正なく再現できるのは、とても良いと思います。

![fis_105.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/240d8fdf-4b81-be94-1ec4-5c7fd684781e.png)


invocation_errorについては、createItemのLambda関数をターゲットにしました。

![fis_106.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/7ce7c1ac-928d-f568-4018-7e2dc5eedef2.png)

### 実験開始
アクションサマリーのステータスがRunningになっていることを確認します。
![fis_101.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/40a75f94-53b2-6180-7266-25fb5ac268d7.png)

createItemsのリクエストを投げてみます。
502エラーが返ってきました。
![fis_103.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/2598a5c7-ee37-60d7-aeaa-25697e877878.png)

```
Request Headers
    Content-Type: text/plain
    User-Agent: PostmanRuntime/7.43.0
    Accept: */*
    Postman-Token: 1fa72320-d6b5-4523-9d0d-dac0308bbb8c
    Host: xxxxxxxxxx.ap-northeast-1.amazonaws.com
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    Content-Length: 36
Request Body
    {"id":"0217","name":"FIS test 0217"}
Response Headers
    Content-Type: application/json
    Content-Length: 36
    Connection: keep-alive
    Date: Mon, 13 Jan 2025 10:19:07 GMT
    x-amz-apigw-id: EUlISG_8NjMEBrA=
    x-amzn-RequestId: 4c340c6c-f86b-40fb-804b-61972863f5dd
    x-amzn-ErrorType: InternalServerErrorException
    X-Cache: Error from cloudfront
    Via: 1.1 33adaf636d9a8b17ab166777508ba07a.cloudfront.net (CloudFront)
    X-Amz-Cf-Pop: NRT20-C3
    X-Amz-Cf-Id: wuWQCPRT-Sk6tXZhwDhV0p-Hm9Do2r3wHRsFjwUcx4odShkizyIlQA==
Response Body
    {"message": "Internal server error"}
```

CloudWatch Logsで確認してみるとLambda関数は実行されていました。
```
REPORT RequestId: 12533aeb-4200-4e65-ad2c-f26f181432e8	Duration: 566.50 ms	Billed Duration: 567 ms	Memory Size: 128 MB	Max Memory Used: 91 MB
```

DynamoDBでレコードを検索すると、レコードが作られていました。
![fis_102.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/5ab073f1-bab4-8223-6d52-5a991d97905e.png)

イメージとしては以下のような形になったと理解しました。
DynamoDBには正常にItemが作成されつつ、リクエスタ側には502エラーが返るというアンマッチ状態です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/a508e5db-33a8-c660-113d-d59f9cd7c809.png)

マイクロサービス化で複数の処理を組み合わせる場合など、実際にはもっと複雑になっているシステムばかりだと思います。リクエスタ側でユニークな値をセットしたり、トランザクションのロールバックを作り込むなどの対策が十分か、各処理を障害状態にしつつ確認していくことで、対策の妥当性確認に役立ちそうです。



## aws:lambda:invocation-http-integration-response を試してみる
続いて最後のアクションを確認してみます。

### aws:lambda:invocation-http-integration-response
このアクションでLambda関数から返すHTTPステータスコード（000-999）の値を指定できるようになります。

| パラメータ                 | 説明                                                                        |
|:-------------------------|:----------------------------------------------------------------------------|
| Content type header                 | レスポンス時のヘッダーのContent-Typeを指定します                                                   |
| Duration                 | 障害が継続する時間の長さ                                                   |
| Invocation percentage     | 関数呼び出し時に障害を発生させる割合                                              |
| Prevent execution |  値がオフの場合、Lambda関数を実行して、かつエラーを返す |
| Status Code | レスポンスするHTTPステータスコードを指定 |

https://docs.aws.amazon.com/fis/latest/userguide/fis-actions-reference.html#invocation-http-integration-response

こちらもPrevent executionの値がオフの場合、Lambda関数を実行して、かつ指定したステータスコードを返すことができます。

今回は、Lambda関数を実行しつつ、HTTPステータス500を返す形にしました。
![fis_107.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/454593fc-ee16-38a9-9392-cff61575f2d8.png)

invocation-http-integration-responseについては、updateItemのLambda関数をターゲットにしました。
![fis_108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/6048c75f-1d6c-30d7-4b00-58909e49c7e5.png)

### 実験開始
アクションサマリーのステータスがRunningになっていることを確認します。
![fis_109.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/7d7884ed-6a09-186f-9afa-1c5d506239ad.jpeg)

updateItemsのリクエストを投げてみます。（Request Bodyで```{"id": "update_complete"}```を指定）
設定した通り、ステータス500エラーが返ってきました。
![fis_110.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/1d80ee51-416f-33d0-9be4-d5ff31e17969.png)

```
Request Headers
    Content-Type: application/json
    User-Agent: PostmanRuntime/7.43.0
    Accept: */*
    Postman-Token: 9220e86a-7750-41e7-bf34-a92f58696d51
    Host: xxxxxxxxxx.ap-northeast-1.amazonaws.com
    Accept-Encoding: gzip, deflate, br
    Connection: keep-alive
    Content-Length: 25

Request Body
    {"id": "update_complete"}

Response Headers
    Content-Type: application/json
    Content-Length: 0
    Connection: keep-alive
    Date: Mon, 13 Jan 2025 11:15:36 GMT
    X-Amzn-Trace-Id: Root=1-6784f5d7-3a9d609f65e3ed842894fa2a;Parent=58276c7d1c637204;Sampled=0;Lineage=1:16a5d95f:0
    x-amzn-RequestId: 0c7c3c91-757d-4449-a0f2-f0d01f97c21d
    x-amz-apigw-id: EUtZzGdotjMER8g=
    X-Cache: Error from cloudfront
    Via: 1.1 079b91267decd177d506dbf353188710.cloudfront.net (CloudFront)
    X-Amz-Cf-Pop: NRT20-C3
    X-Amz-Cf-Id: PQRpk3v68UNC4CTAZxkVp-oxmTBUVTDTMk5X9jL35Sffc4byf8Vseg==

Response Body
    
```
CloudWatch Logsで確認してみるとLambda関数は実行されていました。
```
REPORT RequestId: 49f948e3-5f8b-48c6-9d14-b9edc502a9b7	Duration: 240.91 ms	Billed Duration: 241 ms	Memory Size: 128 MB	Max Memory Used: 93 MB	
```

DynamDBのレコードを確認してみます。
先ほどのレスポンスは500エラーでしたが、idは"update_complete"に更新されていますので、Lambda関数自体は正常に処理されていることが確認できました。
こちらのアクションでも中途半端に処理が実行できてしまったパターンを再現できそうです。

![fis_111.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/b9a532ae-5684-fbce-acf1-3a8b5198461c.png)


## アクションを繋げてシナリオを作成してみる
個々のアクションについては流れを確認できましたので、以下の流れのようにFIS実験のアクションを組み合わせてシナリオにしてみます。

最初の3分
　リクエストの50%で4,000msのDelayが注入される形になります。
次の4分
　全てのリクエストで4,000msのDelayが注入され、50%のリクエストに対してエラーがレスポンスされます。
最後の2分
　全てのリクエストでエラーがレスポンスされます。（なお、Prevent executionがOFFになっていますので、DynamoDBにレコードは作成される状態です）
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/b619971f-98d1-fdba-d206-bac4026e465c.png)

アクションの前後関係を設定する時には、アクションを編集する際に「次のあと開始」で先行するアクションを選択することで実現できます。
![fis_004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/3b4bbfd2-2e64-f304-4b19-6b66f52d6ad4.png)

最終的にアクション設定は以下のようになりました。
![fis_200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/94b2290d-ec8c-1931-955b-bd24e9a65cc3.png)

add-delay-50が終わり次第、add-delay−100とinvocation-error-50が実行されます。
add-delay−100とinvocation-error-50が終わり次第、invocation-error-100が実行されます。

### シナリオを実行してみる
準備が整いましたので、シナリオを実行してみます。
今回、リクエストを続けるため、Postmanのランナーでリクエストを投げ続けた状態で実験を行うことにしました。

結果は以下の通りです。　時間の経過とともに障害状況が変化していることが分かります。
（Max responseで表示しているので、処理時間の部分がいまいちな表示です。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/25c9e819-6379-54d2-d2a2-0d9886c36a6c.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/4db4c938-0269-7601-8527-5077ea967f68.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925746/4141844f-174a-7bf8-fc99-d1f3d8f04778.png)



#### 補足
FISの実験実行時に作成される定義ファイルですが、中身は以下のようなシンプルな内容になっていました。
アクションを実行している間だけ存在して、終了すると自動で削除されていました。
存在している間だけ、Lambdaレイヤーで読み取って障害を注入する仕組みですね。
```
{
	"faults": [
		{
			"actionId": "aws:lambda:invocation-error",
			"parameters": {
				"invocationPercentage": "100",
				"preventExecution": "false",
				"replaceOutputWithString": "{\"errorMessage\":\"FIS Injected Fault\",\"errorType\":\"FISInjectedFault\",\"stackTrace\":[]}",
				"injectAsError": "true",
				"qualifier": "$ANY",
				"minimumRequiredExtensionVersion": "1.0.0"
			},
			"expiration": 1737386069495,
			"startTime": 1737385988697
		}
	]
}
```


## まとめ
　処理をしつつ、エラーを返すPrevent executionの設定は使えそうだと感じました。シナリオ化もできるので一度テンプレートを作ってしまえば、繰り返し実施するのも簡単です。
　一方で、Lambdaレイヤー・環境変数の追加、IAMロールを変更したりとLambdaにかなり手を入れる必要がある点が懸念です。FISを実行するための設定変更の手間があり、さらに試験用の関数と本番環境にデプロイする関数との設定差異をどうするかなどの検討が必要になります。
　トータルで見ればソースコードを改変せずに障害を注入できる点はプラスの面が大きいと思いますので、FISのLambdaアクションを積極的に使っていきたいと思います。