Java + Gradle で Lambda レイヤー用のデプロイパッケージを作成する

Java での Lambda レイヤーを作成手順をまとめました。

Java + Gradle で Lambda レイヤー用のデプロイパッケージを作成する環境

  • macOS: Catalina (10.15.7)
  • JDK: Amazon Corretto 11 (Java 11 JDK)
  • Gradle: 7.4.1

Java + Gradle で Lambda レイヤー用のデプロイパッケージを作成する際の前提

こちらの記事で実装した Lambda 関数をサンプルとして使います。
またこれから紹介する手順は以下の 2 つに分けて説明します。

  • 外部の依存関係ライブラリを Lambda レイヤーに追加する手順
  • 独自ライブラリを Lambda レイヤーに追加する手順

Java + Gradle で外部の依存関係ライブラリを追加した Lambda レイヤー用のデプロイパッケージを作成する手順

この手順ではサンプルで使用している以下の外部ライブラリを Lambda レイヤーに追加します。

  • com.google.code.gson:gson:2.9.0
  • com.amazonaws:aws-lambda-java-core:1.2.1

作業用ディレクトリの作成とプロジェクトの初期化

作業ディレクトリを作成して、gradle init コマンドを実行します。
gradle init コマンド実行時の各設問は最初の 2 つ以外はデフォルト値にしています。

Copied!
$ mkdir ~/java-layer
$ cd ~/java-layer
$ gradle init
Starting a Gradle Daemon (subsequent builds will be faster)

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no]

Project name (default: java-layer):

> Task :init
Get more help with your project: Learn more about Gradle by exploring our samples at https://docs.gradle.org/7.4.1/samples

BUILD SUCCESSFUL in 16s
2 actionable tasks: 2 executed

gradle init コマンド実行後、ディレクトリ構成は以下のようになります。

Copied!
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle

外部ライブラリの定義とデプロイパッケージ作成タスクの定義

build.gradle ファイルを修正します。
dependencies に Lambda レイヤーに追加する外部ライブラリを定義します。
また、task でデプロイパッケージ(.zip ファイルアーカイブ)を作成するタスクを定義します。

外部ライブラリを保存するパスに注意してください。
Lambda のドキュメントでは以下のように記載しています。

Lambda レイヤーの作成と共有 - AWS Lambda

ライブラリの依存関係をレイヤーに含める
Lambda ランタイムごとに、PATH 変数に /opt ディレクトリ内の特定のフォルダが含まれます。レイヤー .zip ファイルアーカイブに同じフォルダ構造を定義すると、関数コードはパスを指定しなくても、レイヤーコンテンツにアクセスできます。
...
各 Lambda ランタイムのレイヤーパス
Java java/lib (CLASSPATH)

上記の記述のように、Lambda レイヤーにアップロードする zip ファイルアーカイブに特定のディレクトリにライブラリなどの依存関係を含めておくだけで、パスを指定しなくてもそれを利用することができます。
今回は Java なので、java/lib ディレクトリに外部ライブラリを保存するよう必要があります。

java-layer/build.gradle
Copied!
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.9.0'
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
}

task buildZip(type: Zip) {
    into('java/lib') {
        from configurations.runtimeClasspath
    }
}

build.dependsOn buildZip

デプロイパッケージの作成その 1

gradle build コマンドを実行してデプロイパッケージを作成します。

Copied!
gradle build

build/distributions ディレクトリに zip 形式のデプロイパッケージが作成されます。
作成されたデプロイパッケージには以下のように java/lib/ ディレクトリと外部ライブラリが格納されます。

Copied!
$ unzip -l build/distributions/java-layer.zip
Archive:  build/distributions/java-layer.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-21-2022 23:02   java/
        0  03-21-2022 23:02   java/lib/
   249277  03-20-2022 23:20   java/lib/gson-2.9.0.jar
     7515  03-20-2022 22:45   java/lib/aws-lambda-java-core-1.2.1.jar
---------                     -------
   256792                     4 files

Lambda レイヤーの作成と Lambda 関数への紐付け 1

作成したデプロイパッケージを使って AWS マネジメントコンソールから Lambda レイヤーを作成と Lambda 関数への紐付けすることもできますが、今回は AWS CLI を使っていきます。
まず、publish-layer-version コマンドで Lambda レイヤーを作成します。
次の手順で使用するので、publish-layer-version コマンド実行結果から LayerVersionArn をメモしておきましょう。

Copied!
$ aws lambda publish-layer-version --layer-name java-basic-layer \
--zip-file fileb://build/distributions/java-layer.zip --compatible-runtimes java11
{
    "Content": {
        "Location": "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/xxxxxxxxxxxxx"
        "CodeSha256": "V9RhiYR2QpTQEq7mKcl3pQnn+TTvMVyUKL9pbTi7Gd4=",
        "CodeSize": 227065
    },
    "LayerArn": "arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer",
    "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer:1",
    "Description": "",
    "CreatedDate": "2022-03-21T14:04:36.472+0000",
    "Version": 1,
    "CompatibleRuntimes": [
        "java11"
    ]
}

