Micronaut GCP

Provides integration between Micronaut and Google Cloud Platform (GCP)

Version:

1 Introduction

This project provides various extensions to Micronaut to integrate Micronaut with Google Cloud Platform (GCP).

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Setting up GCP Support

The micronaut-gcp-common module includes basic setup for running applications on Google Cloud.

implementation("io.micronaut.gcp:micronaut-gcp-common:2.0.2")
<dependency>
    <groupId>io.micronaut.gcp</groupId>
    <artifactId>micronaut-gcp-common</artifactId>
    <version>2.0.2</version>
</dependency>

Prerequisites:

  1. You should have a Google Cloud Platform project created

  2. Install gcloud CLI

  3. Configure default project gcloud config set project YOUR_PROJECT_ID

  4. Authenticate with gcloud auth login

  5. Authenticate application default credential with gcloud auth application-default login

It’s strongly recommended that you use a Service Account for your application.

Google Project ID

The module features a base GoogleCloudConfiguration which you can use to configure or retrieve the GCP Project ID:

🔗
Table 1. Configuration Properties for GoogleCloudConfiguration
Property Type Description

gcp.project-id

java.lang.String

Sets the project id to use.

You can inject this bean and use the getProjectId() method to retrieve the configured or detected project ID.

Google Credentials

The module will setup a bean of exposing the com.google.auth.oauth2.GoogleCredentials instance that are either detected from the local environment or configured by GoogleCredentialsConfiguration:

🔗
Table 2. Configuration Properties for GoogleCredentialsConfiguration
Property Type Description

gcp.credentials.scopes

java.util.List

The default scopes to associate with the application to access specific APIs. See <a href="https://developers.google.com/identity/protocols/googlescopes">Google Scopes</a> for a complete list. Leave this empty if you don’t need additional API access.

gcp.credentials.location

java.lang.String

Sets the location to the service account credential key file.

gcp.credentials.encoded-key

java.lang.String

Sets the Base64 encoded service account key content..

4 Stackdriver Trace

The micronaut-gcp-tracing integrates Micronaut with Cloud Trace from Google Cloud Operations (formerly Stackdriver).

To enable it add the following dependency:

implementation("io.micronaut.gcp:micronaut-gcp-tracing:2.0.2")
<dependency>
    <groupId>io.micronaut.gcp</groupId>
    <artifactId>micronaut-gcp-tracing</artifactId>
    <version>2.0.2</version>
</dependency>

Then enabling Zipkin tracing in your configuration application.yml:

Enabling Stackdriver Trace
tracing:
    zipkin:
        enabled: true
        # [Optional] Set sampling probability to 100% for dev/testing purposes to observe traces.
        #
        # sampler:
        #    probability=1.0

# [Optional] configuration to enable/disable Stackdriver Trace configuration.
# This is defaulted to true.
#
# gcp:
#    tracing:
#        enabled: true

5 Authorizing HTTP Clients

The micronaut-gcp-http-client module can be used to help authorize service-to-service communication. To get started add the following module:

implementation("io.micronaut.gcp:micronaut-gcp-http-client")
<dependency>
    <groupId>io.micronaut.gcp</groupId>
    <artifactId>micronaut-gcp-http-client</artifactId>
</dependency>

You should then configure the service accounts as per the documentation on service-to-service communication and the enable the filter for the outgoing URI paths you wish to include the Google-signed OAuth ID token:

