AWS LambdaのコンテナサポートをJavaで試してみた。

[AWS LambdaとServerless Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lambda) 12/8分の記事です。

----

COVID-19の影響で、オンライン開催となったAWS re:Invent 2020。
初日のキーノートで、AWS Lambdaがコンテナをサポートするという発表がありました。
[New for AWS Lambda – Container Image Support](https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-container-image-support/)

なんかやってみたいなーと思って、昔Javaをやっていたので、
Java用のコンテナイメージ使って、Lambda関数を作ってみました。

## どんなコンテナイメージあるの？
コンテナイメージ(Base Image)にはどんなのがあるのかと、
[DockerHub](https://hub.docker.com/u/amazon)をみると、以下のものがあるようですね。

- amazon/aws-lambda-nodejs
- amazon/aws-lambda-python
- amazon/aws-lambda-java
- amazon/aws-lambda-dotnet
- amazon/aws-lambda-ruby
- amazon/aws-lambda-go
- amazon/aws-lambda-provided

現在標準のランタイムとして提供されている言語はすべてありそうです。
amazon/aws-lambda-providedはTagをみると、ALとかAL2があるようなので、
カスタムランタイム用のようです。

各言語も、例えば、Javaでは、
バージョン8と11が用意されています。

イメージの取得場所なんですが、
`FROM public.ecr.aws/lambda/java:11`
となっていて、
同じくKeyNoteで発表された
[Announcing Amazon ECR Public and Amazon ECR Public Gallery](https://aws.amazon.com/jp/about-aws/whats-new/2020/12/announcing-amazon-ecr-public-and-amazon-ecr-public-gallery/)
が早速使われているみたいですね。

Base Imageを使うと、AWSがパッチ当てとかメンテナンスをしてくれると聞いております。

## やってみた
コードをがっつり書くつもりはなく（すみません）。コード自体は、公式ドキュメントのAWS Lambda for Javaに記載のあったSample Appsのうち、[java-basic](https://github.com/awsdocs/aws-lambda-developer-guide/tree/master/sample-apps/java-basic)の[Hander.java](https://github.com/awsdocs/aws-lambda-developer-guide/blob/master/sample-apps/java-basic/src/main/java/example/Handler.java)を使わさせていただいてます。
ビルド自体はMavenを使ってます（昔に使ってたので）。

Dockerfileも[Dockerhub記載](https://hub.docker.com/r/amazon/aws-lambda-java)のものをベースにしてます。

```dockerfile
FROM public.ecr.aws/lambda/java:11

# Copy function code
COPY target/java-sample-1.0-SNAPSHOT.jar ${LAMBDA_TASK_ROOT}
RUN ls -al
RUN jar -xvf java-sample-1.0-SNAPSHOT.jar
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "example.Handler::handleRequest" ]
```

最初は、jarファイルを${LAMBDA_TASK_ROOT}（/var/task）に配置すれば、
よしなにやってくれるかなと思ったんですが、
そんなわけもなく、
コンパイルされた状態で配置させる必要があるようで、
作成されたjarファイルを解凍してます（めっちゃ下手書きですみません）。

とてもシンプルなJava("Hello Java")と返すようなプログラムだと、
ビルドしたClassファイルだけあれば行けそうだったんですが、
サンプルだと、Googleが提供するJSONデータとJavaオブジェクトを相互に変換するライブラリの**GSON**を使ってて、
それがClassNotFoundExceptionになっちゃうので、この形式にしてます。

久々に使ったので、
もしかしたら、他にいい手段があるかもしれないので、
わかったらアップデートしようかなと思います。

コンテナの中で全部できるかな？ってことでやってみたのがこちら

```dockerfile
FROM public.ecr.aws/lambda/java:11

RUN yum -y update && yum -y install wget
RUN wget http://repos.fedorapeople.org/repos/dchen/apache-maven/epel-apache-maven.repo -O /etc/yum.repos.d/epel-apache-maven.repo
RUN sed -i s/\$releasever/6/g /etc/yum.repos.d/epel-apache-maven.repo
RUN yum install -y apache-maven
ENV JAVA_HOME=/usr/lib/jvm/java-11-amazon-corretto.x86_64/

# Copy function code
COPY src/ ${LAMBDA_TASK_ROOT}/src/
COPY pom.xml ${LAMBDA_TASK_ROOT}
RUN mvn package
RUN cp -r target/java-sample-1.0-SNAPSHOT.jar ${LAMBDA_TASK_ROOT}
RUN jar -xvf java-sample-1.0-SNAPSHOT.jar
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "example.Handler::handleRequest" ]
```
こちらもjarファイルを解凍してます（同じく下手書きですみません）。

便利だなと思ったのは、
コンテナを起動して、cUrlなどでアクセスしてローカルテスト可能なのは便利かなと思いました。

```shell
docker build -t java-sample-v11 .
Successfully tagged java-sample-v11:latest
docker run -p 9000:8080 java-sample-v11:latest
```
別ターミナルで、`curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'` 
って感じで実行すると、問題なければレスポンスがあります。

ログもちゃんと出力されます。
`Init Duration: 0.22 ms  Duration: 504.55 ms     Billed Duration: 600 ms`
となっていて、`Billed Duration`が100ms単位だなーって思いましたが、ローカルなのでw

```
ENVIRONMENT VARIABLES: {
  "PATH": "/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin",
  "LAMBDA_TASK_ROOT": "/var/task",
  "AWS_LAMBDA_FUNCTION_MEMORY_SIZE": "3008",
  "TZ": ":/etc/localtime",
  "AWS_EXECUTION_ENV": "AWS_Lambda_java11",
  "AWS_LAMBDA_LOG_GROUP_NAME": "/aws/lambda/Functions",
  "AWS_LAMBDA_LOG_STREAM_NAME": "$LATEST",
  "LANG": "en_US.UTF-8",
  "_HANDLER": "example.Handler::handleRequest",
  "LAMBDA_RUNTIME_DIR": "/var/runtime",
  "HOSTNAME": "b10284f7ea25",
  "LD_LIBRARY_PATH": "/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib",
  "AWS_LAMBDA_FUNCTION_VERSION": "$LATEST",
  "PWD": "/var/task",
  "AWS_LAMBDA_RUNTIME_API": "127.0.0.1:9001",
  "SHLVL": "0",
  "HOME": "/root",
  "AWS_LAMBDA_FUNCTION_NAME": "test_function"
}CONTEXT: {
  "memoryLimit": 3008,
  "awsRequestId": "fc64a0bd-890c-4339-a460-8ecfc5fa1686",
  "logGroupName": "/aws/lambda/Functions",
  "logStreamName": "$LATEST",
  "functionName": "test_function",
  "functionVersion": "$LATEST",
  "invokedFunctionArn": "",
  "deadlineTimeInMs": 3214833340286,
  "logger": {}
}EVENT: {
  "payload": "hello world!"
}EVENT TYPE: class java.util.LinkedHashMapEND RequestId: fc64a0bd-890c-4339-a460-8ecfc5fa1686
REPORT RequestId: fc64a0bd-890c-4339-a460-8ecfc5fa1686  Init Duration: 0.22 ms  Duration: 504.55 ms     Billed Duration: 600 ms Memory Size: 3008 MB    Max Memory Used: 3008 MB
```

ローカルテストを実行してみて、問題なければ、

1. ECRにレポジトリ作成
1. ECRにログイン
1. TAGづけ
1. ECRにPush、
1. ECRにPushしたイメージを使ってLambdaをデプロイ
となります。
2〜４はコンソールでレポジトリ作成すると、ECR上でコマンド出してくれるので、その通りに実行すれば大丈夫そうです。

デプロイして、テスト実行なりして、動けばOKです。

## 違いあるのかな？

コンテナ利用したLambda関数と普通にjarファイルをアップしてデプロイしたLambda関数で違いあるのかな？っていうのをついでに確認してみました。
両方ともテストイベントの`Hello World` をベースにしたテストイベントを実行してます。

### コンテナ
![スクリーンショット 2020-12-08 19.52.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/0a0d46cb-55ed-2064-263a-f46df3ea17f4.png)

- 初回実行(Cold Start)
![スクリーンショット 2020-12-08 19.56.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/1ab92d39-0bd4-3228-07ee-2f611b513747.png)

- 2度目(Warm Start)
![スクリーンショット 2020-12-08 20.00.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/a57e2420-505f-05d2-7e76-2c41b8a9732b.png)

### jarファイルをデプロイ
![スクリーンショット 2020-12-08 19.52.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/64ec5c27-0218-856d-b2f1-d18e1d289e08.png)

- 初回実行(Cold Start)
![スクリーンショット 2020-12-08 19.56.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/f5b38463-05db-889b-663d-d5e1686a5b2e.png)

- 2度目(Warm Start)
![スクリーンショット 2020-12-08 20.00.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/72752/62330421-bf67-e2b9-03fb-1aeed2dd3782.png)

直前にTwitterで流れてきて、知ったのですが、
コンテナ利用な場合は、Init処理の時間も課金単位に含まれるようです。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Lambda のコンテナイメージパッケージって初期化時間も課金されるのね。カスタムランタイムと同じ扱いか。 <a href="https://t.co/xSrDwI2FPP">pic.twitter.com/xSrDwI2FPP</a></p>&mdash; hayao_k (@hayaok3) <a href="https://twitter.com/hayaok3/status/1336217357702721536?ref_src=twsrc%5Etfw">December 8, 2020</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

カスタムランタイムも同じなんですね。それは知らなかったです。
Java2度目以降は速いですねー（今更感）。
そして、課金単位がちゃんと1ms単位になってる！

## まとめ
AWSJの西谷さんが発表後にブログ[AWS Lambdaがコンテナをサポートしたのでちょっと試してみた](https://www.keisuke69.net/entry/2020/12/02/051538)を書かれており、そこで、
**「あくまでもLambdaの実行モデルであるファンクションモデルはそのままにランタイムの自由度が増した」**
と述べられていますが、その通りかなと思います。

Dockerイメージ用意して、そのままECRにPushして、デプロイできるのは、
Docker上で開発されている開発者にとっては便利なのかもしれませんね。
（普段Docker上で開発してないもので。。。今後の検討課題ではあります）

以前、Amazon Linux２上でビルドしたモジュールを含んだLambdaのパッケージを作ってデプロイするという機会があったのですが、
そういう場合、
Docker上(Amazon Linux2ベースのはず)でモジュールをビルドして、そのままアップロードできるのかなと思ったりしてます。
※あとで試そうかなとは思っています。

ただ、そういう用途でなければ、自分が定常的に使うか。。。と言われると、今のところ、積極的には使わないかなぁーっていうのが結論です。

## おまけ
今年の7月に共著でAWS LambdaをはじめとするServerlessでの開発などについて記した[基礎から学ぶ サーバーレス開発](https://www.amazon.co.jp/dp/486354314X/ref=cm_sw_r_tw_dp_x_pK0ZFbBMPYVKY)という本を書かせていただいたのですが、
コンテナサポート以外にも、Lambda Extensions（これre:Invent落選組だったんだな。。。）、課金単位の変更(100ms->1ms)、メモリ量の増加などのアップデートがあったので、本自体もアップデートしない（書いてた時から、すぐに変わっちゃうんだろうなとは思っていましたが）とダメだなぁ。。。なんて思った人です。

----

検証して一気に書き上げたので、至らぬ点があるかもしませんが、ご容赦ください。。。