カオスエンジニアリングツールGremlinを使ってJavaで実装したAWS Lambda関数に障害を注入してみる

カオスエンジニアリングツールのGremlinでJavaで実装したAWS Lambda関数に障害を注入してみました
2021.03.21

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

こんにちは、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を使用します。

Lambda ランタイム

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の通常実行時には影響はありません。

このコードで再度ビルド・デプロイしておきます。

HelloWorldFunction/src/main/java/helloworld/App.java

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のコードに書いた値となります。

HelloWorldFunction/src/main/java/helloworld/App.java

        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