SEEDS Creator's Blog

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関数を便利にしてみる