Create and Deploy a Micronaut Gateway Function as a Google Cloud Function
This guide describes how to use the Graal Development Kit for Micronaut (GDK) to create a Micronaut® Gateway Function, deploy it using Google Cloud Functions, and enable HTTP access to it. The application is compiled into a native executable using GraalVM Native Image and then built into a container image which can be deployed on Google Cloud Run.
A Micronaut Gateway Function acts a a serverless cloud (HTTP) API gateway function.
Google Cloud Functions is a scalable, pay-as-you-go Functions as a Service (FaaS) product. It lets you run code for virtually any type of application or back-end service without provisioning or managing servers. You can deploy code to Google Cloud Functions by uploading a Java JAR file. Google Cloud Run enables you to deploy scalable containerized applications written in any language on a fully managed platform. Similarly to Google Cloud Functions, the service provisions and manages servers for you.
Prerequisites #
- JDK 17 or higher. See Setting up Your Desktop.
- A Google Cloud Platform (GCP) account. See Setting up Your Cloud Accounts.
- The Google Cloud CLI authenticated with your Google credentials. (Run
gcloud init
for quick authentication.) - A Docker-API compatible container runtime such as Rancher Desktop or Docker installed and running.
- The GDK CLI. See Setting up Your Desktop. (Optional.)
Follow the steps below to create a Micronaut gateway function from scratch. However, you can also download the completed example:
A note regarding your development environment
Consider using Visual Studio Code, which provides native support for developing applications with the Graal Development Kit extension.
If you use IntelliJ IDEA, enable annotation processing.
Windows platform: The GDK guides are compatible with Gradle only. Maven support is coming soon.
1. Create the Gateway Function #
This section describes how to create a Micronaut gateway function for a simple online store. The store provides information about available items and enables the user to order items. An HTTP controller is responsible for the API implementation and a service stores the availability of items.
Create a Micronaut gateway function using the GDK Launcher.
-
Open the GDK Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Gateway Function
- Project Name: gcp-serverless-demo
- Base Package: com.example (Default)
- Clouds: GCP
- Build Tool: Gradle (Groovy) or Maven
- Language: Java (Default)
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: None
- Features: GraalVM Native Image and Micronaut Validation
- Sample Code: No
- Click Generate Project, then click Download Zip. The GDK Launcher creates a Micronaut gateway function with the default package
com.example
in a directory named gcp-serverless-demo. The application ZIP file will be downloaded to your default downloads directory. Unzip it, open it in your code editor, and proceed to the next steps.
Alternatively, use the GDK CLI as follows:
gdk create-gateway-function com.example.gcp-serverless-demo \
--clouds=gcp \
--features=graalvm,validation \
--example-code=false \
--jdk=17 \
--build=gradle \
--lang=java
gdk create-gateway-function com.example.gcp-serverless-demo \
--clouds=gcp \
--features=graalvm,validation \
--example-code=false \
--jdk=17 \
--build=maven \
--lang=java
For more information, see Using the GDK CLI.
The GDK Launcher creates a multi-module project with two subprojects: gcp for Google Cloud Platform, and lib. You develop the gateway function logic in the gcp subproject. If your gateway function is to be deployed to multiple cloud providers, use the lib subproject to create classes that can be shared between the providers. This enables you to separate the code that is different between cloud providers, while keeping most of the implementation in the common lib subproject.
1.1. StoreItem #
Create a StoreItem
model to represent an item in the store in the file lib/src/main/java/com/example/StoreItem.java with the following contents:
package com.example;
import io.micronaut.core.annotation.Introspected;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.serde.annotation.Serdeable;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
@Serdeable
@Introspected // <1>
public class StoreItem {
@NotBlank
@NonNull
private final String name;
private final String description;
@Min(0)
private int numberInStorage;
public StoreItem(String name, String description, int numberInStorage) { // <2>
this.name = name;
this.description = description;
this.numberInStorage = numberInStorage;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public Integer getNumberInStorage() {
return numberInStorage;
}
public void setNumberInStorage(Integer numberInStorage) {
this.numberInStorage = numberInStorage;
}
}
1 The @Introspected
annotation enables Micronaut to serialize and deserialize the model from different formats including JSON
. This provides the ability to use the type inside HTTP requests or responses.
2 The model has fields to store the item’s name, description, and number available.
1.2. StoreController #
Create an HTTP controller in the file lib/src/main/java/com/example/StoreController.java, as follows:
package com.example;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.PathVariable;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.exceptions.HttpStatusException;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.util.Collection;
import java.util.stream.Collectors;
import static io.micronaut.http.HttpStatus.BAD_REQUEST;
import static io.micronaut.http.HttpStatus.NOT_FOUND;
@Controller("/store") // <1>
class StoreController {
private final StorageService storageService;
StoreController(StorageService storageService) { // <2>
this.storageService = storageService;
}
@Get("/all") // <3>
Collection<StoreItem> listAllItems() {
return storageService.getItems();
}
@Get("/available") // <4>
Collection<StoreItem> listAvailableItems() {
return storageService.getItems().stream()
.filter(i -> i.getNumberInStorage() > 0)
.collect(Collectors.toList());
}
@Post(uri = "/order/{name}/{amount}", consumes = "*/*") // <5>
HttpResponse<StoreItem> orderItem(@NotBlank @PathVariable String name, @Min(1) int amount) {
if (storageService.findItem(name).isEmpty()) {
throw new HttpStatusException(NOT_FOUND, "Item '" + name + "' not found");
}
try {
storageService.orderItem(name, amount);
} catch (StorageService.StorageException e) {
throw new HttpStatusException(BAD_REQUEST, "Could not order item '" + name + "'. " + e.getMessage());
}
return HttpResponse.ok(storageService.findItem(name).orElse(null));
}
}
1 The class is defined as a controller with the @Controller
annotation mapped to the path /store
.
2 Use Micronaut argument injection to inject a StorageService
bean by defining it as the constructor argument. You will create the StorageService
in next section.
3 The @Get
annotation maps the listAllItems
method to an HTTP GET request on /store/all
.
4 The @Get
annotation maps the listAvailableItems
method to an HTTP GET request on /store/available
.
5 The @Post
annotation maps the orderItem
method to an HTTP POST request on /store/order/{name}/{amount}
. Use the consumes
argument to specify which content-types are allowed in the request. Throwing HttpStatusException
will set the corresponding HTTP status in the response.
1.3. StorageService #
-
Create an interface for a service that represents the store’s inventory in lib/src/main/java/com/example/StorageService.java:
package com.example; import io.micronaut.core.annotation.NonNull; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import java.util.Collection; import java.util.Optional; public interface StorageService { // <1> Collection<StoreItem> getItems(); Optional<StoreItem> findItem(@NonNull @NotBlank String name); void orderItem(@NonNull @NotBlank String name, @Min(1) int amount); class StorageException extends RuntimeException { // <2> StorageException(String message) { super(message); } } }
1 The storage service provides information about all the items, finds an items by its name, and can place an order for an item.
2 The class includes a custom exception that thrown in case of invalid requests to storage.
-
Create an implementation of the service interface in lib/src/main/java/com/example/DefaultStorageService.java:
package com.example; import io.micronaut.context.annotation.Requires; import io.micronaut.core.annotation.NonNull; import jakarta.inject.Singleton; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Optional; @Singleton // <1> @Requires(missingBeans = StorageService.class) // <2> class DefaultStorageService implements StorageService { protected List<StoreItem> items = List.of( // <3> new StoreItem("chair", "A black chair with 4 legs", 10), new StoreItem("table", "A quality dining table", 6), new StoreItem("sofa", "A gray sofa", 2), new StoreItem("bookshelf", "A futuristic-looking bookshelf", 0) ); @Override public Collection<StoreItem> getItems() { return items; } @Override public Optional<StoreItem> findItem(@NonNull String name) { return items.stream().filter(item -> item.getName().equals(name)).findFirst(); } @Override public void orderItem(@NonNull String name, int amount) { findItem(name).ifPresentOrElse(item -> { if (item.getNumberInStorage() >= amount) { item.setNumberInStorage(item.getNumberInStorage() - amount); } else { throw new StorageException("Insufficient amount in storage"); } }, () -> { throw new StorageException("Item not found in storage"); }); } }
1 Use
jakarta.inject.Singleton
to designate a class as a singleton.2 The
@Requires(missingBeans = StorageService.class)
annotation specifies that this implementation should only be used if no other implementations could be found.3 The implementation stores the items in a
List
and populates some sample items in the list.
If you wish to implement a more advanced StorageService
to be used instead of this one, annotate your implementation with @Singleton
as shown above. Use the @Requires(env = "gcp")
annotation to make it specific to GCP . Visit the Database Module for details about how to store and manipulate data in a database.
1.4. Tests to Verify Gateway Function Logic #
The GDK Launcher created a test class for the controller in gcp/src/test/java/com/example/StoreControllerTest.java, as follows:
package com.example;
import io.micronaut.context.annotation.Property;
import io.micronaut.context.annotation.Requires;
import io.micronaut.http.HttpResponse;
import io.micronaut.http.annotation.Get;
import io.micronaut.http.annotation.Post;
import io.micronaut.http.client.annotation.Client;
import io.micronaut.http.client.exceptions.HttpClientResponseException;
import io.micronaut.test.extensions.junit5.annotation.MicronautTest;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import static io.micronaut.http.HttpStatus.BAD_REQUEST;
import static io.micronaut.http.HttpStatus.NOT_FOUND;
import static io.micronaut.http.HttpStatus.OK;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
@MicronautTest // <1>
@Property(name = "use-test-storage-service", value = "true")
class StoreControllerTest {
@Inject
StoreClient client;
@Test
void testAvailableItems() {
List<StoreItem> availableItems = client.getAvailable();
assertEquals(2, availableItems.size());
assertEquals("pot", availableItems.get(1).getName());
assertEquals(10, availableItems.get(1).getNumberInStorage());
assertNotNull(availableItems.get(1).getDescription());
}
@Test
void testNotFoundException() {
HttpResponse<?> response = client.order("lamp", 1);
assertEquals(NOT_FOUND, response.getStatus());
}
@Test
void testNotSufficientException() {
HttpClientResponseException e = assertThrows(HttpClientResponseException.class, () -> {
client.order("pot", 100);
});
assertEquals(BAD_REQUEST, e.getStatus());
}
@Test
void testOrderRequest() {
StoreItem plate = client.getAll().stream()
.filter(i -> i.getName().equals("plate"))
.findFirst().orElse(null);
assertNotNull(plate);
assertEquals(100, plate.getNumberInStorage());
HttpResponse<StoreItem> response = client.order("plate", 10);
assertEquals(OK, response.getStatus());
assertNotNull(response.body());
assertEquals("plate", response.body().getName());
assertEquals(90, response.body().getNumberInStorage());
}
@Singleton
@Requires(property = "use-test-storage-service", value = "true")
static class TestStorageService extends DefaultStorageService {
TestStorageService() { // <2>
items = List.of(
new StoreItem("plate", "A large plate", 100),
new StoreItem("pot", "A cooking pot", 10),
new StoreItem("pan", "A large pan", 0)
);
}
}
@Client("/store") // <3>
interface StoreClient {
@Get("/all") // <4>
List<StoreItem> getAll();
@Get("/available") // <4>
List<StoreItem> getAvailable();
@Post("/order/{name}/{amount}") // <4>
HttpResponse<StoreItem> order(String name, Integer amount);
}
}
1 Annotate the class with @MicronautTest
so the Micronaut framework will initialize the application context. This enables you to inject beans using the Jakarta @Inject
annotation and to send requests to the StoreController
defined in the application. Configure this test to use an identified context using the @Property(name = "use-test-storage-service", value = "true")
annotation.
2 Create a mock implementation of StorageService
so that the test is independent of the current state of the storage. The @Requires(property = "use-test-storage-service", value = "true")
means that the bean should only be available if the specified property is set.
3 Create a Micronaut Declarative Client with the same /store
path to send requests to the controller.
4 Create three tests using the defined client and assuming that TestStorageService
is used.
2. Run the Tests #
Run the tests using the following command:
./gradlew test
Then open the file gcp/build/reports/tests/test/index.html in a browser to view the results.
./mvnw test
Although you created this gateway function to run on the Google Cloud Function runtime, the tests should run successfully on your local machine.
Furthermore, Micronaut has a test implementation that simulates the Google Cloud environment. Since sequential requests to a function may be processed by different instances, Micronaut creates a separate environment for each request in test.
3. Create a Google Cloud Function #
3.1. Create a Google Cloud Project #
Create a project in GCP and switch to it:
export PROJECT_ID=gdk-serverless-demo
gcloud projects create $PROJECT_ID
gcloud config set project $PROJECT_ID
Note: The project id must be unique, so you need to provide a different one.
Note: If you want to switch to an existing project use
gcloud projects list
to list all the existing projects.
Enable the required services for the new project:
gcloud services enable cloudbuild.googleapis.com
gcloud services enable cloudfunctions.googleapis.com
3.2. Deploy the Application to a Google Cloud Function #
Create a JAR file from the application, as follows:
./gradlew gcp:clean gcp:shadowJar
The command should create the file gcp/build/libs/gcp-1.0-SNAPSHOT-all.jar. The JAR file is the only one in the directory, which is required for correct deployment.
Create a function from the JAR file with the name gdk-function
: set the entry point, the Java runtime version (select java17
or java21
), the trigger, and the source.
- For JDK 17:
gcloud functions deploy gdk-function \ --entry-point io.micronaut.gcp.function.http.HttpFunction \ --runtime java17 \ --trigger-http \ --source gcp/build/libs \ --allow-unauthenticated
- For JDK 21:
gcloud functions deploy gdk-function \ --gen2 \ --entry-point io.micronaut.gcp.function.http.HttpFunction \ --runtime java21 \ --trigger-http \ --source gcp/build/libs \ --allow-unauthenticated
./mvnw package -pl gcp -am
The command should create the file gcp/target/gcp-1.0-SNAPSHOT.jar.
To deploy, rename and move the JAR file to an empty directory:
mkdir gcp/target/jar
mv gcp/target/gcp-1.0-SNAPSHOT.jar gcp/target/jar/gcp.jar
Create a function from the JAR file with the name gdk-function
: set the entry point, the Java runtime version (select java17
or java21
), the trigger, and the source.
- For JDK 17
gcloud functions deploy gdk-function \ --entry-point io.micronaut.gcp.function.http.HttpFunction \ --runtime java17 \ --trigger-http \ --source gcp/target/jar \ --allow-unauthenticated
- For JDK 21
gcloud functions deploy gdk-function \ --gen2 \ --entry-point io.micronaut.gcp.function.http.HttpFunction \ --runtime java21 \ --trigger-http \ --source gcp/target/jar \ --allow-unauthenticated
Note: You might need to set a billing account for your project to use Google Cloud Functions. Visit Cloud Functions Console to verify that you can create functions for your project.
Note: See the Micronaut Google Cloud Functions Support documentation for more configuration information.
4. Test the Google Cloud Function Deployment #
Now the application is deployed to GCP, you can test it by sending requests.
-
Define an environment variable for your gateway URL. Note that the format for retrieving a URL differs depending if you are running on JDK 17 or JDK 21:
JDK 17:
export GATEWAY_URL=$(gcloud functions describe gdk-function \ --format='value(httpsTrigger.url)')
JDK 21:
export GATEWAY_URL=$(gcloud functions describe gdk-function \ --region=$REGION --format='value(uri)')
The
REGION
variable must be set before.Note: On Windows, use
set GATEWAY_URL=<url>
to set it and%GATEWAY_URL%
to access it. Or you can skip this step and paste the URL to each of the following commands manually. -
Use
curl
to retrieve all the items:curl $GATEWAY_URL/store/all
[ { "name": "chair", "description": "A black chair with 4 legs", "numberInStorage": 10 }, { "name": "table", "description": "A quality dining table", "numberInStorage": 6 }, { "name": "sofa", "description": "A gray sofa", "numberInStorage": 2 }, { "name": "bookshelf", "description": "A futuristic-looking bookshelf", "numberInStorage": 0 } ]
-
Get an error when attempting to order too many items:
curl -X POST $GATEWAY_URL/store/order/table/10
{"message": "Bad Request", "_embedded": { "errors": [{ "message": "Could not order item 'table'. Insufficient amount in storage" }] }, ... }
-
Order an item and print the response status code:
curl -X POST -w "\nStatus code: %{http_code}" $GATEWAY_URL/store/order/table/6
{ "name":"table", "description":"A quality dining table", "numberInStorage":0 } Status code: 200
-
Get the available items:
curl $GATEWAY_URL/store/available
[ { "name": "chair", "description": "A black chair with 4 legs", "numberInStorage": 10 }, { "name": "sofa", "description": "A gray sofa", "numberInStorage": 2 } ]
5. Deploy a Native Executable with Google Cloud Run #
In this section you will use GraalVM Native Image to create a native executable from your Java application and then deploy it to Google Cloud Run.
5.1. Configure the Application #
Reconfigure the Micronaut application to run standalone instead of using the Google Cloud Function environment.
In the gcp/build.gradle file, set the runtime to netty
:
micronaut {
runtime("netty")
testRuntime("junit5")
...
}
Add the following dependency:
implementation("io.micronaut:micronaut-http-server-netty")
In the gcp/pom.xml file set the micronaut.runtime
property to netty
.
Add the following dependency:
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-http-server-netty</artifactId>
<scope>compile</scope>
</dependency>
Remove the following dependency:
<dependency>
<groupId>io.micronaut.gcp</groupId>
<artifactId>micronaut-gcp-function-http</artifactId>
<scope>compile</scope>
</dependency>
5.2. Deploy the Application to Container Registry #
Authenticate docker with Google Cloud Registry (GCR) as shown below. (This will give docker the necessary permissions to push container images to the GCR.)
gcloud auth configure-docker
Push the container image of your application to GCR, as follows:
tasks.named('dockerBuildNative') {
images = ["gcr.io/${System.getProperty("projectId")}/gdk-function:latest"]
}
Run with the GCP project id:
./gradlew gcp:dockerPushNative -DprojectId=$PROJECT_ID
./mvnw install
./mvnw clean deploy -pl gcp \
-Djib.to.image=gcr.io/$PROJECT_ID/gdk-function:latest \
-Dpackaging=docker-native
Note: If the push fails you can retry it with the following command:
docker push gcr.io/$PROJECT_ID/gdk-function:latest
5.3. Create a Google Cloud Run Service #
Enable Google Cloud Run and start a service using the container image you just pushed:
gcloud services enable run.googleapis.com
gcloud run deploy gdk-function \
--image=gcr.io/$PROJECT_ID/gdk-function:latest \
--platform managed \
--allow-unauthenticated
Specify the region in which your application should run.
Use the following command to retrieve the URL (replace [REGION]
with your region, for example, us-east1
):
export GATEWAY_URL=$(gcloud run services describe gdk-function \
--region [REGION] --format 'value(status.url)')
Repeat steps from section 4 to test the application.
Summary #
This guide demonstrated how to create a Micronaut application, compile it into a native executable and run it as a Google Cloud Function.