カテゴリー: Lambda

Kotlin/JSのAWS Lambda関数でJsonを返してみる

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 Promiseを利用して非同期に処理するようにしてみました
今回はこのLambda関数で kotlinx.serialization を用いてJsonを返すようにしてみたいと思います

目次

過去の記事

なぜ kotlinx.serialization?

まず kotlinx.serialization を用いる理由です
ここまでの記事を読んでくださっている方々であれば “わざわざないでしょ” と思っている方もいると思います
その通りで, json()JSON.stringifiy() 関数を用いればJsonを返すことができます
しかし, kotlinx.serialization を用いることにより, Client側でも同じクラスを利用することができ, 開発する際に便利であるといえます
またマルチプラットフォームであるため様々な環境で利用できるというのもメリットです

注意事項

この記事では kotlinx.serialization に関する詳しい解説は行いません
詳しく知りたい方は下記のリポジトリをご覧ください
github.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.promise
import kotlin.js.Json
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val tasks = (1..10).map {
async {
delay(1000)
it
}
}
val results = tasks.awaitAll()
return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

Jsonを返すようにする

目標

今回は下記のようなJsonを返すようにしてみます

{
"id": 0, // ID
"text": "", // 文字列
"random_numbers": [5, 38, 24] // 適当な数値
}

依存関係を追加する

kotlinx.serialization は外部ライブラリですので依存関係を追加する必要があります
また, 専用のプラグインも必要であるためプラグインの追加も行います

build.gradle.kts / pluginsブロック

plugins {
kotlin("js") version "1.3.50"
kotlin("plugin.serialization") version "1.3.50"
}

依存関係の追加

build.gradle.kts / dependenciesブロック

sourceSets["main"].dependencies {
implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:0.13.0") // 追加
}

リポジトリに jcenter を追加する必要があるので追加します

build.gradle.kts / repositories

repositories {
mavenCentral()
jcenter() // 追加
}

クライアント側に返すクラスを作成する

作成するクラスが今回のJsonに変換するクラスとなるので各種アノテーションも付与します
今回は Responseクラス の下に作成します(本当は別ファイルにするほうがいいです)

handler.kt

@Serializable
data class ResponseBody(
@SerialName("id")
val id: Int = 0,
@SerialName("text")
val text: String = "",
@SerialName("random_numbers")
val randomNumbers: List<Int> = emptyList()
)
  • @Serializable これがついているクラスがシリアライズ/デシリアライズすることができます
  • @SerialName(String) 引数で決められた文字列がkeyとなるようになります, ない場合は変数名が利用されるようになるのでなくても問題ないです

Jsonを返す準備をする

まずは前回までに書いていたコードの一部を削除しスッキリさせます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
return@promise Response(body = "")
}

続いて, 先ほど作ったクラスを文字列に変換するために kotlinxのJsonオブジェクト を作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable) // 追加
return@promise Response(body = "")
}

⚠注意1
今回の記事ではすでにkotlinが実装している Jsonクラス をインポートしているため同じ名前であるkotlinxの Jsonクラス をインポートできません
kotlinのJsonkotlinxのJson とを間違わないよう注意してください

⚠注意2
JsonConfiguration には Stable 以外の設定もありますが, 実験的機能となっていますので利用はお勧めしません

Jsonを返す

まずは先ほど作成した ResponseBodyクラス のオブジェクトを作成します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) }) // 追加
return@promise Response(body = "")
}

最後に stringify関数 を用いてクラスを文字列に変換し レスポンスのbody に設定します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val json = kotlinx.serialization.json.Json(JsonConfiguration.Stable)
val responseBody = ResponseBody(1, "Lambda Json!!!", List(3) { Random.nextInt(100) })
return@promise Response(body = json.stringify(ResponseBody.serializer(), responseBody)) // 変更
}
  • ResponseBody.serializer() この関数は @Serializable アノテーションがついているクラスの場合コンパイラが自動的に作成してくれます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし {"id":1,"text":"Lambda Json!!!","random_numbers":[ランダムな数字が3つ]} 表示されれば成功です

