Creating Google Cloud Functions with Kotlin

In May 2020 google announced that Java 11 was coming to Google Cloud Functions. Naturally, my first thought was, can I write functions in Kotlin?

Thankfully there is no such limitation. In fact, you could write your functions in any JVM language of your choosing.

Source code for this example can be found on GitHub:
codenerve-com/kotlin-google-cloud-function
Contribute to codenerve-com/kotlin-google-cloud-function development by creating an account on GitHub.

Prerequisites

You'll need some things to follow along with this example:

  • Java 11
  • Gradle
  • Access to GCP with a sample project created
  • gcloud installed and authenticated
  • The source code cloned and imported into your IDE

Main Function

The main function is a straightforward one to get us started. A basic HTTP endpoint that will return "FUNCTION COMPLETE" when triggered.

package com.codenerve.function

import com.google.cloud.functions.HttpFunction
import com.google.cloud.functions.HttpRequest
import com.google.cloud.functions.HttpResponse
import mu.KotlinLogging
import java.io.IOException

class App : HttpFunction {

    private val logger = KotlinLogging.logger {}

    @Throws(IOException::class)
    override fun service(request: HttpRequest, response: HttpResponse) {
        logger.info { "hello world" }
        response.writer.write("FUNCTION COMPLETE")
    }
}

This class extends HttpFunction from the functions-framework-api library, and the function service takes a HttpRequest and a HttpResponse object.

For other non-HTTP ways to trigger a cloud function. You can use a RawBackgroundFunction or a typed variant. For example BackgroundFunction<PubSubMessage> instead. You can find examples for other triggering mechanisms in the functions-framework-java readme.

Gradle Build File

For this example, we are using gradles kotlin dsl to configure our project.

import java.lang.invoke.MethodHandles.invoker

val invoker by configurations.creating

plugins {
    id("org.jetbrains.kotlin.jvm") version "1.3.72"
    id("com.github.johnrengelman.shadow") version "6.0.0"
    application
}

repositories {
    jcenter()
}

dependencies {
    implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("io.github.microutils:kotlin-logging:1.11.5")
    implementation("com.google.cloud.functions:functions-framework-api:1.0.1")
    invoker("com.google.cloud.functions.invoker:java-function-invoker:1.0.0-alpha-2-rc5")

    testImplementation("org.jetbrains.kotlin:kotlin-test")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit")
    testImplementation("org.mockito:mockito-core:3.5.10")
    testImplementation("com.google.truth:truth:1.0.1")
    testImplementation("com.google.guava:guava-testlib:29.0-jre")
}

application {
    mainClassName = "com.codenerve.function.AppKt"
}


task<JavaExec>("runFunction") {
    main = "com.google.cloud.functions.invoker.runner.Invoker"
    classpath(invoker)
    inputs.files(configurations.runtimeClasspath, sourceSets["main"].output)
    args(
        "--target", project.findProperty("runFunction.target") ?: "com.codenerwve.function.App",
        "--port", project.findProperty("runFunction.port") ?: 8080
    )
    doFirst {
        args("--classpath", files(configurations.runtimeClasspath, sourceSets["main"].output).asPath)
    }
}

tasks.named("build") {
    dependsOn(":shadowJar")
}

task("buildFunction") {
    dependsOn("build")
    copy {
        from("build/libs/" + rootProject.name + "-all.jar")
        into("build/deploy")
    }
}
build.gradle.kts

Dependencies

There are a few critical dependencies

  • Functions-framework-api allows us to write lightweight functions that run in many different environments, including google cloud functions and cloud run.
  • Java-function-invoker enables us to run the function locally for testing.

runFunction task

The runFunction task in the Gradle build file triggers the java-function-invoker which wraps the function and serves it using a jetty web server.

To run the function locally, call runFunction using the Gradle wrapper:

 ./gradlew runFunction
 
 ❯ ./gradlew runFunction 

> Task :runFunction
INFO: Serving function...
Oct 06, 2020 9:10:09 PM com.google.cloud.functions.invoker.runner.Invoker logServerInfo
INFO: Function: com.codenerve.function.App
Oct 06, 2020 9:10:09 PM com.google.cloud.functions.invoker.runner.Invoker logServerInfo
INFO: URL: http://localhost:8080/
<==========---> 80% EXECUTING [1m 13s]
> :runFunction

(logging shortened for readability)

Optionally some args can be overridden:

./gradlew runFunction -PrunFunction.target=com.codenerve.function.App -PrunFunction.port=8080

buildFunction Task

The buildFunction task in the Gradle build file works with the com.github.johnrengelman.shadow Gradle plugin to create a fat jar and copy it to the build/deploy directory ready to be uploaded to GCP Cloud Storage.

To execute this, use the Gradle wrapper:

❯ ./gradlew buildFunction 

BUILD SUCCESSFUL in 2s
10 actionable tasks: 9 executed, 1 up-to-date

Deploying with gcloud

So you've tested your function locally and its time to deploy. Deploying can be achieved easily with the gcloud command-line utility.

First, ensure you are deploying to the GCP Project of your choice:

gcloud config get-value project

my-functions-project

Then select the region you wish to deploy to :

gcloud config set functions/region europe-west1

Lastly, deploy the function:

gcloud functions deploy my-test-function \
--entry-point=com.codenerve.function.App \
--source=build/deploy --runtime=java11 --trigger-http \
--allow-unauthenticated

N.B. the entry-point argument is the fully qualified class name of the function, and the source is the location of our fat jar.

If you open the GCP console and navigate to Cloud Functions, you'll see the function:

From here you can open it and view various information including the trigger. Alternatively, you can run a describe from gcloud:

gcloud functions describe my-test-function

availableMemoryMb: 256
buildId: 35e82211-d38b-4c79-b2c5-70af0a665480
entryPoint: com.codenerve.function.App
httpsTrigger:
  url: https://europe-west1-slice-poc.cloudfunctions.net/my-test-function
.... shortened ....

Hitting the trigger URL, you will see the function return the expected response:

❯ curl https://europe-west1-slice-poc.cloudfunctions.net/my-test-function
FUNCTION COMPLETE%

Conclusion

We've seen just how easy it is to deploy a kotlin function to GCP. To explore the topic further. I'd recommend the following documentation:

Cloud Functions documentation | Cloud Functions Documentation
Small, single-purpose functions.
GoogleCloudPlatform/functions-framework-java
FaaS (Function as a service) framework for writing portable Java functions - GoogleCloudPlatform/functions-framework-java
Running a function with Gradle & Kotlin (build.gradle.kts) · Issue #35 · GoogleCloudPlatform/functions-framework-java
Thanks for putting all this together! I was trying to get the local invoker working on my local machine, which is setup using kotlin (1.3.72) and gradle (6.4.1) via the build.gradle.kts file. The G...

Michael Whyte

Michael Whyte