最近Kotlin/JSを触ってます。
実はCloud FunctionsがまだJavaをサポートしていないのでKotlinでCloud Functionsを動かそうと思ったらJSに変換する必要があります。
DartでもJSに変換してCloud Functionsを動かしたことがあったので、同じ用に動かせるのではないかと思いトライしました。
現在はEAPが稼働しているので、時期に対応されますが待ちきれない!という方には参考になると思います。
環境は次のとおりです
Cloud Functions
まずイメージを掴むために、JSのCloud Functionsの公式サンプル実装を見てみます。
const escapeHtml = require('escape-html'); /** * HTTP Cloud Function. * * @param {Object} req Cloud Function request context. * More info: https://expressjs.com/en/api.html#req * @param {Object} res Cloud Function response context. * More info: https://expressjs.com/en/api.html#res */ exports.helloHttp = (req, res) => { res.send(`Hello ${escapeHtml(req.query.name || req.body.name || 'World')}!`); };
これはhelloHttp
という関数のサンプルです。helloHttp
に、request
とresponse
を引数に持つ関数を代入して、レスポンスを返しているコードになります。
同じようなコードをKotlinで書いてみます。
ライブラリの追加
Kotlin 1.3.70からdependenciesに直接依存ライブラリを定義できるようになりました。
- build.gradle.kts
dependencies { implementation(kotlin("stdlib-js")) implementation(npm("escape-html", "1.0.3")) implementation(npm("@types/escape-html", "0.0.20")) }
escape-html
はdukatが使えたので利用します。
- build.gradle.kts
kotlin { sourceSets["main"].kotlin.srcDir("src/main/external") target { nodejs {} useCommonJs() } }
$ dukat -d src/main/external/ build/js/node_modules/@types/escape-html/index.d.ts
helloHttp関数を作成
helloHttp
にラムダ式を代入しています。引数の型はJSから渡されるのでdynamicにしています。
req.query
などは存在しなくてもエラーになりません。変数として扱う限りundefinedなだけです。
- Main.kt
external val exports: dynamic fun main() { exports.helloHttp = { req: dynamic, res: dynamic -> res.send(createMessage(req)) } } private fun createMessage(req: dynamic): String { val message = when { req.query.name !== undefined -> escapeHTML(req.query.name) req.body.name !== undefined -> escapeHTML(req.body.name) else -> "World" } return "Hello $message" }
ローカルで実行
次のコマンドでNode.jsで実行できます。
$ ./gradlew nodeRun
ただし先程の例だと何も出力されません。helloHttp関数が呼ばれていないからです。Cloud Functionsをローカルで確認するには @google-cloud/functions-framework
を使うと確認できます。
現状、build.gradle.kts
のdependenciesからはdevDependencies
に追加できないので、npx
を使ってコマンドを実行することで回避します。
$ cd build/js/packages/${PROJECT_NAME} $ npx @google-cloud/functions-framework --target=helloHttp npx: installed 52 in 3.789s Serving function... Function: helloHttp URL: http://localhost:8080/
ローカルに立ち上がるので、アクセスするとhelloHttp関数が呼ばれます。
$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/" Hello World
nameクエリをつけてアクセスしてみます。
$ curl -X POST -H "Content-Type: application/json" "http://localhost:8080/?name=\"Kotlin/JS\"" Hello "Kotlin/JS"
しっかりescape-htmlも動いています。
Cloud Functionsデプロイのためのプロジェクト構成
Cloud Functionsのデプロイに必要なものはJSのコードと、ライブラリを利用していたらpackage.jsonが必要です。
2つのファイルをまとめるタスクを定義します。
タスク定義
- build.gradle.kts
tasks { val packaging by creating(Copy::class) { from("build/js/packages/${project.name}/kotlin/${project.name}.js", "build/js/packages/${project.name}/package.json") into("functions") rename { it.replace("${project.name}.js", "index.js") } doLast { val jsonFile = file("functions/package.json") val texts = jsonFile.readLines() .map { it.replace("kotlin/${project.name}.js", "index.js") } jsonFile.writeText(texts.joinToString("\n")) } } }
functions
フォルダを生成して配置してます。jsファイルは、Cloud Functionsの制約があるのでindex.js
となるようにリネームしています。合わせて、package.jsonのエントリーポイントをindex.js
となるように書き換えてます。置き換え後のpackage.jsonは次のようになってます。
デプロイ先で依存の解決がされるので、npm install
する必要はないです。
- functions/package.json
{ "main": "index.js", "devDependencies": { "source-map-support": "0.5.16" }, "dependencies": { "kotlin": "1.3.72", "escape-html": "1.0.3", "@types/escape-html": "0.0.20" }, "peerDependencies": {}, "optionalDependencies": {}, "bundledDependencies": [], "name": "kotlin-js-cloud-functions", "version": "1.0.0-SNAPSHOT" }
次のように実行することでfunctions
フォルダに必要なファイルを 設置できます。
$ ./gradlew clean compileKotlinJs $ ./gradlew packaging
JSファイルを生成するだけならnodeRun
ではなくてcompileKotlinJs
で生成できます。
生成してから置き換えタスクを実行します。
実行後は次のような配置になります。
$ tree -L 2 . ├── build │ ├── js │ ├── kotlin │ └── tmp ├── build.gradle.kts ├── functions │ ├── index.js │ └── package.json ├── gradle │ └── wrapper ├── gradle.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main └── test
デプロイ
gcloudでデプロイします。
取り掛かる前に、GCPのConfigurationが意図しているものかどうか確認しておいてください。
$ gcloud config list
functionsフォルダに移動してからコマンドを実行します。
$ cd functions $ gcloud functions deploy helloHttp --region=asia-northeast1 --trigger-http --runtime=nodejs8 --allow-unauthenticated
サクッと確認したいため、--allow-unauthenticated
フラグを付けてpublicにしています。
次のようなURLが排出されると思います。
https://{REGION_NAME}-{PROJECT_NAME}.cloudfunctions.net/{FUNCTION_NAME}
まとめ
コードとしてはシンプルに導入できます。
デバッグも組み込まれてはいないですが、npxを利用することで回避できます。 デプロイに関しても、必要な処理は今回用意したタスクでまかなえるのでそこまで手間にならないと思います。
リポジトリはこちらです。
参考
Google Cloud Blog - News, Features and Announcements
Functions Framework | Cloud Functions Documentation | Google Cloud
gcloud functions deploy | Cloud SDK Documentation | Google Cloud