Create and Deploy a Micronaut Gateway Function to Oracle Cloud Infrastructure
This guide describes how to create a Micronaut Gateway function, deploy it using Oracle Cloud Infrastructure Functions, and access it via Oracle Cloud Infrastructure API Gateway.
Oracle Cloud Infrastructure Functions is a fully managed, multi-tenant, highly scalable, on-demand, Functions-as-a-Service platform.
Prerequisites #
- JDK 17 or higher. See Setting up Your Desktop.
- An Oracle Cloud Infrastructure account. See Setting up Your Cloud Accounts.
- The GCN CLI. See Setting up Your Desktop. (Optional.)
Follow the steps below to create the application from scratch. However, you can also download the completed example in Java:
A note regarding your development environment
Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native extension.
Note: If you use IntelliJ IDEA, enable annotation processing.
1. Create the Application #
This section describes how to create an application for a simple online store. The store will provide information about available items and enable the user to order items. An HTTP controller is responsible for the API implementation and a service stores the availability of items.
Create an application using the GCN Launcher.
-
Open the GCN Launcher in advanced mode.
- Create a new project using the following selections.
- Project Type: Gateway Function
- Project Name: oci-serverless-demo
- Base Package: com.example (Default)
- Clouds: OCI
- Language: Java (Default)
- Build Tool: Gradle (Groovy) or Maven
- Test Framework: JUnit (Default)
- Java Version: 17 (Default)
- Micronaut Version: (Default)
- Cloud Services: None
- Features: GraalVM Native Image, Micronaut Validation
- Sample Code: No
- Click Generate Project. The GCN Launcher creates an application with the default package
com.example
in a directory named oci-serverless-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.
Alternatively, use the GCN CLI as follows:
gcn create-gateway-function com.example.oci-serverless-demo \
--clouds=oci \
--features=graalvm,validation \
--build=gradle \
--lang=java
gcn create-gateway-function com.example.oci-serverless-demo \
--clouds=oci \
--features=graalvm,validation \
--build=maven \
--lang=java
For more information, see Using the GCN CLI.
1.1. StoreItem #
The launcher created 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 #
The launcher generated 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 #
-
The launcher created 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.
-
The launcher generated 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 grey 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 = "oraclecloud")
annotation to make it specific to Oracle Cloud Infrastructure. Visit the Database Module for details about how to store and manipulate data in a database.
1.4. Tests to Verify Application Logic #
The launcher created a test class for the controller in oci/src/test/java/com/example/StoreControllerTest.java, as follows:
package com.example;
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(environments = "test-storage-service") // <1>
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(env = "test-storage-service")
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 environment using the @MicronautTest(environments="test-storage-service")
annotation.
2 Create a mock implementation of StorageService
so that the test is independent of the current state of the storage. The @Requires(env="test-storage-service")
annotation specifies that the bean should only be available in the identified environment. (In this case it matches the one identified in the @MicronautTest
annotation.)
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 oci/build/reports/tests/test/index.html in a browser to view the results.
./mvnw test
Although you created this application to run on Oracle Cloud Infrastructure, the tests should run successfully on your local machine.
Furthermore, Micronaut has a test implementation that simulates the Oracle Cloud Infrastructure Function environment. Since sequential requests to a function may be processed by different instances, Micronaut creates a separate environment for each request in a test.
3. Create a Function in Oracle Cloud Infrastructure #
This section describes how to create a container image from the function and publish it to Oracle Cloud Infrastructure Registry (also known as Container Registry).
3.1. Authenticate with Container Registry #
Container Registry is an Oracle-managed registry to store, share, and manage container images (such as Docker images).
Before you can publish a container image to Container Registry, you must first authenticate with it. For this, create an authentication token and retrieve a few more properties.
-
Find out your Object storage namespace by viewing your Tenancy details page. Make a note of it.
-
An authentication token is bound to a user, so first select a user. In the Oracle Cloud Console, open the navigation menu, click Identity & Security. Under Identity click Users. Click the name of the user.
-
Scroll to the bottom of the User Details page, and select Auth tokens in the Resources section. Click Generate token:
-
Create a description for the token and click Generate token:
-
Click Copy to copy the generated token:
-
-
Find out your region identifier. You can see your region on the right of the header in the Oracle Cloud Console. Look for your region identifier in the Oracle Cloud Infrastructure regions documentation page. For example, ca-toronto-1 is for Canada Southeast (Toronto).
-
Authenticate with
docker
to Container Registry with your region identifier:docker login <region-identifier>.ocir.io
-
When asked for a username, provide
<object storage namespace>/<username>
, for exampleaaaaaaaaaaaa/example@example.com
. -
When asked for a password, provide the Auth token.
The command should complete by printing
Login Succeeded
. (It may take some time before the authentication token activates.) -
3.2. Publish the Application to Container Registry #
To publish a container image to Container Registry, provide the path to a container repository in the build file.
Modify the oci/build.gradle file. Make sure it contains the following contents and that nothing overrides the set properties:
tasks {
dockerfileNative {
args("-XX:MaximumHeapSizePercent=80")
}
dockerBuild {
images = ["[REGION].ocir.io/[TENANCY]/[REPO]/$project.name:$project.version"]
}
dockerBuildNative {
images = ["[REGION].ocir.io/[TENANCY]/[REPO]/$project.name-native:$project.version"]
}
}
-
Modify the
dockerBuild.images
property: enter your region identifier (REGION
), your object storage namespace (TENANCY
), and repository (REPO
) correctly. The value of the property should be similar toca-toronto-1.ocir.io/aaaaaaaaaaaa/gcn-function-demo/$rootProject.name:$project.version
.Note: The name of the repository where the container image will be published is
gcn-function-demo
. You could set it to any other name, such as[USERNAME]/[REPO]/gcn-function-demo
. You can also specify a different project name or version instead of the parameters. -
Set the
dockerfile.baseImage
property to use the Java 17 runtime environment. -
Additionally, note that the value of
micronaut.runtime
isoracle_function
because this is the runtime you want to use.
After modifying the configuration, run the following command to publish the container image to Container Registry:
./gradlew oci:dockerPush
Modify the oci/pom.xml file. Add the following properties:
<properties>
...
<regionIdentifier>[REGION IDENTIFIER]</regionIdentifier>
<objectStorageNamespace>[OBJECT STORAGE NAMESPACE]</objectStorageNamespace>
<jib.docker.image>${regionIdentifier}.ocir.io/${objectStorageNamespace}/gcn-function-demo/${project.parent.artifactId}</jib.docker.image>
<jib.docker.tag>${project.version}</jib.docker.tag>
...
</properties>
-
Enter your region identifier and your object storage namespace correctly. The parameter should be similar to
ca-toronto-1.ocir.io/aaaaaaaaaaaa/gcn-function-demo/${project.parent.artifactId}
.Note: The
gcn-function-demo
is the name of the repository where the container image will be published. You could set it to any name, such as[USERNAME]/[REPO]/gcn-function-demo
. You can also specify a different project name or version instead of the parameters. -
Additionally, notice that the
micronaut.runtime
property is set tooracle_function
as this is the runtime you want to use.
After modifying the configuration, run the following commands to publish the container image to Container Registry:
./mvnw install -pl lib -am
./mvnw deploy -Dpackaging=docker -pl oci
3.3. Create a Virtual Cloud Network #
Create a Virtual Cloud Network for the function.
-
In the Oracle Cloud Console, open the navigation menu, click Networking. Click Virtual Cloud Networks.
-
Click Start VCN Wizard.
-
Select Create VCN with Internet connectivity, click Start VCN Wizard.
-
Enter a name for the VCN and select the desired compartment from the drop-down list. Click Next.
-
Review the information and click Create.
When the VCN has been successfully created, click View Virtual Cloud Network. The next step is to add an ingress rule to allow HTTPS connections to the public subnet of the VCN.
-
Select Security Lists from the list of Resources and then select the Default Security List.
-
Click Add Ingress Rules.
-
Use the following properties:
- Source Type: CIDR
- Source CIDR: 0.0.0.0/0
- IP Protocol: TCP
- Destination Port Range: 443
Click Add Ingress Rules.
3.4. Create an Application and a Function #
This section describes how to create the Oracle function.
-
In the Oracle Cloud Console, open the navigation menu, click Developer Services. Under Functions, click Applications.
-
Click Create application.
-
Enter
gcn-serverless-demo
as the name of the application, select the VCN you created above, its public subnet, and an appropriate shape for your local environment. Click Create. -
In the page that describes your new application, click the Functions resource. Click Create function and select Create from existing image. (If required, change compartment to select your published container image.)
-
Name the function as
gcn-serverless-func
, select a repository and a container image. Increase the memory to512
and click Create.
4. Create an API Gateway in Oracle Cloud Infrastructure #
This section describes how to create an API Gateway in Oracle Cloud Infrastructure to provide access to the function.
4.1. Create an API Gateway #
-
In the Oracle Cloud Console, open the navigation menu, click Developer Services. Under API Management click Gateways.
-
Click Create Gateway.
-
Enter
gcn-serverless-demo-gateway
as the name of the new gateway,Public
as its type, and select your compartment. Choose your VCN and its public subnet. Click Create Gateway.
4.2. Create a Deployment #
-
In the page describing your new gateway, select the Deployments resource. Click Create deployment.
-
Enter a name for the deployment, enter
/store
as the path prefix (as it is the prefix of the controller you created). Select your compartment and click Next. -
Select No Authentication and click Next.
-
Enter
/{path*}
as the value ofpath
to allow all paths (starting with the/store
prefix). Allow all methods by selectingANY
. SelectAdd single backend
,Oracle functions
, choose the application and function from their respective drop-down lists. Click Next. -
Review the deployment and click Create.
After the deployment is created, on the page describing your new Gateway page, copy the hostname. This is the hostname that you will use to send requests to the function.
4.3. Create a Policy #
You must give the gateway permission to call the function. To do this, create a policy that will apply for all the API gateways in the compartment.
-
In the Oracle Cloud Console, open the navigation menu, click Identity & Security. Under Identity click Policies.
-
Select the compartment and click Create Policy.
- Provide a name for the policy, select your compartment, and select Show manual editor. Paste the following contents in the policy (filling in your compartment OCID):
Allow any-user to use functions-family in compartment id [YOUR COMPARTMENT OCID] where ALL {request.principal.type= 'ApiGateway', request.resource.compartment.id = '[YOUR COMPARTMENT OCID]'}
- Click Create.
5. Test the Oracle Function #
Now the application is deployed to Oracle Cloud Infrastructure, you can test it by sending requests.
-
Define an environment variable for your gateway hostname:
export GATEWAY_HOSTNAME=aaaaaaaaaaaaaaaaaaaaaaa.apigateway.ca-toronto-1.oci.customer-oci.com
Note: On Windows, use
set GATEWAY_HOSTNAME=<hostname>
to set it and%GATEWAY_HOSTNAME%
to access it. Or you can skip this step and paste the hostname to each of the following commands manually. -
Use
curl
to retrieve all the items:curl https://$GATEWAY_HOSTNAME/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 grey 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 https://$GATEWAY_HOSTNAME/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}" https://$GATEWAY_HOSTNAME/store/order/table/6
{"message": "Bad Request", "_embedded": {"errors": [{"message": "Could not order item 'table'. Insufficient amount in storage"}]}, ... } Status code: 400
-
Get the available items:
curl https://$GATEWAY_HOSTNAME/store/available
[{"name": "chair", "description": "A black chair with 4 legs", "numberInStorage": 10}, {"name": "sofa", "description": "A grey sofa", "numberInStorage": 2}]
6. Publish a Native Executable to Oracle Cloud Infrastructure #
6.1. Publish the Native Executable to Container Registry #
To publish a native executable container image to Container Registry, provide the path to a container repository in the build file.
Modify the oci/build.gradle file. Make sure it contains the following contents and that nothing overrides the set properties:
var regionIdentifier = "[REGION IDENTIFIER]"
var objectStorageNamespace = "[OBJECT STORAGE NAMESPACE]"
tasks {
dockerBuild {
images = ["${regionIdentifier}.ocir.io/${objectStorageNamespace}/gcn-function-demo/${rootProject.name}:${project.version}"]
}
dockerBuildNative {
images = ["${regionIdentifier}.ocir.io/${objectStorageNamespace}/gcn-function-demo/${rootProject.name}-native:${project.version}"]
}
dockerfile {
baseImage('fnproject/fn-java-fdk:jre17-latest')
}
dockerfileNative {
args("-XX:MaximumHeapSizePercent=80")
baseImage('gcr.io/distroless/cc-debian10')
}
}
- Fill in the region identifier and object storage namespace as described in section 3.2.
- Set the container image name for the native docker build
- Use a distroless base container image and limit the heap size to 80% for native executable
After modifying the configuration, run the following command to publish the container image to Container Registry:
./gradlew oci:dockerPushNative
Modify the oci/pom.xml file. Add the following profile:
<profiles>
<profile>
<id>docker-native</id>
<activation>
<property>
<name>packaging</name>
<value>docker-native</value>
</property>
</activation>
<properties>
<jib.docker.image>${regionIdentifier}.ocir.io/${objectStorageNamespace}/gcn-function-demo/${project.parent.artifactId}-native:${project.version}</jib.docker.image>
</properties>
<build>
<plugins>
<plugin>
<groupId>io.micronaut.build</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
<configuration>
<baseImageRun>gcr.io/distroless/cc-debian10</baseImageRun>
<nativeImageBuildArgs>
<arg>--initialize-at-build-time=com.example</arg>
</nativeImageBuildArgs>
</configuration>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<configuration>
<to>
<image>${jib.docker.image}</image>
</to>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
- Make sure that the region identifier and object storage namespace properties are set as described in section 3.2..
- Set the container image name for the native docker build
- Use a distroless base container image
After modifying the configuration, run the following commands to publish the container image to Container Registry:
./mvnw install -pl lib -am
./mvnw deploy -Dpackaging=docker-native -pl oci
6.2. Create the Oracle Cloud Infrastructure Function #
To create a function on Oracle Functions with the native executable, repeat the steps in section 3.2., then set up an API gateway as described in section 4.
Alternatively, you can open the existing application in Oracle Cloud Infrastructure, find the function that you created and click Edit. In the popup, choose the native executable from the container registry and click Save. Once the function reloads, use the same API Gateway to access it and test it as described in section 5.
Summary #
This guide demonstrated how to create a Micronaut Gateway Function application, run it on Oracle Functions, and set up an API Gateway to provide access to the function.