最後に

いかがでしたでしょうか
LambdaでJsonを返すのはありがちです
これを利用することによりKotlinで作成するマルチプラットフォームなアプリケーションではかなり強力になりうると考えます
また, ここまで4つの記事に分け Kotlin/JS をもちいて AWS Lambda関数 を作って便利にしてみました
これがベストプラクティスかどうかはプロジェクトによると思いますが、1つの参考になるとうれしい限りです
ここまで読んでいただきありがとうございました


蛇足

JSON.stringify() を使ってJsonを返してみる

JavaScriptにもとから存在する, JSON.stringify を用いてもJsonを返すことができます

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = ResponseBody(1, "Lambda Json!!!", listOf(0, 1, 2))
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","randomNumbers":[0,1,2]}

しかしこれではJsonのKeyは指定できないので変数をすべて private に変更すると結果が変わるので注意が必要です

privateに変更したときの実行結果

{"id_0":1,"text_0":"Lambda Json!!!","randomNumbers_0":[0,1,2]}

こういうケースもあるため kotlinx.serialization を用いることをお勧めします

json() と JSON.stringify() を使ってJsonを返してみる

一番オーソドックスでわかりやすい返し方だと私は思います

handler.js

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val responseBody = json(
"id" to 0,
"text" to "Lambda Json!!!",
"random_numbers" to listOf(0, 1, 2)
)
return@promise Response(body = JSON.stringify(responseBody))
}

実行結果

{"id":1,"text":"Lambda Json!!!","random_numbers":[0,1,2]}

蛇足まで読んでいただきありがとうございました

Kotlin/JSのAWS Lambda関数でPromiseを使うようにする

f:id:seeds-std:20191010160615p:plain
こんにちは, Web事業部の西村です
私の前回の記事 では dyanmic の利用をなくすようにしました
そして今回は, Callbackの呼び出しをやめて, JavaScriptの非同期処理( Promise )になるよう変更したいと思います
また, Promiseスタンダード なものと Coroutines のものの2種類がありますので, この記事ではその両方を紹介したいと思います

目次

過去の記事

なぜ非同期に?

非同期処理することにより複数の処理を同時に実行することができます
例えば, 複数のファイルを受け取り, S3に保存する といったことを考えてみます
非同期処理ではない場合, 1ずつしかファイルのアップロードができません
非同期処理の場合, 複数のファイルを同時にアップロードできるようになるので時間の短縮が図れます

注意事項

この記事では Promise の詳しい解説は行いません
詳しく知りたい方は下記の記事を参考にしてください
qiita.com

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 まで作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

また, handler.ktは下記のようになっています

package jp.co.seeds_std.lambda.kotlin
import kotlin.js.Json
import kotlin.js.Promise
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

スタンダードなPromise

まずはkotlin-stdlibに実装されている Promise を利用してみます

関数の書き換え

handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
executor(Response(body = "Hello from Kotlin Async Lambda!"))
}
  • executor は処理成功時に実行する関数で, 引数はジェネリクスで指定したクラス (この記事では Response ) となります
  • reject は処理失敗時に実行する関数で, 引数は Throwable クラスのオブジェクトとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Async Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(スタンダード)

今度はNode.jsの setTimeout を利用して本当に非同期に処理されているかを確認したいと思います
その前に, Kotlin/JSにはNode.jsの setTimeout は定義されていないので追加します
今回追加する場所はコードの最下部に追加します

handler.kt

external fun setTimeout(callback: dynamic, delay: Int, vararg args: Any?): Int
external fun clearTimeout(timeout: Int)
  • external : 外部の関数/変数を利用するために記述します。トランスパイルした場合にそのまま残るようになります
  • callback の部分が dynamic となっていますが, 関数の引数が固定値ではないためやむを得ず dynamic としています