次に、作成した Lambda レイヤーをサンプルの Lambda 関数へ紐付けします。
--layers オプションに先ほどメモした LayerVersionArn を指定します。

Copied!
aws lambda update-function-configuration --function-name java-sample \
 --layers arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer:1

Lambda 関数から外部ライブラリを排除する

サンプルの Lambda 関数のデプロイパッケージに外部ライブラリを含める必要がなくなるので、含めないように修正します。
build.gradle ファイルを修正します。
task でデプロイパッケージ(.zip ファイルアーカイブ)を作成するタスクから外部ライブラリに関連する定義を削除します。

java-sample/build.gradle
Copied!
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.9.0'
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
}

task buildZip(type: Zip) {
    from compileJava
    from processResources
    // 以下の定義が不要になります。
    // into('lib') {
    //     from configurations.runtimeClasspath
    // }
}

build.dependsOn buildZip

デプロイパッケージの作成その 2

gradle build コマンドを実行してデプロイパッケージを作成します。

Copied!
gradle build

build/distributions ディレクトリに zip 形式のデプロイパッケージが作成されます。
作成されたデプロイパッケージには以下のように外部ライブラリがなくなり、クラスファイルのみが格納されます。

Copied!
$ unzip -l build/distributions/java-sample.zip
Archive:  build/distributions/java-sample.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-21-2022 22:51   sample/
     2686  03-21-2022 22:51   sample/App.class
     1381  03-21-2022 22:51   previous-compilation-data.bin
---------                     -------
     4067                     3 files

Lambda 関数の更新と実行その 1

作成したデプロイパッケージを使って AWS マネジメントコンソールから Lambda 関数を更新することもできますが、今回は AWS CLI を使って Lambda 関数の更新をします。

Copied!
aws lambda update-function-code --function-name java-sample --zip-file fileb://build/distributions/java-sample.zip

Lambda 関数を実行して、エラーが出ず以下のように出力されれば成功です。
また、Lambda 関数の CloudWatch Logs のロググループに payload の値などが出力されているはずです。

Copied!
$ aws lambda invoke --function-name java-sample  \
--payload '{"text":"Hello"}' response.txt --cli-binary-format raw-in-base64-out
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

Java + Gradle で独自ライブラリを追加した Lambda レイヤー用のデプロイパッケージを作成する手順

この手順ではサンプル内のログ処理を独自ライブラリとして Lambda レイヤーに追加します。

java-sample/src/main/java/sample/App.java
Copied!
package sample;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.LambdaLogger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.util.Map;

public class App implements RequestHandler<Map<String, String>, String> {
  Gson gson = new GsonBuilder().setPrettyPrinting().create();

  @Override
  public String handleRequest(Map<String, String> event, Context context) {
    String response = new String("200 OK");
    // ---ここから---
    LambdaLogger logger = context.getLogger();
    // log execution details
    logger.log("ENVIRONMENT VARIABLES: " + gson.toJson(System.getenv()));
    logger.log("CONTEXT: " + gson.toJson(context));
    // process event
    logger.log("EVENT: " + gson.toJson(event));
    logger.log("EVENT TYPE: " + event.getClass().toString());
    // ---ここまでの処理を独自ライブラリ化します。---
    return response;
  }
}

独自ライブラリ処理の実装

まず、独自ライブラリ処理を実装するためのファイルを作成します。

Copied!
cd ~/java-layer
mkdir src/main/java/sample_lib
touch src/main/java/sample_lib/Util.java

サンプル内のログ処理を参考にというかほぼそのまま持ってきます。

java-layer/src/main/java/sample_lib/Util.java
Copied!
package sample_lib;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.LambdaLogger;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

import java.util.Map;

public class Util {
  public static void log(Map<String, String> event, Context context) {
    Gson gson = new GsonBuilder().setPrettyPrinting().create();
    LambdaLogger logger = context.getLogger();
    // log execution details
    logger.log("ENVIRONMENT VARIABLES: " + gson.toJson(System.getenv()));
    logger.log("CONTEXT: " + gson.toJson(context));
    // process event
    logger.log("EVENT: " + gson.toJson(event));
    logger.log("EVENT TYPE: " + event.getClass().toString());
  }
}

デプロイパッケージ作成タスクに独自ライブラリを追加するように定義する

build.gradle ファイルを修正します。
buildZip タスクで外部ライブラリ(configurations.runtimeClasspath)に加えコンパイルした独自ライブラリ(jar)も Lambda レイヤーに追加するように定義します。

java-layer/build.gradle
Copied!
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.9.0'
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
}

task buildZip(type: Zip) {
    into('java/lib') {
        from jar, configurations.runtimeClasspath
    }
}

build.dependsOn buildZip

デプロイパッケージの作成その 3

gradle build コマンドを実行してデプロイパッケージを作成します。

Copied!
gradle build