gcp:
  http:
    client:
      auth:
        patterns:
          - /foo/**
          - /bar/**

6 Cloud Function Support

Micronaut GCP includes extended support for Google Cloud Function - designed for serverless workloads.

6.1 Simple Functions

Micronaut GCP offers two ways to write cloud functions with Micronaut. The first way is more low level and involves using Micronaut’s built in support for functions. Simply add the following dependency to your classpath:

implementation("io.micronaut.gcp:micronaut-gcp-function")
<dependency>
    <groupId>io.micronaut.gcp</groupId>
    <artifactId>micronaut-gcp-function</artifactId>
</dependency>

Then add the Cloud Function API as a compileOnly dependency (provided with Maven):

compileOnly("com.google.cloud.functions:functions-framework-api:1.0.1")
<dependency>
    <groupId>com.google.cloud.functions</groupId>
    <artifactId>functions-framework-api</artifactId>
    <version>1.0.1</version>
    <scope>provided</scope>
</dependency>

Now define a class that implements one of Google Cloud Found’s interfaces, for example com.google.cloud.functions.BackgroundFunction, and extends from io.micronaut.function.executor.FunctionInitializer.

The following is an example of a BackgroundFunction that uses Micronaut and Google Cloud Function:

/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.background;

import com.google.cloud.functions.*;
import io.micronaut.gcp.function.GoogleFunctionInitializer;

import javax.inject.*;
import java.util.*;

public class Example extends GoogleFunctionInitializer (1)
        implements BackgroundFunction<PubSubMessage> { (2)

    @Inject LoggingService loggingService; (3)

    @Override
    public void accept(PubSubMessage message, Context context) {
        loggingService.logMessage(message);
    }
}

class PubSubMessage {
    String data;
    Map<String, String> attributes;
    String messageId;
    String publishTime;
}

@Singleton
class LoggingService {

    void logMessage(PubSubMessage message) {
        // log the message
    }
}
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.background

import com.google.cloud.functions.*
import io.micronaut.gcp.function.GoogleFunctionInitializer

import javax.inject.*

class Example extends GoogleFunctionInitializer (1)
        implements BackgroundFunction<PubSubMessage> { (2)

    @Inject LoggingService loggingService (3)

    @Override
    void accept(PubSubMessage message, Context context) {
        loggingService.logMessage(message)
    }
}

class PubSubMessage {
    String data
    Map<String, String> attributes
    String messageId
    String publishTime
}

@Singleton
class LoggingService {

    void logMessage(PubSubMessage message) {
        // log the message
    }
}
/*
 * Copyright 2017-2020 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package example.background

import com.google.cloud.functions.BackgroundFunction
import com.google.cloud.functions.Context
import io.micronaut.gcp.function.GoogleFunctionInitializer
import javax.inject.Inject
import javax.inject.Singleton

class Example : GoogleFunctionInitializer(), (1)
        BackgroundFunction<PubSubMessage> { (2)
    @Inject
    lateinit var loggingService: LoggingService (3)

    override fun accept(message: PubSubMessage, context: Context) {
        loggingService.logMessage(message)
    }
}

class PubSubMessage {
    var data: String? = null
    var attributes: Map<String, String>? = null
    var messageId: String? = null
    var publishTime: String? = null
}

@Singleton
class LoggingService {

    fun logMessage(message: PubSubMessage) {
        // log the message
    }
}
1 The function extends from io.micronaut.function.executor.FunctionInitializer
2 The function implements com.google.cloud.functions.BackgroundFunction
3 Dependency injection can be used on the fields

When you extend from FunctionInitializer the Micronaut ApplicationContext will be initialized and dependency injection will be performed on the function instance. You can use inject any bean using javax.inject.Inject as usual.

Functions require a no argument constructor hence you must use field injection (which requires lateinit in Kotlin) when injecting dependencies into the function itself.

The FunctionInitializer super class provides numerous methods that you can override to customize how the ApplicationContext is built if desired.

Running Functions Locally

Raw functions cannot be executed locally. They can be tested by instantiating the function and inspecting any side effects by providing mock arguments or mocking dependent beans.

Deployment

When deploying the function to Cloud Function you should use the fully qualified name of the function class as the handler reference.

First build the function with:

$ ./gradlew clean shadowJar

Then cd into the build/libs directory (deployment has to be done from the location where the JAR file resides):

$ cd build/libs

To deploy the function make sure you have gcloud CLI then run:

$ gcloud beta functions deploy myfunction --entry-point example.function.Function --runtime java11 --trigger-http

In the example above myfunction refers to the name of your function and can be changed to whatever name you prefer to name your function. example.function.Function refers to the fully qualified name of your function class.

To obtain the trigger URL you can use the following command:

$ YOUR_HTTP_TRIGGER_URL=$(gcloud beta functions describe myfunction --format='value(httpsTrigger.url)')

You can then use this variable to test the function invocation:

$ curl -i $YOUR_HTTP_TRIGGER_URL

6.2 HTTP Functions

It is common to want to take just a slice of a regular Micronaut HTTP server application and deploy it as a function.

Configuration

To facilitate this model, Micronaut GCP includes an additional module that allows you to use regular Micronaut annotations like @Controller and @Get to define your functions that can be deployed to cloud function.

With this model you need to add the micronaut-gcp-function-http dependency to your application:

implementation("io.micronaut.gcp:micronaut-gcp-function-http:2.0.2")
<dependency>
    <groupId>io.micronaut.gcp</groupId>
    <artifactId>micronaut-gcp-function-http</artifactId>
    <version>2.0.2</version>
</dependency>

And define the Google Function API as a development only dependency:

developmentOnly("com.google.cloud.functions:functions-framework-api:1.0.1")
<dependency>
    <groupId>com.google.cloud.functions</groupId>
    <artifactId>functions-framework-api</artifactId>
    <version>1.0.1</version>
    <scope>developmentOnly</scope>
</dependency>

Running Functions Locally

First to run the function locally you should then make the regular Micronaut server a developmentOnly dependency since it is not necessary to include it in the JAR file that will be deployed to Cloud Function:

developmentOnly("io.micronaut:micronaut-http-server-netty")
<dependency>
    <groupId>io.micronaut</groupId>
    <artifactId>micronaut-http-server-netty</artifactId>
    <scope>developmentOnly</scope>
</dependency>

You can then use ./gradlew run or ./mvnw compile exec:exec to run the function locally using Micronaut’s Netty-based server.

Alternatively, you could configure the Google Function Framework for Java which includes a Maven plugin, or for Gradle include the following:

Configuring the Function framework in Gradle
configurations {
    invoker
}

dependencies {
    invoker 'com.google.cloud.functions.invoker:java-function-invoker:1.0.0-beta1'
}


task('runFunction', type: JavaExec, dependsOn: classes) {
    main = 'com.google.cloud.functions.invoker.runner.Invoker'
    classpath(configurations.invoker)
    args(
            '--target', 'io.micronaut.gcp.function.http.HttpFunction',
            '--classpath', (configurations.runtimeClasspath + sourceSets.main.output).asPath,
            '--port', 8081

    )
}

With this in place you can run ./gradlew runFunction to run the function locally.

Deployment

When deploying the function to Cloud Function you should use the HttpFunction class as the handler reference.

First build the function with:

$ ./gradlew clean shadowJar

Then cd into the build/libs directory (deployment has to be done from the location where the JAR file resides):

$ cd build/libs

To deploy the function make sure you have gcloud CLI then run:

$ gcloud beta functions deploy myfunction --entry-point io.micronaut.gcp.function.http.HttpFunction --runtime java11 --trigger-http

In the example above myfunction refers to the name of your function and can be changed to whatever name you prefer to name your function.

To obtain the trigger URL you can use the following command:

$ YOUR_HTTP_TRIGGER_URL=$(gcloud beta functions describe myfunction --format='value(httpsTrigger.url)')

You can then use this variable to test the function invocation:

$ curl -i $YOUR_HTTP_TRIGGER_URL/hello/John