続いて handler 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = Promise<Response> { executor, reject ->
val results = (1..10).map {
Promise<Int> { childExecutor, _ ->
setTimeout({childExecutor(it)}, 1000)
}
}
Promise.all(results.toTypedArray()).then {
executor(Response(body = "Result: ${it.sum()} - Standard"))
}
}

このコードでは 1秒待ってから数字を返す というコードを10回繰り返すものとなっています

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Standard と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

通常の処理であれば 1秒待って数字を返す を10回行えば10秒かかるはずですが, 非同期に実行されているため約1秒で完了します

CoroutinesのPromise

標準実装の Promise では見にくく感じませんでしたか?(私は少なからず見にくいなと感じました)
非同期処理を書くのであれば, Node.jsみたいに async で書きたいところです
そこで使うのが kotlinx.coroutines です

依存関係を追加する

kotlinx.coroutines は外部ライブラリなので build.gradle.kts に依存関係を追加する必要があります

build.gradle.kts

implementation(kotlin("stdlib-js"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.2") // 追加

コードを書き換える

まずはただの文字列を返すように handelr 関数を変更します

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
return@promise Response(body = "Hello from Kotlin Coroutines Lambda!")
}

先ほどのコードと比較するとかなり単調なものになりました
※エラーを返す際は throw NullPointerException() のように throw するだけとなります

動作確認

関数をデプロイし, API GatewayのURLにアクセスし Hello from Kotlin Coroutines Lambda! と表示されれば成功です

もう少し恩恵を受けてみる(Coroutines)

先ほどと同じ関数を実装してみます

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json) = GlobalScope.promise {
val tasks = (1..10).map {
async {
delay(1000)
it
}
}
val results = tasks.awaitAll()
return@promise Response(body = "Result: ${results.sum()} - Coroutines")
}

先ほどとは異なり, CoroutineScope 内で実行できるため, async 関数を利用できます

動作確認

関数をデプロイし, API GatewayのURLにアクセスし約1秒後に Result: 55 - Coroutines と表示されれば成功です
※初回の実行では2-3秒ほどかかる場合もあります

最後に

いかがでしたでしょうか
非同期処理が行えるとよりできることの幅が増えるかと思います
また、Coroutinesの Promise と標準実装の Promise は互換性があるため外部ライブラリが非同期処理を行う場合にも使え,
よりKotlinらしい書き方ができるようになっていくと思います
次回は kotlinx.serialization を用いてJsonのレスポンスを行う記事を書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数でJsonを返してみる

Kotlin/JSのAWS Lambda関数を便利にしてみる

f:id:seeds-std:20191010160615p:plain

こんにちは, Web事業部の西村です
私の前回の記事 ではKotlin/JSを用いてLambda関数を書いてみました
しかし, dynamic を利用していたため使いにくい部分もあったと思います
今回はその点を改良しより使いやすくなるよう変更したいと思います

目次

過去の記事

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクト

前回の記事 で作成したプロジェクトを利用します
プロジェクトの構成は下記のようになっています

KotlinLambda
├─.gradle/
├─.idea/
├─build/
├─gradle/
├─src/
│ └─main/
│   └─kotlin/
│     └─jp.co.seeds_std.lambda.kotlin/
│       └─handler.kt
├─build.gradle.kts
├─compress.zip
├─gradlew
├─gradlew.bat
└─settings.gradle.kts

Jsonを使って便利にしてみる

dynamicをなくす

Kotlin/JSには dynamic に似た Json があります
dynamic はJavaScriptのデータであったり, オブジェクトを格納することができますが, Json はKey-Value形式でのみ格納できるものとなっています
handler関数の引数, eventcontext はどちらもJavaScriptのJsonオブジェクトですのでKotlinの Json に変換できます
また, Callbackの第二引数もJsonオブジェクトを設定するためこちらも変更できます

handler.kt

fun handler(event: dynamic, context: dynamic, callback: (Error?, dynamic) -> Unit)

↑ が ↓ に変更できます

fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit)

このJsonオブジェクトへのアクセスはMapのように利用してアクセスすることができます

event["body"] // こちらか
event.get("body") // こちらでデータの取得ができます