build/distributions ディレクトリに zip 形式のデプロイパッケージが作成されます。
作成されたデプロイパッケージには以下のように java/lib/ ディレクトリと独自ライブラリ(java-layer.jar)と外部ライブラリが格納されます。

Copied!
$ unzip -l build/distributions/java-layer.zip
Archive:  build/distributions/java-layer.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-21-2022 23:17   java/
        0  03-21-2022 23:17   java/lib/
     1446  03-21-2022 23:17   java/lib/java-layer.jar
   249277  03-20-2022 23:20   java/lib/gson-2.9.0.jar
     7515  03-20-2022 22:45   java/lib/aws-lambda-java-core-1.2.1.jar
---------                     -------
   258238                     5 files

Lambda レイヤーの更新と Lambda 関数への紐付け 2

作成したデプロイパッケージを使って AWS マネジメントコンソールから Lambda レイヤーの更新と Lambda 関数への紐付けすることもできますが、今回は AWS CLI を使っていきます。
まず、publish-layer-version コマンドで Lambda レイヤーを更新します。
次の手順で使用するので、publish-layer-version コマンド実行結果から LayerVersionArn をメモしておきましょう。

Copied!
$ aws lambda publish-layer-version --layer-name java-basic-layer \
--zip-file fileb://build/distributions/java-layer.zip --compatible-runtimes java11
{
    "Content": {
        "Location": "https://awslambda-ap-ne-1-layers.s3.ap-northeast-1.amazonaws.com/xxxxxxxxxxxxx"
        "CodeSha256": "V9RhiYR2QpTQEq7mKcl3pQnn+TTvMVyUKL9pbTi7Gd4=",
        "CodeSize": 227065
    },
    "LayerArn": "arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer",
    "LayerVersionArn": "arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer:2",
    "Description": "",
    "CreatedDate": "2022-03-21T14:04:36.472+0000",
    "Version": 1,
    "CompatibleRuntimes": [
        "java11"
    ]
}

次に、作成した Lambda レイヤーをサンプルの Lambda 関数へ紐付けします。
--layers オプションに先ほどメモした LayerVersionArn を指定します。

Copied!
aws lambda update-function-configuration --function-name java-sample \
 --layers arn:aws:lambda:ap-northeast-1:zzzzzzzzzzzzzz:layer:java-basic-layer:2

独自ライブラリの取り込みと Lambda 関数から独自ライブラリの処理を呼び出す

まず、libs ディレクトリを作成し、独自ライブラリの jar ファイルをコピーします。

Copied!
mkdir ~/java-sample/libs
cp ~/java-layer/build/libs/java-layer.jar ~/java-sample/libs

次に、サンプルの Lambda 関数build.gradle ファイルを修正します。
独自ライブラリを使用するために dependencieslibs ディレクトリないの jar ファイルを依存関係ライブラリとして定義します。

java-sample/build.gradle
Copied!
plugins {
    id 'java'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.google.code.gson:gson:2.9.0'
    implementation 'com.amazonaws:aws-lambda-java-core:1.2.1'
    implementation fileTree(dir: 'libs', include: '*.jar')
}

task buildZip(type: Zip) {
    from compileJava
    from processResources
}

build.dependsOn buildZip

最後に、サンプルの Lambda 関数から独自ライブラリの処理を呼び出すように修正します。

java-sample/src/main/java/sample/App.java
Copied!
package sample;

import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;

// 独自ライブラリのインポート
import sample_lib.Util;

import java.util.Map;

public class App implements RequestHandler<Map<String, String>, String> {
  @Override
  public String handleRequest(Map<String, String> event, Context context) {
    // 独自ライブラリの処理の呼び出し
    Util.log(event, context);
    String response = new String("200 OK");
    return response;
  }
}

デプロイパッケージの作成その 4

gradle build コマンドを実行してデプロイパッケージを作成します。

Copied!
gradle build

build/distributions ディレクトリに zip 形式のデプロイパッケージが作成されます。
作成されたデプロイパッケージの中身は以下のように外部ライブラリがなくなり、クラスファイルのみになります。

Copied!
$ unzip -l build/distributions/java-sample.zip
Archive:  build/distributions/java-sample.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  03-21-2022 23:23   sample/
     1392  03-21-2022 23:23   sample/App.class
      528  03-21-2022 23:23   previous-compilation-data.bin
---------                     -------
     1920                     3 files

Lambda 関数の更新と実行その 2

作成したデプロイパッケージを使って AWS マネジメントコンソールから Lambda 関数を更新することもできますが、今回は AWS CLI を使って Lambda 関数の更新をします。

Copied!
aws lambda update-function-code --function-name java-sample --zip-file fileb://build/distributions/java-sample.zip

Lambda 関数を実行して、エラーが出ず以下のように出力されれば成功です。
また、Lambda 関数の CloudWatch Logs のロググループに payload の値などが出力されているはずです。

Copied!
$ aws lambda invoke --function-name java-sample  \
--payload '{"text":"Hello"}' response.txt --cli-binary-format raw-in-base64-out
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}

参考情報