Cloud FunctionsをKotlin/JVMで書きたい！

GCPにはAWS Lambdaに相当する関数実行環境である[Cloud Functions](https://cloud.google.com/functions)があり、これがちょっとしたツールを作る時には非常に便利です。
このCloud Functionsはしばらく前から[Java11をサポート](https://cloud.google.com/blog/ja/products/application-development/introducing-java-11-on-google-cloud-functions)しており、JVM環境が動くということは当然ながらKotlin/JVMでの実装も可能です！

記事にするにはちょっと旬を逃してる気はしますが、普段からKotlinを書いてる身なのでフルKotlin/JVMでCloud Functionsを動かすためのだいたい最小構成についてまとめてみます。
（ある程度Kotlin+Gradleに慣れている前提でお話します）

## GCPプロジェクトを作成してCloud Build APIを有効にしておく

ここでは説明は省きますが、下記ドキュメントを参考に最低限GCPプロジェクトの作成は済ませておいてください。
https://cloud.google.com/resource-manager/docs/creating-managing-projects

また今回の流れでは[Cloud Build API](https://cloud.google.com/build/docs/quickstart-build)の有効化が必要になるのでGCPのダッシュボードよりCloud Buildを選択して有効化しておいてください。（有効化してないとデプロイ時に有効化してないぞと怒られて失敗します）

![スクリーンショット 2021-12-02 17.46.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/543fcdd3-52dc-8f94-5c2e-1bf4f4add8ef.png)


## functions-framework-javaの導入

Cloud FunctionsをJVMで動作させるには [functions-framework-java](https://github.com/GoogleCloudPlatform/functions-framework-java)というライブラリを利用します。
使い方は簡単で基本的には下記で導入は完了です。

```kotlin:build.gradle
configurations {
    invoker
}

dependencies {
    implementation("com.google.cloud.functions:functions-framework-api:1.0.4")
    invoker("com.google.cloud.functions.invoker:java-function-invoker:1.1.0")
}
```

## 処理の記述

ここまででCloud Functionsを記述するための準備は出来ているので早速実装に入ります。
HttpFunctionを継承したKotlinクラスを作成してください。

```kotlin:Application
class Application : HttpFunction {
    override fun service(request: HttpRequest, response: HttpResponse) {
        response.writer.write("Hello World!")
    }
}
```

`fun service(request: HttpRequest, response: HttpResponse)`というメソッドの実装を求められますが、ここがアクセスがあった際に呼ばれるエントリポイントとなります。
上記では"Hello World!"という文字列を返しているだけですが、任意の処理を記述して値を返すように変更してくれればOKです！

また、requestの引数からはアクセス時のクエリパラメータなどが取得できるので処理の分岐等も簡単です。
![スクリーンショット 2021-12-02 16.14.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/9e7276c7-cdf1-90b8-4351-b77d4be53a62.png)

## ローカル環境での実行

コードの実装まで終わったらまずはローカル環境での実行を試してみます。
[READMEでの説明](https://github.com/GoogleCloudPlatform/functions-framework-java#running-a-function-with-gradle)の通り、`runFunction`というタスクをGradleに追加していきます。

```kotlin:build.gradle
tasks.register("runFunction", JavaExec) {
    main = "com.google.cloud.functions.invoker.runner.Invoker"
    classpath(configurations.invoker)
    inputs.files(configurations.runtimeClasspath, sourceSets.main.output)
    args(
            "--target", project.findProperty("run.functionTarget") ?: "",
            "--port", project.findProperty("run.port") ?: 8080
    )
    doFirst {
        args("--classpath", files(configurations.runtimeClasspath, sourceSets.main.output).asPath)
    }
}
```

その上で、下記コマンドでローカル環境での実行が可能です。

```sh:shell
$ ./gradlew runFunction -Prun.functionTarget=com.kazakago.cloud_functions_starter.Application
```

`com.kazakago.cloud_functions_starter.Application`と書いている部分はご自身のエントリポイントクラスのフルパスに差し替えて指定してください。
問題なく実行されると受付状態になるので、ブラウザやcurlコマンドあたりで適当にアクセスしてもらえば"Hello World!"が表示されるはずです。

```sh:shell
$ curl localhost:8080
# Output: Hello World!
```

![スクリーンショット 2021-12-02 16.24.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/a90cbaaa-a001-6972-1d28-979a4ab01804.png)

IDEを利用している場合にはもちろんブレークポイントも動作するのでデバッグも捗ります。

## Cloud Functionsへのデプロイ

ローカル環境で期待通りの動きが出来たら、晴れてCloud Functionsにデプロイしてみます。
まずは[google-cloud-sdk](https://cloud.google.com/sdk/docs/install)をPCにインストールしてください。
※この記事ではMac+[Homebrew](https://brew.sh/index_ja)導入済みを前提に進めます。他プラットフォーム等は公式Docをご参照ください。

```sh:shell
$ brew install --cask google-cloud-sdk
```

上記コマンドでgoogle-cloud-sdkがインストールできるので作成したGCPプロジェクトへのアクセス権限があるアカウントでログインしてください。

```sh:shell
$ gcloud auth login
```

問題なくログインまで出来たら下記コマンドでデプロイを行います。
`gcloud functions deploy`に続く`cloud_functions_starter`と記述している部分はこの関数の名前になります。ダッシュボード上で表示されるのでわかりやすい名前をつけてあげてください。
`--project`と`--entry-point`についてはご自身のプロジェクト名やフルパスに変更してください。
今回は`--trigger-http`と`--allow-unauthenticated`を付与して外部からの呼び出しを許可しています。
この辺は用途に合わせて変更してください！

```sh:shell
gcloud functions deploy cloud_functions_starter --project=kazakago-playground --entry-point=com.kazakago.cloud_functions_starter.Application --runtime=java11 --trigger-http --allow-unauthenticated
```

デプロイが完了するとアクセスできるURLが表示されるのでそれをブラウザかcurlで叩くとローカルで実行したときと同様の振る舞いがされているはずです。
URLがわからなくなった場合は[この辺のドキュメント](https://cloud.google.com/functions/docs/first-java#testing_the_deployed_function)を見てもらえば調べられると思います。

![スクリーンショット 2021-12-02 18.34.37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/129872/7d8642c4-4607-3548-c1d2-f34730f75c28.png)

## build.gradleの全体像

ここまでのbuild.gradle.ktsをまとめてみます。
`JavaVersion.VERSION_11`や`kotlinOptions.jvmTarget = "11"`の指定はなくても動作するかもしれませんが、Cloud Functionsの実行環境である11と同等になるように設定しておいて損はないかと思います。

```kotlin:build.gradle
plugins {
    id "org.jetbrains.kotlin.jvm" version "1.6.0"
}

sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11

compileKotlin {
    kotlinOptions.jvmTarget = "11"
}
compileTestKotlin {
    kotlinOptions.jvmTarget = "11"
}

repositories {
    mavenCentral()
}

configurations {
    invoker
}

dependencies {
    implementation("com.google.cloud.functions:functions-framework-api:1.0.4")
    invoker("com.google.cloud.functions.invoker:java-function-invoker:1.1.0")
}

tasks.register("runFunction", JavaExec) {
    main = "com.google.cloud.functions.invoker.runner.Invoker"
    classpath(configurations.invoker)
    inputs.files(configurations.runtimeClasspath, sourceSets.main.output)
    args(
            "--target", project.findProperty("run.functionTarget") ?: "",
            "--port", project.findProperty("run.port") ?: 8080
    )
    doFirst {
        args("--classpath", files(configurations.runtimeClasspath, sourceSets.main.output).asPath)
    }
}
```

Githubにそのまま動かすことの出来るサンプルプロジェクトも置いてあるので良ければご参照ください。
https://github.com/KazaKago/cloud_functions_starter

## 余談1: Cloud Functions for Firebaseについて

Firebaseを利用している方であればご存知とは思いますが、Firebase側にもCloud Functionsという機能が提供されておりそちらは[Cloud Functions for Firebase](https://firebase.google.com/docs/functions)と呼ばれています。
バックエンドとしてはおそらく同一のもので、機能的にも（微妙に違う部分はあれど）だいたい一緒です。
デプロイしたコードは連携しているFirebaseとGCPのダッシュボード両方に表示されます。

違いとしてCloud Functions for Firebaseは[Firebase CLI](https://firebase.google.com/docs/cli)コマンドからもデプロイが可能という点があります。
しかしながらこのFirebase CLIでは現在JavascriptまたはTypescriptのみサポートしており、今回メインで話したKotlin/JVMプロジェクトのデプロイは出来ません。
なので私のようなほとんどFirebaseの機能しか使ってないモバイルエンジニアであっても、Kotlin/JVMでCloud Functionsを書きたい場合にはgoogle-cloud-sdkを利用する必要があります。ご注意くださいませ！

## 余談2: Gradle Kotlin DSLについて

もちろん書こうと思えばGradle Kotlin DSLでも書けるのですが
デフォルトの状態だとCloud Buildがbuild.gradle.ktsをうまくフックしてくれないのでそのままでは自動ビルドが行えませんでした。今回は追加の記述を省くためにgroovyで実装しています。
Kotlin DSLで書きたい場合は[fat-jarを生成できる環境](https://cloud.google.com/functions/docs/concepts/java-deploy#deploy_from_a_jar)を整えた上でCloud Buildに流し込めば問題ないと思います。