GCPにはAWS Lambdaに相当する関数実行環境であるCloud Functionsがあり、これがちょっとしたツールを作る時には非常に便利です。
このCloud Functionsはしばらく前からJava11をサポートしており、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の有効化が必要になるのでGCPのダッシュボードよりCloud Buildを選択して有効化しておいてください。(有効化してないとデプロイ時に有効化してないぞと怒られて失敗します)
functions-framework-javaの導入
Cloud FunctionsをJVMで動作させるには functions-framework-javaというライブラリを利用します。
使い方は簡単で基本的には下記で導入は完了です。
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クラスを作成してください。
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の引数からはアクセス時のクエリパラメータなどが取得できるので処理の分岐等も簡単です。
ローカル環境での実行
コードの実装まで終わったらまずはローカル環境での実行を試してみます。
READMEでの説明の通り、runFunction
というタスクを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)
}
}
その上で、下記コマンドでローカル環境での実行が可能です。
$ ./gradlew runFunction -Prun.functionTarget=com.kazakago.cloud_functions_starter.Application
com.kazakago.cloud_functions_starter.Application
と書いている部分はご自身のエントリポイントクラスのフルパスに差し替えて指定してください。
問題なく実行されると受付状態になるので、ブラウザやcurlコマンドあたりで適当にアクセスしてもらえば"Hello World!"が表示されるはずです。
$ curl localhost:8080
# Output: Hello World!
IDEを利用している場合にはもちろんブレークポイントも動作するのでデバッグも捗ります。
Cloud Functionsへのデプロイ
ローカル環境で期待通りの動きが出来たら、晴れてCloud Functionsにデプロイしてみます。
まずはgoogle-cloud-sdkをPCにインストールしてください。
※この記事ではMac+Homebrew導入済みを前提に進めます。他プラットフォーム等は公式Docをご参照ください。
$ brew install --cask google-cloud-sdk
上記コマンドでgoogle-cloud-sdkがインストールできるので作成したGCPプロジェクトへのアクセス権限があるアカウントでログインしてください。
$ gcloud auth login
問題なくログインまで出来たら下記コマンドでデプロイを行います。
gcloud functions deploy
に続くcloud_functions_starter
と記述している部分はこの関数の名前になります。ダッシュボード上で表示されるのでわかりやすい名前をつけてあげてください。
--project
と--entry-point
についてはご自身のプロジェクト名やフルパスに変更してください。
今回は--trigger-http
と--allow-unauthenticated
を付与して外部からの呼び出しを許可しています。
この辺は用途に合わせて変更してください!
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がわからなくなった場合はこの辺のドキュメントを見てもらえば調べられると思います。
build.gradleの全体像
ここまでのbuild.gradle.ktsをまとめてみます。
JavaVersion.VERSION_11
やkotlinOptions.jvmTarget = "11"
の指定はなくても動作するかもしれませんが、Cloud Functionsの実行環境である11と同等になるように設定しておいて損はないかと思います。
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と呼ばれています。
バックエンドとしてはおそらく同一のもので、機能的にも(微妙に違う部分はあれど)だいたい一緒です。
デプロイしたコードは連携しているFirebaseとGCPのダッシュボード両方に表示されます。
違いとしてCloud Functions for FirebaseはFirebase 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を生成できる環境を整えた上でCloud Buildに流し込めば問題ないと思います。