Dockerfile不要でコンテナイメージが作成できるBuildpacksを使ってコンテナイメージを作ってみました。
「Java」タグの記事が6件件あります
全てのタグを見るLambda SnapStartに関するちょっとしたメモ(2022/12)
re:Invent 2022の期間中にLambdaのコールドスタート問題を解消する大きなアップデートが発表されました。
New – Lambda SnapStart で Lambda 関数を高速化
過去に検証した内容のように、特にJavaの場合のコールドスタートに時間がかかる問題があり、この解消に有効とのことです。
追加費用もなく利用できるとのことなので、常に有効化でよいのではないかと考えていたのですが、いくつか制限がありそうです。
Log4Shellに脆弱な環境をSpring Bootでつくる
過去にQiitaに投稿した内容のアーカイブです。
勉強がてら、話題のLog4Shellに脆弱な環境を作ってみます。 Log4j2のバージョン2.14.0を使っていると簡単に脆弱な環境を作れました。すぐにバージョンアップしないと危険です!
脆弱性に対するSpring公式サイトの解説
https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot
題材
公式のGetting Started Guidesの中にあるBuilding a RESTful Web Serviceをベースにします。
- GitHubからソースコードの取得
https://github.com/spring-guides/gs-rest-service/archive/main.zip
complete
フォルダー内に完成したソースが含まれます。
│ build.gradle
│ gradlew
│ gradlew.bat
│ manifest.yml
│ mvnw
│ mvnw.cmd
│ pom.xml
│ settings.gradle
│
├─.mvn
│ └─wrapper
│ maven-wrapper.jar
│ maven-wrapper.properties
│
├─gradle
│ └─wrapper
│ gradle-wrapper.jar
│ gradle-wrapper.properties
│
└─src
├─main
│ └─java
│ └─com
│ └─example
│ └─restservice
│ Greeting.java
│ GreetingController.java
│ RestServiceApplication.java
│
└─test
└─java
└─com
└─example
└─restservice
GreetingControllerTests.java
ログ出力にLog4J2を使うように修正
Log4j2を使うように変更します。バージョンは脆弱性のある2.14.0
です。
https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto.logging.log4j
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
<relativePath /> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>rest-service-complete</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>rest-service-complete</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
+ <log4j2.version>2.14.0</log4j2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter</artifactId>
+ <exclusions>
+ <exclusion>
+ <groupId>org.springframework.boot</groupId>
+ <artifactId>spring-boot-starter-logging</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
HTTPリクエストのHTTPヘッダーをログ出力するように変更
package com.example.restservice;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
+ import java.util.Map;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
+ import org.springframework.web.bind.annotation.RequestHeader;
@RestController
public class GreetingController {
+ static protected Logger logger = LoggerFactory.getLogger(GreetingController.class);
private static final String template = "Hello, %s!";
private final AtomicLong counter = new AtomicLong();
@GetMapping("/greeting")
- public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name) {
+ public Greeting greeting(@RequestParam(value = "name", defaultValue = "World") String name,
+ @RequestHeader Map<String, String> headers) {
+
+ headers.forEach((key, value) -> {
+ logger.info(String.format("Header '%s' = %s", key, value));
+ });
return new Greeting(counter.incrementAndGet(), String.format(template, name));
}
}
なんとこれだけで、脆弱です。
起動します。
./mvnw spring-boot:run
確認
トレンドマイクロさんが、チェックサイトを用意しているのを見つけましたので、これを使って確認してみます。
https://log4j-tester.trendmicro.com/
こんな感じで、サイトから直接リクエストを投げてチェックできます。 今回はlocalhostで起動しているので、右側のcURLスクリプトを生成して、実行します。
結果はこんな感じで表示されます。
Log4J2の2.14.0
を使っていると脆弱であることがわかります。
pom.xml
のバージョンを2.15.0
にあげて再実行すると、脆弱性は検出されませんでした。
怖いですね。すぐにバージョンアップしましょう。
QuarkusでAWS Lambdaを作る
過去にQiitaに投稿した内容のアーカイブです。
Quarkus(https://quarkus.io/) とはSUPERSONIC SUBATOMIC JAVA
で、
A Kubernetes Native Java stack tailored for OpenJDK HotSpot and GraalVM, crafted from the best of breed Java libraries and standards.
だそうです。 何言ってるかわかりませんがすごそうです。
GraalVMの機能でJavaのプログラムをネイティブに変換することが可能なので、AWS Lambdaのカスタムランタイムと組み合わせることで AWS Lambda上で動作するJavaアプリケーションの特性である コールドスタートが遅い問題 の解決に期待ができます。
公式サイトの手順に従い試してみましたので、手順を残します。
QUARKUS - BUILDING A NATIVE EXECUTABLE https://quarkus.io/guides/building-native-image
QUARKUS - AMAZON LAMBDA https://quarkus.io/guides/amazon-lambda
環境
- Docker Desktop (Mac) 2.3.0.4
- VSCode + Visual Studio Code Remote - Containers extension
- Amazon Linux 2 (on Docker)
雛形アプリのデプロイ手順
手順1. Amazon Linux 2の環境設定
DockerイメージのAmazon Linux 2はtar
やunzip
ができないので色々入れておきます。
全部必要かわからないですが、これぐらい入れておきました。
yum install -y sudo shadow-utils procps tar.x86_64 gzip xz unzip witch git python3 tree
手順2. GraalVMのインストール
公式サイトからダウンロードして展開します。
curl -s -L -o /tmp/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-20.1.0/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz
tar zxf /tmp/graalvm-ce-java11-linux-amd64-20.1.0.tar.gz -C /opt/
ln -s /opt/graalvm-ce-java11-20.1.0 /opt/graalvm
JAVA_HOME
をGraalVMにして、MavenでのビルドにGraalVMを使用します。
export GRAALVM_HOME=/opt/graalvm
export JAVA_HOME=$GRAALVM_HOME
export PATH=$GRAALVM_HOME/bin:$PATH
最後にNativeビルド時に必要なnative-image
をインストールします。コマンドはgu
(GraalVM Updater)です。
gu install native-image
手順3. Mavenのインストール
Quarkusのビルドで使用するMavenは3.6.2以上のバージョンが必要です。yum
でインストールできるバージョンが古かったので、公式サイトからダウンロードしてインストールしました。
curl -s -L -o /tmp/apache-maven-3.6.3-bin.tar.gz https://downloads.apache.org/maven/maven-3/3.6.3/binaries/apache-maven-3.6.3-bin.tar.gz
tar zxf /tmp/apache-maven-3.6.3-bin.tar.gz -C /opt/
ln -s /opt/apache-maven-3.6.3 /opt/apache-maven
mvnのバージョン確認
bash-4.2# mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: /opt/apache-maven
Java version: 11.0.7, vendor: GraalVM Community, runtime: /opt/graalvm-ce-java11-20.1.0
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "4.19.76-linuxkit", arch: "amd64", family: "unix"
bash-4.2#
GraalVMのJVMで動作していることがわかります。
手順4. Mavenでプロジェクト作成
mvn
コマンドでプロジェクトを生成します。
mvn archetype:generate \
-DarchetypeGroupId=io.quarkus \
-DarchetypeArtifactId=quarkus-amazon-lambda-archetype \
-DarchetypeVersion=1.6.0.Final
しばらくするとプロンプトで質問されますので答えます。 []で囲んだ部分がユーザー入力です。
Define value for property 'groupId': [myGroup]
Define value for property 'artifactId': [myArtifact]
Define value for property 'version' 1.0-SNAPSHOT: : []
Define value for property 'package' myGroup: : [example]
Confirm properties configuration:
groupId: myGroup
artifactId: myArtifact
version: 1.0-SNAPSHOT
package: example
Y: : [Y]
artifactId
(myArtifact)でディレクトリが作成され、プロジェクトが生成されます。
作成直後のプロジェクト構成はこんな感じ。
bash-4.2# tree myArtifact/
myArtifact/
├── build.gradle
├── gradle.properties
├── payload.json
├── pom.xml
├── settings.gradle
└── src
├── main
│ ├── java
│ │ └── example
│ │ ├── InputObject.java
│ │ ├── OutputObject.java
│ │ ├── ProcessingService.java
│ │ ├── StreamLambda.java
│ │ ├── TestLambda.java
│ │ └── UnusedLambda.java
│ └── resources
│ └── application.properties
└── test
├── java
│ └── example
│ └── LambdaHandlerTest.java
└── resources
└── application.properties
9 directories, 14 files
bash-4.2#
手順5. Lambdaで起動するHandlerの設定
Lambdaで呼び出されるHandlerはresources/application.properties
のquarkus.lambda.handler
にて設定します。
quarkus.lambda.handler=test
上記設定の場合は、以下のtest
と名前をつけたクラスが呼び出されます。
@Named("test")
public class TestLambda implements RequestHandler<InputObject, OutputObject> {
}
あとは通常のLambdaと同様にコーディングします。
雛形にはtest
の他にstream
なども用意されています。
手順6. デプロイパッケージの作成
一旦雛形のままデプロイパッケージを作成してみます。
mvn clean package -Pnative
めちゃくちゃ時間がかかります。10分以上かかると思います。
手順7. デプロイパッケージの内容の確認
無事デプロイパッケージができると、target/function.zip
が生成されいます。試しに展開してみると、中身はbootstrap
のみでした。
bash-4.2# unzip function.zip
Archive: function.zip
inflating: bootstrap
bash-4.2#
手順8. Lambdaのデプロイ
target
ディレクトリ内には、manage.sh
というファイルも生成されており、ここからAWS上にデプロイしたりできるようです。
私はカスタムランタイムが初めてだったこともあり、マネジメントコンソールから試してみました。
デプロイパッケージ作成後に、target/sam.native.yaml
というファイルも生成されるので、ハンドラー名や環境変数はこちらを参考にしました。
関数を新規作成します。
ランタイムをユーザー独自のブートストラップを提供する
とします。
関数を作成したら、プログラムをアップロードします。
関数コードのところのアクション
から.zipファイルをアップロード
を選び、function.zip
を選択します。
ハンドラーはnot.used.in.provided.runtime
となります。
環境変数にDISABLE_SIGNAL_HANDLERS
を追加し値をtrue
とします。
ここまでで設定は完了です。以下のJSONをインプットにテスト実行してみましょう。
{
"name": "Bill",
"greeting": "hello"
}
動作結果がこちら。無事動きました。
AWS SDK を追加
AWS SDKを使用するには単純にpom.xmlに追加するだけでなく、いくつか設定が必要です。
手順1. SSL通信の有効化
resources/application.properties
に以下の内容を追加します。(公式ドキュメント的にはデフォルトで有効って書いてある気もしますが)
quarkus.ssl.native=true
手順2. 依存関係の追加
まずは、quarkus-jaxb
が必要とのことで、これを追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jaxb</artifactId>
</dependency>
次にAWS SDKのライブラリーを追加するのですが、
For native image, however the URL Connection client must be preferred over the Apache HTTP Client when using synchronous mode, due to issues in the GraalVM compilation (at present).
翻訳すると
ただし、ネイティブイメージの場合、GraalVMコンパイルの問題(現在)のため、同期モードを使用するときは、Apache HTTPクライアントよりもURL接続クライアントを優先する必要があります。
だそうです。そのため、以下のような記述となります。
<properties>
<aws.sdk2.version>2.10.69</aws.sdk2.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>bom</artifactId>
<version>${aws.sdk2.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>url-connection-client</artifactId>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<exclusions>
<!-- exclude the apache-client and netty client -->
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>apache-client</artifactId>
</exclusion>
<exclusion>
<groupId>software.amazon.awssdk</groupId>
<artifactId>netty-nio-client</artifactId>
</exclusion>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>commons-logging-jboss-logging</artifactId>
<version>1.0.0.Final</version>
</dependency>
</dependencies>
手順3. Javaコードの作成
S3にアクセスする例ですが、S3Clientを生成する際に、httpClient
にて明示的にUrlConnectionHttpClient
を指定します。
S3Client s3 = S3Client.builder()
.region(Region.AP_NORTHEAST_1)
.httpClient(software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient.builder().build())
.build();
手順4. SSL通信に必要な設定
SSL通信を行うためにデプロイパッケージに以下を含める必要があります。
- カスタムBootstrap
- libsunec.so
- cacerts
まず、src/main/zip.native/
ディレクトリを作成し、bootstrap
を作成します。
#!/usr/bin/env bash
./runner -Djava.library.path=./ -Djavax.net.ssl.trustStore=./cacerts
次にlibsunec.so
とcacerts
をコピーします。この2つのファイルはGraalVMに含まれています。
cp $GRAALVM_HOME/lib/libsunec.so $PROJECT_DIR/src/main/zip.native/
cp $GRAALVM_HOME/lib/security/cacerts $PROJECT_DIR/src/main/zip.native/
手順5. デプロイパッケージの作成
デプロイパッケージの作成手順は変わりません。
mvn clean package -Pnative
手順6. デプロイパッケージの内容の確認
SSL有効化した状態でデプロイパッケージを作成すると、target/function.zip
の中身が変わります。
bash-4.2# unzip function.zip
Archive: function.zip
inflating: bootstrap
inflating: cacerts
inflating: libsunec.so
inflating: runner
bash-4.2#
事前準備したbootstrap``cacerts``libsunec.so
が含まれているのがわかります。
終わりに
通常のJavaランタイムよりも、コールドスタートが早くなる検証結果はこちらで確認ください。
QuarkusがJava Lambdaを救う!?
過去にQiitaに投稿した内容のアーカイブです。
すごいですQuarkus!!!!!! JavaのLambdaのコールドスタートがチョッパヤです!!
以下の公式サイトを参考に試しました。
https://quarkus.io/guides/amazon-lambda#tracing-with-aws-xray-and-graalvm
テスト対象
前回のAWS LambdaのJavaは遅い?とほぼ同じですが、以下が異なります。いずれもQuarkusの制限?制約?です。
- アノテーションが付いてる
- httpClientがUrlConnectionHttpClient
ソース全体
package example;
import javax.inject.Named;
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.regions.Region;
@Named("test")
public class TestLambda implements RequestHandler<Object, Object> {
@Override
public Object handleRequest(Object input, Context context) {
String ENV_BUCKET = System.getenv("BUCKET");
S3Client s3 = S3Client.builder()
.region(Region.AP_NORTHEAST_1)
.httpClient(software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient.builder().build())
.build();
PutObjectResponse result = s3.putObject(
PutObjectRequest.builder().bucket(ENV_BUCKET).key("filename.txt").build(),
RequestBody.fromString("contents"));
System.out.println(result);
return "";
}
}
検証結果
回数 | レイテンシ(ms) | 処理内容 |
---|---|---|
1 | 2700 | |
2 | 250 | |
3 | 305 | |
4 | 319 | |
5 | 187 |
前回はコールドスタートが6200msだったので、めちゃ早になりました!
検証結果(+Provisioned Concurrency)
こんなチョッパヤなQuarkusさんをProvisionedにしたらどうなるんでしょうね。ワクワクしますね。
回数 | レイテンシ(ms) | 処理内容 |
---|---|---|
1 | 417 | |
2 | 198 | |
3 | 206 | |
4 | 270 | |
5 | 147 |
期待を裏切らない速さ!すごいよ、Quarkus!
参考
https://quarkus.io/guides/amazon-lambda#tracing-with-aws-xray-and-graalvm https://aws.amazon.com/jp/blogs/architecture/field-notes-optimize-your-java-application-for-aws-lambda-with-quarkus/