hero_picture
Cover Image for AWS Lambda関数をKotlin/JSで書いてみる

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

WEB事業部の西村です。

唐突ですが皆さんはLambda関数を作成する際にどのような言語を用いていますでしょうか?

用途に合った言語や自分で書きやすいと思う言語などの要因で決めていると思われます

私はKotlinという言語が好きでKotlinで書こうと思ったのですが、Kotlinで書かれている記事はそのほとんどがJavaで実行することを前提とした元なっています

しかし、Javaではコールドスタートした際に実行時間がかかってしまうというデメリットがあります

KotlinはJavaだけでなくJavaScriptでも動作させることができるので今回はNode.jsで実行させる関数の作成を行いたいと思います

目次

この記事の目標

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

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

この記事では AWS Lambda と Amazon API Gateway については詳しく解説いたしません

あらかじめLambda関数とその関数を呼び出すためのAPIの作成を行っておいてください

開発環境

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

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

プロジェクトの作成

プロジェクトの作成を行います

任意のディレクトリにプロジェクト用のフォルダを作成します(この記事では KotlinLambda としています)

その後そのディレクトリ内に build.gradle.kts と settings.gradle.kts の2つのファイルを作成します

1KotlinLambda
2├─build.gradle.kts
3└─settings.gradle.kts

作成したらIntelliJ IDEAでプロジェクトのフォルダを開きます

続いて、 build.gradle.kts と settings.gradle.kts を開き下記の内容を入力します

build.gradle.kts

1plugins {
2    kotlin("js") version "1.3.50"
3}
4repositories {
5    mavenCentral()
6}
7kotlin {
8    target {
9        nodejs()
10        useCommonJs()
11    }
12    sourceSets["main"].dependencies {
13        implementation(kotlin("stdlib-js"))
14    }
15}

settings.gradle.kts

1rootProject.name = "kotlin_lambda"

その後画面右下に出ている Gradle projects need to be imported の Import Changes をクリックします

CONFIGURE SUCCESSFUL と出てきたら準備完了です

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

次にソースディレクトリを作成します

プロジェクトのディレクトリに src/main/kotlin でディレクトリを作成します

またパッケージ名を追加する場合、パッケージを作成します(この記事では jp.co.seeds_std.lambda.kotlin とします)

作成したディレクトリに handler.kt というファイルを作成し下記の内容を記述します

handler.kt

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

デプロイ

デプロイパッケージの作成を行います

圧縮を自動化するためのタスクを追加します

build.gradle.kts を再度開き下記の内容を追加します

build.gradle.kts

1import com.google.gson.JsonParser
2import java.io.OutputStream
3import java.util.zip.ZipOutputStream
4import java.util.zip.ZipEntry
5
6plugins { ... }
7repositories { ... }
8kotlin { ... }
9
10open class CompressTask : DefaultTask() {
11    lateinit var buildDirectory: File
12    lateinit var projectName: String
13    var rootProjectName: String? = null
14    private val blackLists = setOf("kotlin-test-nodejs-runner", "kotlin-test")
15
16    @TaskAction
17    fun compress() {
18        val projectPrefix = if(rootProjectName != null && rootProjectName != projectName) "$rootProjectName-" else ""
19        val zipFile = File(buildDirectory, "../compress.zip")
20        val outputStream = ZipOutputStream(zipFile.outputStream(), Charsets.UTF_8).apply {
21            setLevel(9)
22        }
23        val jsDir = File(buildDirectory, "js")
24        val projectDir = File(jsDir, "packages/$projectPrefix$projectName")
25        val nodeModuleDir = File(jsDir, "node_modules")
26        addDependencies(outputStream, File(projectDir, "package.json"), nodeModuleDir, mutableSetOf(*blackLists.toTypedArray()))
27        addZipEntry(outputStream, File(projectDir, "kotlin/$projectPrefix$projectName.js"))
28        outputStream.close()
29    }
30    private fun addZipEntry(zipOutputStream: ZipOutputStream, file: File, addDirectory: String = "") {
31        val name = if(addDirectory.isEmpty()) file.name else "$addDirectory/${file.name}"
32        if(file.isDirectory) {
33            file.listFiles()?.forEach {
34                addZipEntry(zipOutputStream, it, name)
35            }
36        } else {
37            val zipEntry = ZipEntry(name)
38            zipOutputStream.addZipEntry(zipEntry) {
39                it.writeFile(file)
40            }
41        }
42    }
43    private fun addDependencies(zipOutputStream: ZipOutputStream, packageJsonFile: File, nodeModuleDir: File, addedDependencies: MutableSet<String> = mutableSetOf()) {
44        val packageJson = JsonParser().parse(packageJsonFile.reader()).asJsonObject
45        if(!packageJson.has("dependencies")) return
46        val dependencies = packageJson.getAsJsonObject("dependencies")
47        dependencies.keySet().forEach {
48            if(!addedDependencies.contains(it)) {
49                addedDependencies.add(it)
50                val dependenciesDir = File(nodeModuleDir, it)
51                addZipEntry(zipOutputStream, dependenciesDir, "node_modules")
52                addDependencies(zipOutputStream, File(dependenciesDir, "package.json"), nodeModuleDir, addedDependencies)
53            }
54        }
55    }
56    private inline fun ZipOutputStream.addZipEntry(entry: ZipEntry, crossinline block: (OutputStream) -> Unit) {
57        try {
58            putNextEntry(entry)
59            block(this)
60        } catch (e: Exception) {
61            e.printStackTrace()
62        } finally {
63            closeEntry()
64        }
65    }
66    @Suppress("NOTHING_TO_INLINE")
67    private inline fun OutputStream.writeFile(file: File) {
68        file.inputStream().use {
69            it.copyTo(this)
70        }
71    }
72}
73tasks.register("noMainCall") {
74    doFirst {
75        kotlin.target.compilations.all { compileKotlinTask.kotlinOptions.main = "noCall" }
76    }
77}
78tasks.register<CompressTask>("compress") {
79    dependsOn("compileKotlinJs", "noMainCall")
80    this.buildDirectory = rootProject.buildDir
81    this.projectName = project.name
82    this.rootProjectName = rootProject.name
83}
84tasks["compileKotlinJs"].mustRunAfter("noMainCall")
85

追加後、画面右下に出ている Gradle projects need to be imported の Import 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関数を便利にしてみる