AWS Lambda 用の Java11 コードを docker-lambda 環境でローカル開発するメモ

Docker に慣れちゃうと、別にインストールしても問題ないものまでコンテナ上で動かしたくなりません？特に更新が激しいものはアップデートの導入が面倒なので、つい Docker image を探しがち。

というわけで、AWS Lambda で実行する Java 11 コードを、[docker-lambda](https://github.com/lambci/docker-lambda) を用いてローカル環境で開発・テスト実行させてみました。Docker 以外はインストール不要なので、気軽に試せて良い感じです。

ググってみても python や nodejs の説明ばかりヒットする気がしたので、簡単なメモとしてまとめておきます。

# docker-lambda とは

[docker-lambda](https://github.com/lambci/docker-lambda) は AWS Lambda 開発環境を Docker ベースで提供するものです。以下、概要部分を簡単に訳しておきますね。

> インストールされているソフトウェアとライブラリ、ファイル構造と権限、環境変数、コンテキストオブジェクトと動作を含む「ライブ AWS Lambda 環境」をほぼ同じように複製するサンドボックス化されたローカル環境。ユーザーと実行中のプロセスも同じです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/41fe2a69-4c11-9aeb-99da-f5f908ae02a3.png)
docker-lambda を使用することで、同じ厳密な Lambda 環境で関数を実行し、ライブでデプロイしたときに同じ動作を示せます。AWS Lambda に存在するライブラリバージョンと同様にネイティブ依存関係をコンパイルし、AWS CLI を使用してデプロイすることもできます。

# サンプルコードの導入

テスト用のフォルダ(ディレクトリ)を作成し、[docker-lambda/examples/java/](https://github.com/lambci/docker-lambda/tree/master/examples/java) にあるファイルを配置します。[該当リポジトリ](https://github.com/lambci/docker-lambda) を丸ごと clone もしくは zip 形式でダウンロードして、該当部分だけをコピーすると楽でしょう。

## サンプルコードのビルド

ダウンロードしたサンプルは [Java のソースコード](https://github.com/lambci/docker-lambda/blob/master/examples/java/src/main/java/org/lambci/lambda/ExampleHandler.java) です。テスト用のフォルダに移動し、Docker コマンドでビルド(コードのコンパイル)を実施します。

```
docker run --rm -v $PWD:/var/task lambci/lambda:build-java11 gradle build
```

なお私は Windows 環境なので ``$PWD`` が使えないため、実際のフォルダ (今回は c:/work/java/docker-lambda) を指定しました。

```
docker run --rm -v c:/work/java/docker-lambda:/var/task lambci/lambda:build-java11 gradle build
```

問題無く実行されれば build フォルダが作成され、その中に幾つかのフォルダが生成されるはずです。この中の docker フォルダに、コンパイルされたクラスのバイトコードと、実行に必要なライブラリが格納されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/723e7bf4-2105-5a07-e5a8-34a03a70e06e.png)

## サンプルコードの実行

作成された実行クラスは、``build/docker/`` フォルダ内にあります。以下のコマンドで実行してみます。

```
docker run --rm -v $PWD/build/docker:/var/task \
  lambci/lambda:java11 org.lambci.lambda.ExampleHandler \
  '{"some": "event"}'
```

Windows 環境では改行を使わず、JSON の指定方法も修正して、実際には以下のように実施しています。

```
docker run --rm -v c:/work/java/docker-lambda/build/docker:/var/task lambci/lambda:java11 org.lambci.lambda.ExampleHandler "{\"some\": \"event\"}"
```

# AWS SDK API を利用してみる

サンプルの実行だけで終わるのも寂しいですので、S3 のバケット一覧を取得してみます。

## サンプルコードの修正

[AWSのガイド](https://docs.aws.amazon.com/ja_jp/sdk-for-java/v1/developer-guide/examples-s3-buckets.html) を参考に、まずは import 文を追加します。

```Java:src/main/java/org/lambci/lambda/ExampleHandler.java
import com.amazonaws.regions.Regions;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import com.amazonaws.services.s3.model.Bucket;
import java.util.List;
```

そして ``handleRequest`` 関数の終わり (Return文の前) に以下のコードを追加します。

```Java:src/main/java/org/lambci/lambda/ExampleHandler.java
        logger.log("Your Amazon S3 buckets are:\n");
        final AmazonS3 s3 = AmazonS3ClientBuilder.standard().withRegion(Regions.DEFAULT_REGION).build();
        List<Bucket> buckets = s3.listBuckets();
        for (Bucket b : buckets) {
            logger.log("* " + b.getName() + "\n");
        }
```

この段階でビルドを実施すると、S3 関連の SDK ライブラリが見つからずエラーになります。テスト用フォルダにある build.gradle にビルド用の設定(2行)を追加します。

```gradle:build.gradle
dependencies {
    implementation platform('com.amazonaws:aws-java-sdk-bom:1.11.99') // この行を追加
    implementation (
        'com.amazonaws:aws-lambda-java-core:1.2.0',
        'com.amazonaws:aws-lambda-java-events:2.2.7', // カンマ追加
        'com.amazonaws:aws-java-sdk-s3' // この行を追加
    )
}
```

これで先ほどと同じコマンドで、ビルドできるようになりました。

## 修正したサンプルコードの実行

さきほどと同様にコードを実行しようとすると、権限エラーになります。以下のように -e オプションで ``AWS_ACCESS_KEY`` と ``AWS_SECRET_ACCESS_KEY`` の値を渡してあげましょう。

```
docker run --rm -v $PWD/build/docker:/var/task \
  -e AWS_ACCESS_KEY_ID=XXX -e AWS_SECRET_ACCESS_KEY=XXXXXX \
  lambci/lambda:java11 org.lambci.lambda.ExampleHandler \
  '{"some": "event"}'
```

Windows の場合は1行にまとめて、JSON 表記など修正して以下のように実行します。

```
docker run --rm -v c:/work/java/docker-lambda/build/docker:/var/task -e AWS_ACCESS_KEY_ID=XXX -e AWS_SECRET_ACCESS_KEY=XXXXXX lambci/lambda:java11 org.lambci.lambda.ExampleHandler "{\"some\": \"event\"}"
```

こんな感じで s3 バケットのリストが得られれば成功です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/97c6f352-b4b9-dfeb-7173-56f586b8d617.png)

``AWS_ACCESS_KEY`` と ``AWS_SECRET_ACCESS_KEY`` は、AWS でユーザーを作成して、その「認証情報」タブから作成・取得ができます。なんの権限もない新規ユーザーを作成し、開発対象である Lambda 関数と同じアクセス権限(ロールやポリシー)を与えて利用するのが良さそうです。

# stay-open モードで開発する

4つのオプションを追加して、更に便利に開発していきましょう。

* 環境変数 ``DOCKER_LAMBDA_STAY_OPEN=1`` で API 待ち受けモードになる
* ``-p 9001:9001`` オプションで待ち受けするポートを指定する
* 環境変数 ``DOCKER_LAMBDA_WATCH=1`` でファイルの変更を監視し、サービスの再スタートを実施する
* ``--restart on-failure`` Dockerオプションでエラー時に自動的に再起動する (Java 11では必須ではない)

以下のようなコマンド実行になります。

```
docker run -v $PWD/build/docker:/var/task \
  --restart on-failure \
  -e DOCKER_LAMBDA_WATCH=1 -e DOCKER_LAMBDA_STAY_OPEN=1 -p 9001:9001 \
  -e AWS_ACCESS_KEY_ID=XXX -e AWS_SECRET_ACCESS_KEY=XXXXXX \
  lambci/lambda:java11 org.lambci.lambda.ExampleHandler \
  '{"some": "event"}'
```

Windows の場合は以下のように実行します。

```
docker run -v c:/work/java/docker-lambda/build/docker:/var/task --restart on-failure -e DOCKER_LAMBDA_WATCH=1 -e DOCKER_LAMBDA_STAY_OPEN=1 -p 9001:9001 -e AWS_ACCESS_KEY_ID=XXX -e AWS_SECRET_ACCESS_KEY=XXXXXX lambci/lambda:java11 org.lambci.lambda.ExampleHandler "{\"some\": \"event\"}"
```

別なターミナル画面を開き、以下のように curl などでアクセスしてみましょう。

```
curl -d "{}" http://localhost:9001/2015-03-31/functions/myfunction/invocations
```

以下のように Return 文の内容 "Hello World!" が返ってくれば動作は成功です。ログは Docker を実行したほうのターミナルのほうに表示されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/193519/e5b13514-741c-796b-6c49-e805214041c8.png)

試しにソースコードを修正してビルドを実行すると、/build/docker フォルダの変更を感知して、サービスが自動的に再始動します。

# おわりに

Docker 環境されあれば docker-lambda を用いてローカルで AWS Lambda 関数の開発を実施できます。好みのエディタでサクサク開発でき、テストも容易です。いろんなコードを書いて、試してみましょう！

それではまた。