カオスエンジニアリングツールGremlinを使ってJavaで実装したAWS Lambda関数に障害を注入してみる
こんにちは、CX事業本部のうらわです。
カオスエンジニアリングツールのGremlinでは以下に挙げた条件がありますがホストやコンテナといったInfrastructure層だけではなくEC2でホストしているWebアプリやAWS LambdaといったApplication層に対しても障害を注入できます(ALFI: application-level fault injection と呼ばれています)。
- PRO以上の料金プランで利用できる
- Application層への攻撃機能はオープンベータ(本記事執筆時点)
- 対応プログラミング言語はJavaのみ(本記事執筆時点)
今回は実際にALFIを試してみます。具体的には、AWS Lambda(以下 Lambda)にサンプル関数をデプロイしGremlinから障害を注入してみます。
環境
Macで作業しています。AWS SAM CLIとJavaのビルドツールのgradleはあらかじめHomebrewでインストールしておきます。
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H15 $ sam --version SAM CLI, version 1.18.2 $ gradle --version ------------------------------------------------------------ Gradle 6.8.3 ------------------------------------------------------------ Build time: 2021-02-22 16:13:28 UTC Revision: 9e26b4a9ebb910eaa1b8da8ff8575e514bc61c78 Kotlin: 1.4.20 Groovy: 2.5.12 Ant: Apache Ant(TM) version 1.10.9 compiled on September 27 2020 JVM: 11.0.9 (Oracle Corporation 11.0.9+11) OS: Mac OS X 10.15.7 x86_64
本記事のコードは以下のリポジトリに格納してあります。
Java 11をインストールする
AWS LambdaのJavaランタイムはJava 8とJava 11が対応しています。今回はJava 11を使用します。
HomebrewでJava 11をインストールします。以下の記事を参考にさせていただきました。
MacのBrewで複数バージョンのJavaを利用する + jEnv
$ brew tap homebrew/cask-versions $ brew install java11 $ /usr/libexec/java_home -V Matching Java Virtual Machines (2): 14.0.2, x86_64: "Java SE 14.0.2" /Library/Java/JavaVirtualMachines/jdk-14.0.2.jdk/Contents/Home 11.0.9, x86_64: "OpenJDK 11.0.9" /Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home /Library/Java/JavaVirtualMachines/jdk-14.0.2.jdk/Contents/Home
JAVA_HOME
にjava 11のインストールパスを設定しておきます。
export JAVA_HOME=/Library/Java/JavaVirtualMachines/openjdk-11.jdk/Contents/Home
サンプルアプリを用意する
AWS SAM CLIを利用してサンプルアプリケーションを作成します。以下のオプションを指定してsam init
を実行しサンプル関数を用意します。
-r java11
: LambdaランタイムとしてJava 11を使用します。-d gradle
: ビルドツールとしてgradleを使用します。--app-template hello-world
: API Gatewayと文字列を返すだけの関数雛形です。-n gremlin-lambda-sample
: 関数名(ディレクトリ名)です。
$ sam init -r java11 -d gradle --app-template hello-world -n gremlin-lmabda-sample
ビルドしてデプロイします。
# gradleでビルドされる $ sam build # ガイドにしたがってデプロイ設定をする $ sam deploy --guided
デプロイ完了後に表示されるAPI Gatewayのエンドポイントへcurlでリクエストを送ってみます。
$ curl -XGET https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ { "message": "hello world", "location": "xx.xx.xx.xx" }
マネジメントコンソールでLambdaのテスト実行も試しておきます。
Gremlinのライブラリを組み込む
関数にGremlinのライブラリのコードを仕込みます。これはGremlinの公式ドキュメントを参考にします。
コンストラクタApp()
でgremlinService
を作成し、handleRequest()
内のgremlinService.applyImpact()
で障害を注入します。なお、Gremlin関連のコードはLambdaの通常実行時には影響はありません。
このコードで再度ビルド・デプロイしておきます。
package helloworld; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; import com.gremlin.*; import com.gremlin.aws.AwsApplicationCoordinatesResolver; /** * Handler for requests to Lambda function. */ public class App implements RequestHandler<APIGatewayProxyRequestEvent, APIGatewayProxyResponseEvent> { private final GremlinService gremlinService; public App() { final GremlinServiceFactory factory = new GremlinServiceFactory(new GremlinCoordinatesProvider() { @Override public ApplicationCoordinates initializeApplicationCoordinates() { ApplicationCoordinates coords = AwsApplicationCoordinatesResolver.inferFromEnvironment() .orElseThrow(IllegalStateException::new); return coords; } }); gremlinService = factory.getGremlinService(); } public APIGatewayProxyResponseEvent handleRequest(final APIGatewayProxyRequestEvent input, final Context context) { TrafficCoordinates trafficCoordinates = new TrafficCoordinates.Builder() .withType(this.getClass().getSimpleName()) .withField("method", "handleRequest") .build(); gremlinService.applyImpact(trafficCoordinates); Map<String, String> headers = new HashMap<>(); headers.put("Content-Type", "application/json"); headers.put("X-Custom-Header", "application/json"); APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent() .withHeaders(headers); try { final String pageContents = this.getPageContents("https://checkip.amazonaws.com"); String output = String.format("{ \"message\": \"hello world\", \"location\": \"%s\" }", pageContents); return response .withStatusCode(200) .withBody(output); } catch (IOException e) { return response .withBody("{}") .withStatusCode(500); } } private String getPageContents(String address) throws IOException{ URL url = new URL(address); try(BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream()))) { return br.lines().collect(Collectors.joining(System.lineSeparator())); } } }
Lambdaの環境変数を設定する
Gremlinと接続するためにデプロイ後のLambdaに以下の環境変数を設定する必要があります。
Team IDと証明書・秘密鍵はGremlinのTeam Settings > Configuration画面で確認・入手しておきます。
Key | Value | 目的 |
---|---|---|
GREMLIN_ALFI_ENABLED | true | Gremlinによる障害の注入を有効にする |
GREMLIN_ALFI_IDENTIFIER | LAMBDADEMO | GremlinでClientを特定するための文字列 |
GREMLIN_TEAM_CERTIFICATE_OR_FILE | -----BEGIN CERTIFICATE-----\n< 証明書の中身 >\n-----END CERTIFICATE----- | Gremlinとの接続するために必要な認証設定 |
GREMLIN_TEAM_PRIVATE_KEY_OR_FILE | -----BEGIN EC PRIVATE KEY-----\n< 秘密鍵の中身 >\n-----END EC PRIVATE KEY----- | Gremlinとの接続するために必要な認証設定 |
GREMLIN_TEAM_ID | < Team ID > | アプリケーションが所属するチームを特定するためのID |
環境変数の設定後、再度マネジメントコンソールでLambdaのテスト実行をしておきます。
GremlinのClients > Applicationを確認し、LAMBDADEMO
という名前のアプリが表示されていれば設定は完了です。
障害を注入する
前述のClients > Applicationの画面からAttack clientを選択します。すると以下の画面のようにAppliaction Queryが設定された状態でAttackの設定画面に遷移します。
次に、Traffic Queryを選択します。今回は特定のコードブロックで障害を注入するCustom Traffic Typeを選択しName、Percent to Impact、Custom Valueを設定します。
NameとCustom Valueの設定値は以下のようにLambdaのコードに書いた値となります。
TrafficCoordinates trafficCoordinates = new TrafficCoordinates.Builder() // ここで得られるクラス名をNameに設定する .withType(this.getClass().getSimpleName()) // ここの第一引数をCustom ValueのKeyに、第二引数をValueに設定する .withField("method", "handleRequest") .build();
最後に、攻撃内容を設定します。指定した時間のレイテンシを発生させる、または強制的に例外を発生させる、という2つの攻撃の設定が可能です。
まずは強制的に例外を発生させてみます。Throw ExceptionをOnにします。
以上で設定は完了です。Unleash Gremlinで障害の注入を開始します。
再度curlでリクエストを送ってみると、Internal server error
になりました。
$ curl -XGET https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ {"message": "Internal server error"}
マネジメントコンソールでもテスト実行を試してみます。Logを確認するとGremlinによって例外が発生していることが確認できます。
次に、レイテンシの発生を試してみます。Gremlinの画面でこれまでと同様にApplication QueryとTraffic Queryを設定した後、Choose Gremlinでレイテンシの時間を設定します。
今回のLambdaは20秒でタイムアウトする設定になっているため、20000msを設定してみます。
Unleash GremlinでAttackの実行後、これまでと同様にcurlとマネジメントコンソールで状況を確認してみます。
# 20秒経ってエラー $ curl -XGET https://xxxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/ {"message": "Internal server error"}
意図通り、APIの実行はタイムアウトによりエラーになりました。
まとめ
本記事ではLambdaに対してGremlinによる障害の注入方法をご紹介しました。今回はAPI GatewayとLambdaの挙動を確認しましたが、ここにAPIの実行元であるWebアプリ等が絡んでくることで、Lambdaで実際に例外やタイムアウトが発生した場合や、AWSの障害が発生した場合にWebアプリがどのような挙動をするか検証することができるかと思います。
参考資料
https://github.com/gremlin/alfi-lambda
Getting Started with Application Level Failure Injection (ALFI) - Hello World