mn create-app --features=oracle-function,oracle-cloud-sdk,graalvm example.micronaut.micronautguide --build=gradle --lang=kotlin --jdk=11
Table of Contents
Deploy a Micronaut HTTP API Gateway Function (Serverless) application to Oracle Cloud
Learn how to deploy a Micronaut HTTP API Gateway Function (Serverless) application to Oracle Cloud.
Authors: Burt Beckwith
Micronaut Version: 3.9.2
1. Getting Started
In this guide, we will create a Micronaut application written in Kotlin.
2. What you will need
To complete this guide, you will need the following:
-
Some time on your hands
-
A decent text editor or IDE
-
JDK 11 or greater installed with
JAVA_HOME
configured appropriately -
Docker installed
-
A paid or free trial Oracle Cloud account (create an account at signup.oraclecloud.com)
-
Oracle Cloud CLI installed with local access to Oracle Cloud configured by running
oci setup config
3. Solution
We recommend that you follow the instructions in the next sections and create the application step by step. However, you can go right to the completed example.
-
Download and unzip the source
4. Writing the Application
Create an application using the Micronaut Command Line Interface or with Micronaut Launch.
If you don’t specify the --build argument, Gradle is used as the build tool. If you don’t specify the --lang argument, Java is used as the language.
|
If you use Micronaut Launch, select "Micronaut Application" as application type, JDK version 11 or higher, and add the oracle-function
, oracle-cloud-sdk
, and graalvm
features.
The previous command creates a Micronaut application with the default package example.micronaut
in a directory named micronautguide
.
If you have an existing Micronaut application and want to add the functionality described here, you can view the dependency and configuration changes from the specified features and apply those changes to your application. |
4.1. Dependencies
Add a dependency for oci-java-sdk-core
so we have access to classes for managing compute instances:
implementation("com.oracle.oci.sdk:oci-java-sdk-core")
4.2. InstanceData
Then create an InstanceData
DTO class to represent Compute Instance properties:
package example.micronaut
import com.oracle.bmc.core.model.Instance
import com.oracle.bmc.core.model.Instance.LifecycleState
import io.micronaut.core.annotation.Introspected
import java.util.Date
@Introspected
class InstanceData(instance: Instance) {
val availabilityDomain: String
val compartmentOcid: String
val displayName: String
val lifecycleState: LifecycleState
val ocid: String
val region: String
val timeCreated: Date
init {
availabilityDomain = instance.availabilityDomain
compartmentOcid = instance.compartmentId
displayName = instance.displayName
lifecycleState = instance.lifecycleState
ocid = instance.id
region = instance.region
timeCreated = instance.timeCreated
}
}
4.3. MicronautguideController
The generated application contains a MicronautguideController
class which is good for getting started, but we’ll update it to demonstrate working with OCI SDK APIs, in this case working with Compute Instances.
Replace the generated MicronautguideController
with this:
package example.micronaut
import com.oracle.bmc.core.ComputeClient
import com.oracle.bmc.core.model.Instance.LifecycleState.Running
import com.oracle.bmc.core.model.Instance.LifecycleState.Stopped
import com.oracle.bmc.core.requests.GetInstanceRequest
import com.oracle.bmc.core.requests.InstanceActionRequest
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import org.slf4j.LoggerFactory
@Controller("/compute") (1)
class MicronautguideController(private val computeClient: ComputeClient) { (2)
private val log = LoggerFactory.getLogger(javaClass.name)
@Get("/status/{ocid}") (3)
fun status(ocid: String) = InstanceData(getInstance(ocid))
@Post("/start/{ocid}") (4)
fun start(ocid: String): InstanceData {
log.info("Starting Instance: {}", ocid)
var instance = getInstance(ocid)
if (instance.lifecycleState == Stopped) {
val response = instanceAction(ocid, START)
log.info("Start response code: {}", response.__httpStatusCode__)
instance = response.instance
} else {
log.info("The instance was in the incorrect state ({}) to start: {}",
instance.lifecycleState, ocid)
}
log.info("Started Instance: {}", ocid)
return InstanceData(instance)
}
@Post("/stop/{ocid}") (5)
fun stop(ocid: String): InstanceData {
log.info("Stopping Instance: {}", ocid)
var instance = getInstance(ocid)
if (instance.lifecycleState == Running) {
val response = instanceAction(ocid, STOP)
log.info("Stop response code: {}", response.__httpStatusCode__)
instance = response.instance
} else {
log.info("The instance was in the incorrect state ({}) to stop: {}",
instance.lifecycleState, ocid)
}
log.info("Stopped Instance: {}", ocid)
return InstanceData(instance)
}
private fun instanceAction(ocid: String, action: String) =
computeClient.instanceAction(InstanceActionRequest.builder()
.instanceId(ocid)
.action(action)
.build())
private fun getInstance(ocid: String) =
computeClient.getInstance(GetInstanceRequest.builder()
.instanceId(ocid)
.build()).instance
companion object {
private const val START = "START"
private const val STOP = "STOP"
}
}
1 | The controller’s root URI is /compute |
2 | Here we dependency-inject the SDK ComputeClient instance |
3 | This endpoint accepts GET requests and returns current instance properties for the specified instance OCID |
4 | This endpoint accepts POST requests and starts the specified instance if it is stopped |
5 | This endpoint accepts POST requests and stops the specified instance if it is running |
5. Testing the Application
The code in this guide should work with recent versions of the Micronaut framework 2.5+, but the tests require at least version 2.5.7 |
We need to update the generated MicronautguideControllerTest
to test the changes we made in MicronautguideController
.
This will be a unit test, so we’ll need some mock beans to replace the beans auto-registered by the oracle-cloud-sdk
module which make requests to Oracle Cloud. We also need to take into consideration the strict class loader isolation used by Fn Project (which powers Oracle Cloud Functions).
When running our tests, the test and mock classes are loaded by the regular Micronaut framework classloader, but the function invocations are made in a custom Fn
classloader, so it is not directly possible to share state between the two, which complicates traditional approaches to mocking. There is support in Fn
for sharing classes however, so instead of setting values in mock instances from the test to be used by the controller we’ll set values in a MockData
helper class that’s registered as a class that’s shared between classloaders.
Create the MockData
class:
package example.micronaut.mock
import com.oracle.bmc.core.model.Instance.LifecycleState
object MockData {
var instanceOcid = "test-instance-id"
var instanceLifecycleState: LifecycleState? = null
fun reset() {
instanceOcid = "test-instance-id"
instanceLifecycleState = null
}
}
Next create the MockAuthenticationDetailsProvider
class. The methods all return null
since they won’t be called; the bean merely needs to exist for dependency injection:
package example.micronaut.mock
import com.oracle.bmc.auth.AuthCachingPolicy
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider
import com.oracle.bmc.auth.ConfigFileAuthenticationDetailsProvider
import io.micronaut.context.annotation.Replaces
import java.io.InputStream
import jakarta.inject.Singleton
@AuthCachingPolicy(cacheKeyId = false, cachePrivateKey = false) (1)
@Singleton
@Replaces(ConfigFileAuthenticationDetailsProvider::class) (2)
class MockAuthenticationDetailsProvider : BasicAuthenticationDetailsProvider {
override fun getKeyId(): String? = null
override fun getPrivateKey(): InputStream? = null
override fun getPassPhrase(): String? = null
override fun getPassphraseCharacters(): CharArray? = null
}
1 | The AuthCachingPolicy annotation disables caching; without this we would need to provide a valid private key since the provider methods would be invoked when constructing SDK client classes |
2 | We use @Replaces to replace the previously auto-configuration Oracle Cloud authentication method |
Next create the MockComputeClient
class which will replace the real ComputeClient
bean:
package example.micronaut.mock
import com.oracle.bmc.auth.BasicAuthenticationDetailsProvider
import com.oracle.bmc.core.ComputeClient
import com.oracle.bmc.core.model.Instance
import com.oracle.bmc.core.requests.GetInstanceRequest
import com.oracle.bmc.core.requests.InstanceActionRequest
import com.oracle.bmc.core.responses.GetInstanceResponse
import com.oracle.bmc.core.responses.InstanceActionResponse
import io.micronaut.context.annotation.Replaces
import jakarta.inject.Singleton
@Singleton
@Replaces(ComputeClient::class) (1)
class MockComputeClient(provider: BasicAuthenticationDetailsProvider) (3)
: ComputeClient(provider) { (2)
override fun getInstance(request: GetInstanceRequest) =
GetInstanceResponse.builder()
.instance(buildInstance(MockData.instanceLifecycleState!!))
.build()
override fun instanceAction(request: InstanceActionRequest): InstanceActionResponse {
val lifecycleState = if ("START" == request.action)
Instance.LifecycleState.Starting else
Instance.LifecycleState.Stopping
return InstanceActionResponse.builder()
.instance(buildInstance(lifecycleState))
.build()
}
private fun buildInstance(lifecycleState: Instance.LifecycleState) =
Instance.builder()
.id(MockData.instanceOcid)
.lifecycleState(lifecycleState)
.build()
}
1 | We use @Replaces to replace a previously auto-registered ComputeClient bean |
2 | The mock class subclasses the real ComputeClient class and overrides only the methods used by the controller |
3 | The BasicAuthenticationDetailsProvider bean (in this case the MockAuthenticationDetailsProvider bean created earlier) is dependency-injected because it’s needed by the ComputeClient constructor |
Finally, replace the generated MicronautguideControllerTest
with this:
package example.micronaut
import com.oracle.bmc.core.model.Instance.LifecycleState
import com.oracle.bmc.core.model.Instance.LifecycleState.Running
import com.oracle.bmc.core.model.Instance.LifecycleState.Stopped
import example.micronaut.mock.MockData
import example.micronaut.mock.MockData.reset
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpStatus.OK
import io.micronaut.oraclecloud.function.http.test.FnHttpTest
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import java.util.UUID
@MicronautTest
@Disabled("Kotlin tests currently fail due to issue with Fn")
class MicronautguideControllerTest {
@Test
fun testStatus() {
val instanceOcid = UUID.randomUUID().toString()
MockData.instanceOcid = instanceOcid; (2)
MockData.instanceLifecycleState = Running;
val response = FnHttpTest.invoke( (3)
HttpRequest.GET<Any>("/compute/status/$instanceOcid"),
SHARED_CLASSES)
assertEquals(OK, response.status())
assertEquals(
"{\"lifecycleState\":\"Running\",\"ocid\":\"$instanceOcid\"}", (4)
response.body())
}
@Test
fun testStart() {
val instanceOcid = UUID.randomUUID().toString()
MockData.instanceOcid = instanceOcid;
MockData.instanceLifecycleState = Stopped;
val response = FnHttpTest.invoke(
HttpRequest.POST<Any?>("/compute/start/$instanceOcid", null),
SHARED_CLASSES)
assertEquals(OK, response.status())
assertEquals(
"{\"lifecycleState\":\"Starting\",\"ocid\":\"$instanceOcid\"}",
response.body())
}
@Test
fun testStop() {
val instanceOcid = UUID.randomUUID().toString()
MockData.instanceOcid = instanceOcid;
MockData.instanceLifecycleState = Running;
val response = FnHttpTest.invoke(
HttpRequest.POST<Any?>("/compute/stop/$instanceOcid", null),
SHARED_CLASSES)
assertEquals(OK, response.status())
assertEquals(
"{\"lifecycleState\":\"Stopping\",\"ocid\":\"$instanceOcid\"}",
response.body())
}
@AfterEach
fun cleanup() {
reset()
}
companion object {
private val SHARED_CLASSES = listOf( (1)
MockData::class.java,
LifecycleState::class.java)
}
}
1 | The MockData and LifecycleState need to be passed to test function invocations as shared classes |
2 | Here we set data to be used by the mocks in the MockData class |
3 | The controller is invoked with the FnHttpTest class that, along with MockFnHttpServer , provides a bridge between Micronaut controllers and an Fn Project gateway in tests |
4 | We expect the String response to be the JSON generated from the InstanceData returned by the controller |
To run the tests:
./gradlew test
Then open build/reports/tests/test/index.html
in a browser to see the results.
6. Configuring Oracle Cloud Resources
We need to configure some cloud infrastructure to support deploying functions.
Initially, do all the configuration steps described in the Deploy a Micronaut Function (Serverless) application to Oracle Cloud guide’s "Configuring Oracle Cloud Resources" section since they’re the same as for HTTP Gateway functions. To summarize, do the following (unless a resource exists and can be used):
-
create a compartment
-
create a function user and group
-
create an auth token
-
configure the OCIR repository in your build script and authenticate to OCIR
-
create a VCN and subnet
-
create policies
There is some more infrastructure configuration to do, but we’ll need to create the function first.
7. Creating the function
First, build the function as a Docker image and push it to the OCIR repository by running:
./gradlew dockerPush
Once you’ve pushed the Docker container, create the function in the console. First, log out from your administrator account and log in as the user created above.
Open the Oracle Cloud Menu and click "Developer Services", and then "Applications" under "Functions":
Click "Create Application":
Choose a name for the application, e.g. mn-guide-http-function-app
, and select the VCN created earlier. Select the private subnet, and click "Create":
Click "Functions" under "Resources" on the left, and then click "Create Function":
Choose a name for the function, e.g. mn-guide-http-function
, select the repository where you pushed the Docker image, and select the uploaded image. Select 512MB memory and click "Create":
8. Configuring Oracle Cloud Resources (continued)
Like earlier, do all the configuration steps described in the Deploy a Micronaut Function (Serverless) application to Oracle Cloud guide’s "Enable Tracing and Logs" section since they’re the same as for HTTP Gateway functions. To summarize, do the following (unless a resource exists and can be used):
-
create an APM domain
-
enable logs for your HTTP function
-
enable traces for your HTTP function
Next we’ll create an API Gateway, plus a few smaller tasks.
8.1. API Gateway
Create an API gateway by clicking the Oracle Cloud menu and selecting "Developer Services", and then click "Gateways":
Click "Create Gateway"
then choose a name, e.g. mn-guide-gateway
, then choose a compartment, VCN, and subnet as before:
Click "Deployments", then "Create Deployment":
Choose a name for the deployment (e.g. mn-guide-deployment
), and use the controller’s root URI (/compute
) as the "Path Prefix" value, then click "Next".
Enter /{path*}
as the "Path" value to capture all incoming requests; the Micronaut router will match the incoming path and request method with the proper controller method. Choose ANY
under "Methods", and Oracle Functions
as the "Type". Choose mn-guide-http-function-app
as the "Application" and mn-guide-http-function
as the "Function Name", then click "Next":
Verify that everything looks ok and click "Create":
Click the "Copy" link in the "Endpoint" column; this is the base controller URL which will be needed later when testing the function:
See the API Gateway docs for more information.
8.2. Remaining Configuration
8.2.1. Ingress Rule
First, add an ingress rule for HTTPS on port 443. Open the Oracle Cloud Menu and click "Networking", then "Virtual Cloud Networks":
Click the link for mn-functions-vcn
:
Then click "Security Lists", and click the link for "Default Security List for mn-functions-vcn":
Then click "Add Ingress Rules":
Enter 0.0.0.0/0
for the source CIDR value, and 433
for the destination port range, and click "Add Ingress Rules":
Next we need to grant the function permission to access other cloud resources, in this case compute instances. That will involve creating a dynamic group and adding a new policy statement.
8.2.2. Dynamic Group
Create a Dynamic Group by clicking the Oracle Cloud menu and selecting "Identity & Security", and then click "Dynamic Groups":
Click "Create Dynamic Group":
Then enter a name and description for the group, e.g. "mn-guide-dg", and a matching rule, i.e. the logic that will be used to determine group membership. We’ll make the rule fairly broad - enter ALL {resource.type = 'fnfunc', resource.compartment.id = 'ocid1.compartment.oc1..aaaaaxxxxx'}
replacing ocid1.compartment.oc1..aaaaaxxxxx
with the compartment OCID where you’re defining your functions, and click "Create":
See the Dynamic Group docs for more information.
8.2.3. Dynamic Group Policy Statement
Next create a policy statement granting members of the dynamic group permission to manage compute instances. Open the Oracle Cloud Menu and click "Identity & Security", and then "Policies":
Click the link for the Policy you created earlier (i.e. mn-functions-compartment-policy
):
Then click "Edit Policy Statements":
Click "+ Another Statement":
and enter Allow dynamic-group mn-guide-dg to manage instances in compartment <compartment-name>
, replacing <compartment-name>
with the compartment OCID where you’re defining your functions, and click "Save Changes":
9. Invoking the function
Since the function works with Compute Instances, make sure you have at least one running. If you don’t have any, one easy option is with the Deploy a Micronaut application to Oracle Cloud guide.
Now is when you need the base controller URL that you copied when creating the API Gateway; it should look something like https://cjrgh5e3lfqz….apigateway.us-ashburn-1.oci.customer-oci.com/compute
and end in /compute
since that’s the root URI of the controller.
First, get the status of an instance in a web browser or with cURL by appending /status/INSTANCE_OCID
to the base controller URL, replacing INSTANCE_OCID
with the OCID of the Compute Instance to query:
curl -i https://cjrgh5e3lfqz....apigateway.us-ashburn-1.oci.customer-oci.com/compute/status/ocid1.instance.oc1.iad.anuwcljrbnqp5k...
and the output should look something like this:
{
"availabilityDomain":"nFuS:US-ASHBURN-AD-1",
"compartmentOcid":"ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbmqnj...",
"displayName":"dribneb",
"lifecycleState":"RUNNING",
"ocid":"ocid1.instance.oc1.iad.anuwcljrbnqp5k...",
"region":"iad",
"timeCreated":1624594779093
}
You can also invoke the /status action in a web browser since it’s a GET method, but the others require cURL or some other application that can make POST requests
|
The first invocation ("cold start") will take a while as the infrastructure is configured, probably 10-20 seconds or more but subsequent invocations should return in 1-2 seconds.
Next, stop the instance with the same URL, except replace /status/
with /stop/
:
curl -i -H "Content-Type: application/json" -X POST https://cjrgh5e3lfqz....apigateway.us-ashburn-1.oci.customer-oci.com/compute/stop/ocid1.instance.oc1.iad.anuwcljrbnqp5k...
and the output should look something like this (it should be the same as before except lifecycleState
should be STOPPING
):
{
"availabilityDomain":"nFuS:US-ASHBURN-AD-1",
"compartmentOcid":"ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbmqnj...",
"displayName":"dribneb",
"lifecycleState":"STOPPING",
"ocid":"ocid1.instance.oc1.iad.anuwcljrbnqp5k...",
"region":"iad",
"timeCreated":1624594779093
}
Once the status is STOPPED
you can start it again with the same URL, except replace /stop/
with /start/
:
curl -i -H "Content-Type: application/json" -X POST https://cjrgh5e3lfqz....apigateway.us-ashburn-1.oci.customer-oci.com/compute/start/ocid1.instance.oc1.iad.anuwcljrbnqp5k...
and the output should look something like this (it should be the same as before except lifecycleState
should be STARTING
):
{
"availabilityDomain":"nFuS:US-ASHBURN-AD-1",
"compartmentOcid":"ocid1.compartment.oc1..aaaaaaaarkh3s2wcxbbmqnj...",
"displayName":"dribneb",
"lifecycleState":"STARTING",
"ocid":"ocid1.instance.oc1.iad.anuwcljrbnqp5k...",
"region":"iad",
"timeCreated":1624594779093
}
10. Deploying as a Native Executable
10.1. Install GraalVM
We will use GraalVM, the polyglot embeddable virtual machine, to generate a Native executable of our function.
Compiling native executables ahead-of-time with GraalVM improves startup time and reduces the memory footprint of JVM-based applications and functions.
Only Java and Kotlin projects support using GraalVM’s native-image tool. Groovy relies heavily on reflection, which is only partially supported by GraalVM.
|
The easiest way to install GraalVM on Linux or Mac is to use SDKMan.io.
sdk install java 22.3.r11-grl
If you still use Java 8, use the JDK11 version of GraalVM. |
sdk install java 22.3.r17-grl
For installation on Windows, or for manual installation on Linux or Mac, see the GraalVM Getting Started documentation.
After installing GraalVM, install the native-image
component, which is not installed by default:
gu install native-image
10.2. Building and deploying the native executable
Deploying the function as a native executable is similar to the earlier deployment above.
First you need to update your build script with the location to deploy the native executable Docker container.
Edit build.gradle
like before, but set the images
property in the dockerBuildNative
block this time, replacing REGION
, TENANCY
, and REPO
as before:
dockerBuildNative {
images = ["[REGION].ocir.io/[TENANCY]/[REPO]/$project.name-native:$project.version"]
}
Since it’s unlikely that you’ll be deploying both jar-based containers and native executable-based containers, you can use the same repo:
dockerBuildNative {
images = ["[REGION].ocir.io/[TENANCY]/[REPO]/$project.name:$project.version"]
}
Next, update the version.
Edit build.gradle
and increment the version to 0.2
:
version = "0.2"
Depending on the Micronaut version you’re using, you might also need to update some properties in your build script to update the Docker configuration.
In your build.gradle
, change the base image to gcr.io/distroless/cc-debian10
in the dockerfileNative
block:
dockerfileNative {
args("-XX:MaximumHeapSizePercent=80")
baseImage('gcr.io/distroless/cc-debian10')
}
Then from the demo project directory, run:
./gradlew dockerPushNative
Once you’ve pushed the Docker container, edit the function in the console to use the new container, and to reduce the memory to 128MB:
Use the same OCI command as before to invoke the function. No changes are needed because the function OCID doesn’t change when deploying new containers.
11. Next steps
Explore more features with Micronaut Guides.
Read more about the Micronaut Oracle Cloud integration.
Also check out the Oracle Cloud Function documentation for more information on the available functionality.