Callbackに渡すのJsonを関数で作れるようにする

API Gatewayでは下記のJsonを処理するようになっています *1

{
"isBase64Encoded": true|false,
"statusCode": httpStatusCode,
"headers": { "headerName": "headerValue", ... },
"multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... },
"body": "..."
}

こちらをもとに引数が5つの関数を作成したいと思います
記述する場所はhandler関数の下になります

handler.kt

fun responseJson(statusCode: Int = 200, body: String = "{}", isBase64Encoded: Boolean = false, headers: Json = json(), multiValueHeaders: Json = json()) = json(
"statusCode" to statusCode,
"body" to body,
"isBase64Encoded" to isBase64Encoded,
"headers" to headers,
"multiValueHeaders" to multiValueHeaders
)
  • json() Jsonを作成する関数です/引数は vararg Pair<String, Any?> となっており json() と書くと空のJsonを作ることができます

Callbackに渡すよう変更する

現在handler関数は下記のようになっていると思います

handler.kt

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response: dynamic = object {}
response["statusCode"] = 200
response.body = "Hello from Kotlin Lambda!"
callback(null, response)
}

この関数を先ほど作成した関数を用いた形に変更します

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response = responseJson(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}

関数をデプロイする

前回と同様の手順を用いて関数をデプロイします
画面右側の Gradle タブを開き, kotlin_lambda -> Tasks -> other の順に開き, その中の compress をダブルクリックして実行します
ビルドに成功するとプロジェクトのディレクトリに compress.zip というファイルが更新されます
この生成されたファイルをコンソールからアップロードし, 保存します

動作確認

あらかじめ作成しておいたAPI GatewayのURLにアクセスし Hello from Kotlin Json Lambda! と表示されれば成功です

レスポンスをクラスに変更する

レスポンスはクラスを用いても影響が少ないためクラスに置き換えます
リクエストにクラスを利用しない理由については蛇足をご覧ください

まずは responseJson関数 を変更します

handler.kt

fun responseJson(statusCode: Int = 200, body: String = "{}", isBase64Encoded: Boolean = false, headers: Json = json(), multiValueHeaders: Json = json()) = json(
"statusCode" to statusCode,
"body" to body,
"isBase64Encoded" to isBase64Encoded,
"headers" to headers,
"multiValueHeaders" to multiValueHeaders
)

data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

続いて, Callbackの引数を変更します

fun handler(event: dynamic, context: dynamic, callback: (Error?, Json?) -> Unit)

fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit)

