メモ2ブログ

メモtoウェブログ。旧ブログはこちら。 http://sakebook.blogspot.jp/

Kotlin/JSでCloud Functionsを動かす

最近Kotlin/JSを触ってます。

実はCloud FunctionsがまだJavaをサポートしていないのでKotlinでCloud Functionsを動かそうと思ったらJSに変換する必要があります。

DartでもJSに変換してCloud Functionsを動かしたことがあったので、同じ用に動かせるのではないかと思いトライしました。

sakebook.hatenablog.com

現在はEAPが稼働しているので、時期に対応されますが待ちきれない!という方には参考になると思います。

環境は次のとおりです

  • Kotlin v1.3.72
  • dukat v0.0.28
  • Node.js v13.3.0
  • Google Cloud SDK v290.0.0

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に、requestresponseを引数に持つ関数を代入して、レスポンスを返しているコードになります。

同じようなコードを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を利用することで回避できます。 デプロイに関しても、必要な処理は今回用意したタスクでまかなえるのでそこまで手間にならないと思います。

リポジトリはこちらです。

github.com

参考

Google Cloud Blog - News, Features and Announcements

Functions Framework  |  Cloud Functions Documentation  |  Google Cloud

gcloud functions deploy  |  Cloud SDK Documentation  |  Google Cloud