最後にレスポンスを変更します

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Json?) -> Unit) {
val response = responseJson(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}

@JsExport
@JsName("handler")
fun handler(event: Json, context: Json, callback: (Error?, Response?) -> Unit) {
val response = Response(body = "Hello from Kotlin Class Lambda!")
callback(null, response)
}

この変更ができたらデプロイし動作を確認します
Hello from Kotlin Class Lambda! と表示されれば成功です

最後に

いかがでしたでしょうか
Kotlinを用いていると dynamic では少し取り扱いにくく感じてしまいます
しかし Json にしてしまうことで,Mapのように利用でき, より使いやすいのではないかと思います
次回は非同期処理にする記事を書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数でPromiseを使うようにする


蛇足

リクエストのほうもクラス使ったらだめなの?

注意事項はありますが、クラスを利用しても問題はないです

handler.kt

package jp.co.seeds_std.lambda.kotlin
import kotlin.js.Json
import kotlin.js.json
@JsExport
@JsName("handler")
fun handler(event: Event, context: Json, callback: (Error?, Response?) -> Unit) {
println("body: ${event.body} isBase64Encoded: ${event.isBase64Encoded}")
// event.copy() // Error!
val response = Response(body = "Hello from Kotlin Json Lambda!")
callback(null, response)
}
data class Event(val body: String?, val isBase64Encoded: Boolean)
data class Response(
val statusCode: Int = 200,
val body: String = "{}",
val isBase64Encoded: Boolean = false,
val headers: Json = json(),
val multiValueHeaders: Json = json()
)

この時ログに bodyisBase64Encoded の出力が行われます
その後 copy() を実行しようとした場合, 変数 eventEventクラスではなく Json となっていますので, 関数が見つからず実行時にエラーが出てしまいます
また, 注意点として private な変数はトランスパイルした際に変数名が変更されてしまうためうまく利用できません
そのため, リクエストの eventcontext はJsonで, レスポンスはKotlinのクラスを利用していくことがいいのかなと私は思います
蛇足も読んでいただきありございました

AWS Lambda関数をKotlin/JSで書いてみる

f:id:seeds-std:20190925123639p:plain

WEB事業部の西村です。

唐突ですが皆さんはLambda関数を作成する際にどのような言語を用いていますでしょうか?
用途に合った言語や自分で書きやすいと思う言語などの要因で決めていると思われます
私はKotlinという言語が好きでKotlinで書こうと思ったのですが、Kotlinで書かれている記事はそのほとんどがJavaで実行することを前提とした元なっています
しかし、Javaではコールドスタートした際に実行時間がかかってしまうというデメリットがあります
KotlinはJavaだけでなくJavaScriptでも動作させることができるので今回はNode.jsで実行させる関数の作成を行いたいと思います

目次

この記事の目標

Kotlin/JSを用いたLambda関数の作成とそのレスポンス確認

この記事で取り扱わないこと

この記事では AWS LambdaAmazon API Gateway については詳しく解説いたしません
あらかじめLambda関数とその関数を呼び出すためのAPIの作成を行っておいてください

開発環境

この記事では下記の環境で開発を行っています

  • AdoptOpenJDK 1.8.0_222-b10
  • IntelliJ IDEA Community 2019.2.2
  • Kotlin 1.3.50

プロジェクトの作成

プロジェクトの作成を行います
任意のディレクトリにプロジェクト用のフォルダを作成します(この記事では KotlinLambda としています)
その後そのディレクトリ内に build.gradle.ktssettings.gradle.kts の2つのファイルを作成します

KotlinLambda
├─build.gradle.kts
└─settings.gradle.kts

作成したらIntelliJ IDEAでプロジェクトのフォルダを開きます
続いて、 build.gradle.ktssettings.gradle.kts を開き下記の内容を入力します

build.gradle.kts

plugins {
kotlin("js") version "1.3.50"
}
repositories {
mavenCentral()
}
kotlin {
target {
nodejs()
useCommonJs()
}
sourceSets["main"].dependencies {
implementation(kotlin("stdlib-js"))
}
}

settings.gradle.kts

rootProject.name = "kotlin_lambda"

その後画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら準備完了です

ハンドラソースコードの追加

次にソースディレクトリを作成します
プロジェクトのディレクトリに src/main/kotlin でディレクトリを作成します
またパッケージ名を追加する場合、パッケージを作成します(この記事では jp.co.seeds_std.lambda.kotlin とします)
作成したディレクトリに handler.kt というファイルを作成し下記の内容を記述します

handler.kt

package jp.co.seeds_std.lambda.kotlin
@JsExport
@JsName("handler")
fun handler(event: dynamic, context: dynamic, callback: (Error?, dynamic) -> Unit) {
val response: dynamic = object {}
response["statusCode"] = 200
response.body = "Hello from Kotlin Lambda!"
callback(null, response)
}
  • @JsExport JavaScriptのexportを行うアノテーションです
  • @JsName(name: String) 記述したクラスや関数をトランスパイルした際 name に指定した名前となるよう変換してくれるようになります
  • dynamic JavaScriptのオブジェクトをそのまま取り扱います(. [] でその中の関数や変数にアクセスできます)

デプロイ

デプロイパッケージの作成を行います
圧縮を自動化するためのタスクを追加します
build.gradle.kts を再度開き下記の内容を追加します

build.gradle.kts

import com.google.gson.JsonParser
import java.io.OutputStream
import java.util.zip.ZipOutputStream
import java.util.zip.ZipEntry
plugins { ... }
repositories { ... }
kotlin { ... }
open class CompressTask : DefaultTask() {
lateinit var buildDirectory: File
lateinit var projectName: String
var rootProjectName: String? = null
private val blackLists = setOf("kotlin-test-nodejs-runner", "kotlin-test")
@TaskAction
fun compress() {
val projectPrefix = if(rootProjectName != null && rootProjectName != projectName) "$rootProjectName-" else ""
val zipFile = File(buildDirectory, "../compress.zip")
val outputStream = ZipOutputStream(zipFile.outputStream(), Charsets.UTF_8).apply {
setLevel(9)
}
val jsDir = File(buildDirectory, "js")
val projectDir = File(jsDir, "packages/$projectPrefix$projectName")
val nodeModuleDir = File(jsDir, "node_modules")
addDependencies(outputStream, File(projectDir, "package.json"), nodeModuleDir, mutableSetOf(*blackLists.toTypedArray()))
addZipEntry(outputStream, File(projectDir, "kotlin/$projectPrefix$projectName.js"))
outputStream.close()
}
private fun addZipEntry(zipOutputStream: ZipOutputStream, file: File, addDirectory: String = "") {
val name = if(addDirectory.isEmpty()) file.name else "$addDirectory/${file.name}"
if(file.isDirectory) {
file.listFiles()?.forEach {
addZipEntry(zipOutputStream, it, name)
}
} else {
val zipEntry = ZipEntry(name)
zipOutputStream.addZipEntry(zipEntry) {
it.writeFile(file)
}
}
}
private fun addDependencies(zipOutputStream: ZipOutputStream, packageJsonFile: File, nodeModuleDir: File, addedDependencies: MutableSet<String> = mutableSetOf()) {
val packageJson = JsonParser().parse(packageJsonFile.reader()).asJsonObject
if(!packageJson.has("dependencies")) return
val dependencies = packageJson.getAsJsonObject("dependencies")
dependencies.keySet().forEach {
if(!addedDependencies.contains(it)) {
addedDependencies.add(it)
val dependenciesDir = File(nodeModuleDir, it)
addZipEntry(zipOutputStream, dependenciesDir, "node_modules")
addDependencies(zipOutputStream, File(dependenciesDir, "package.json"), nodeModuleDir, addedDependencies)
}
}
}
private inline fun ZipOutputStream.addZipEntry(entry: ZipEntry, crossinline block: (OutputStream) -> Unit) {
try {
putNextEntry(entry)
block(this)
} catch (e: Exception) {
e.printStackTrace()
} finally {
closeEntry()
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun OutputStream.writeFile(file: File) {
file.inputStream().use {
it.copyTo(this)
}
}
}
tasks.register("noMainCall") {
doFirst {
kotlin.target.compilations.all { compileKotlinTask.kotlinOptions.main = "noCall" }
}
}
tasks.register<CompressTask>("compress") {
dependsOn("compileKotlinJs", "noMainCall")
this.buildDirectory = rootProject.buildDir
this.projectName = project.name
this.rootProjectName = rootProject.name
}
tasks["compileKotlinJs"].mustRunAfter("noMainCall")

追加後、画面右下に出ている Gradle projects need to be importedImport Changes をクリックします
CONFIGURE SUCCESSFUL と出てきたら画面右側の Gradle タブを開きます
kotlin_lambda -> Tasks -> other の順に開き、その中の compress をダブルクリックして実行します
するとプロジェクトのディレクトリに compress.zip というファイルが生成されます
この生成されたファイルをコンソールからアップロードします
その後の ハンドラkotlin_lambda.{パッケージ名}.handler に変更します (この記事では kotlin_lambda.jp.co.seeds_std.lambda.kotlin.handler となります)
アップロードと変更が完了したら保存を行います

動作確認

あらかじめ作成しておいたAPI GatewayのURLにアクセスし Hello from Kotlin Lambda! と表示されれば成功です

最後に

いかがでしたでしょうか
Javaの時より動作も軽くなったと感じるのではないかと思います
次回はもう少し使いやすくなるような記事も書きたいと思います
ここまで読んでいただきありございました

Kotlin/JSのAWS Lambda関数を便利にしてみる

AWS サーバレスでlibreofficeを使ってExcelをPDF変換

クラウド事業部の川勝です。

弊社のプロジェクトのいくつかでは、帳票のPDF出力にExcelで作成したものをPDF変換かけて使用するというものがあります。
Excel->PDF変換にはlibreoffceのheadlessモードを使用しているのですが、今回その仕組みを汎用的に使えるようにサーバレスで構築してみたのでご紹介いたします。

要件

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

構成

f:id:seeds-std:20190827191927p:plain

Serverless libreoffice

まずAWS Lambdaでlibreofficeが使えるのか?(外部コマンドあるか)というところからはじめました。
外部コマンドでsofficeがあるかどうか。
ちょっと古い2014年の記事ですがどうもなさそうです…

AWS Lambdaをいろいろ暴く – Qiita

(ちなみにamazon linuxベースとのことなので大体の外部コマンドは使用可能ですね。)

自力でインストールするしかない、、できるのか、、?と調べていたところ以下を発見いたしました。

serverless-libreoffice/STEP_BY_STEP.md at master · vladgolubev/serverless-libreoffice · GitHub

こちらのStep:1ではlibreofficeをコンパイルするところから始まっていますが、リポジトリ内にコンパイル済みのファイルがありますのでそちらを使用します。(Step:2)

S3にserverless-libreofficeをアップロード

$ aws s3api create-bucket --bucket lambda-libreoffice-demo
$ curl -L https://github.com/vladgolubev/serverless-libreoffice/releases/download/v6.1.0.0.alpha0/lo.tar.gz -o lo.tar.gz
$ aws s3 cp lo.tar.gz s3://lambda-libreoffice-demo/lo.tar.gz --acl public-read-write

これで前準備は完了。 

Lambda実行時にアップロードしたファイルを展開して使用できるようにします。
今回はgoで実装していきます。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
}

main.goのinitでs3から取得したファイルを /tmp に展開します。
initで呼び出すことでlambdaコンテナの起動時のみ実行されます。
注意点はLambdaでは /tmp 以下にしか書き込み権限がないのと容量制限(500MB)があります。

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数のためにディスクにスクラッチスペースが必要な場合はどうすればよいですか?

各 Lambda 関数では /tmp ディレクトリに 500 MB の容量 (非永続型) を割り当てることができます。

ひとまずこれで /tmp/instdir/program/soffice として実行可能になります。
受け取ったExelファイルは /tmp/sample.xlsx にファイルとして保存しているとして、以下の様な感じで実行できました。

const convertCommand = "/tmp/instdir/program/soffice --headless --invisible --nodefault --nofirststartwizard --nolockcheck --nologo --norestore --convert-to pdf --outdir /tmp %s"
command := fmt.Sprintf(convertCommand, "/tmp/sample.xlsx")
exec.Command("sh", "-c", command).Run()

/tmp/sample.pdf が生成されるので、これをbase64エンコードしてjson形式で返却します。

…がしかし、実行はできましたが、このままだと日本語が文字化けてしまいます。
日本語フォントに対応が必要そうです。

日本語フォント

「lambda 日本語フォント」とかで調べていると以下の記事にあたりました。

AWS LambdaでPhantomJS日本語フォント対応 | RCO Ad-Tech Lab Blog

Lambdaの実行環境にフォントを追加する – Qiita

ようはfont-cache生成できればよさそうです。
上記記事では生成したfont-cacheをデプロイパッケージに含めていましたが、initでlibreofficeの展開もしているのでそこでfont-cache生成したらいいかな、と思ったのでその方向で実装しました。

initに追加します。

// main.go
func init() {
cmd := "curl https://" + os.Getenv("S3_BUCKET_NAME") + ".s3.amazonaws.com/lo.tar.gz -o /tmp/lo.tar.gz && cd /tmp && tar -xf /tmp/lo.tar.gz"
exec.Command("sh", "-c", cmd).Run()
os.Setenv("HOME", os.Getenv("LAMBDA_TASK_ROOT"))
cmd := "mkdir -p /tmp/cache/fontconfig && fc-cache " + path.Join(os.Getenv("HOME"), ".fonts")
exec.Command("sh", "-c", cmd).Run()
}

.fonts ディレクトリに使用するfontファイルをデプロイパッケージに含めてlambdaにアップロードします。

以下ディレクトリ構成でzipに固める

.
├── .fonts
│   ├── hoge.ttc
│   └── fuga.ttc
├── .fonts.conf
└── main

main goのbuild済みファイルです。
.fonts.confは参考URLのまま以下の設定にしています。

<fontconfig>
<cachedir>/tmp/cache/fontconfig</cachedir>
</fontconfig>

これで実行するとPDFに日本語が含まれていてもOKになりました!

Timezone

構築後に社内で試してもらっていると日付関係が変?という報告がありました。
LambdaのデフォルトtimezoneはUTCなのでExceleで =Now() とかするとUTCの時刻になるようです。
これは対応が簡単で、lambdaの環境変数設定で解決。
TZ をkeyにして設定可能でした。

key: TZ
value: Asia/Tokyo

ちなみに日付の並び順は変わらなかったので、Excel側でフォーマット指定してもらうことにしました…

=TEXT(NOW(),"yyyy/MM/dd hh:mm:ss")

これにて無事日本語が含まれるExcelデータもPDF変換可能です!

その他ハマったところ

バイナリメディアタイプ

要件のところで書いていた話

  • ExcelファイルをWEB APIにPOSTしたらPDF変換されて返ってくる
  • 今回はbase64エンコードしたデータをjsonでやり取りする方式(理由は後述)

API Gatewayの設定でバイナリメディアタイプを設定することで、ファイルアップロードしたらPDFがそのままダウンロードできる…と思ってやっていたのですが、どうにもうまくいかず断念しました。(これは自分のgo言語の力量不足かもしれませんが..)

あと、ブラウザからではリクエストヘッダーに Accept: application/pdf が付与されないためCloudFrontをかまさないといけない、、、ということなので今回はjsonでやり取りでいいかな、となったのも理由です。

送信するときにExcelデータをbase64エンコードして、変換したPDFもbase64エンコードして返却させています。
送信側でプログラム書く場合はこの方がシンプルに構築できるのでいいかな、と感じています。

API Gatewayのタイムアウト

API GatewayのタイムアウトはLambdaより短いです。
Lambda ・・・最長15分

よくある質問 – AWS Lambda |AWS

Q: AWS Lambda 関数はどれくらいの時間実行できますか?

AWS Lambda 関数は、1 回あたりの実行時間を最長 15 分に設定することができます。タイム> アウトは 1 秒から 15 分までの間で任意に設定できます。

API Gateway ・・・最長29秒

Amazon API Gateway の制限事項と重要な注意点 – Amazon API Gateway

統合のタイムアウト・・・Lambda、Lambda プロキシ、HTTP、HTTP プロキシ、AWS 統合など、すべての統合タイプで 50 ミリ秒~29 秒。

また制限の解除も対象外です。

Lambdaのデフォルトのメモリ(128MB)だとタイムアウトすることが多かったのでしたので256MBで実行するようにしました。

最後に

サーバレス開発で今回のように外部コマンドもLambda上で展開してしまえば結構なんでもできそうだなと思いました。
API gatewayのバイナリメディアタイプに関しては引き続き調べていきたいところです。

あと今回はサーバーレスアプリ自体にはあまり触れませんでしたが SAM CLI を使用して開発、デプロイしています。

GitHub – awslabs/serverless-application-model: AWS Serverless Application Model (SAM) is an open-source framework for building serverless applications

このあたりの話はまたの機会に…

以上です!

© SEEDS Co.,Ltd.