mn create-app --features=oracle-function,oracle-cloud-sdk,graalvm example.micronaut.micronautguide --build=gradle --lang=groovy --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 Groovy.
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 groovy.transform.CompileStatic
import io.micronaut.core.annotation.Introspected
@CompileStatic
@Introspected
class InstanceData {
final String availabilityDomain
final String compartmentOcid
final String displayName
final LifecycleState lifecycleState
final String ocid
final String region
final Date timeCreated
InstanceData(Instance instance) {
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
import com.oracle.bmc.core.requests.GetInstanceRequest
import com.oracle.bmc.core.requests.InstanceActionRequest
import com.oracle.bmc.core.responses.InstanceActionResponse
import io.micronaut.http.annotation.Controller
import io.micronaut.http.annotation.Get
import io.micronaut.http.annotation.Post
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.oracle.bmc.core.model.Instance.LifecycleState.Running
import static com.oracle.bmc.core.model.Instance.LifecycleState.Stopped
@Controller('/compute') (1)
class MicronautguideController {
private final Logger log = LoggerFactory.getLogger(getClass().name)
private static final String START = 'START'
private static final String STOP = 'STOP'
private final ComputeClient computeClient
MicronautguideController(ComputeClient computeClient) { (2)
this.computeClient = computeClient
}
@Get('/status/{ocid}') (3)
InstanceData status(String ocid) {
return new InstanceData(getInstance(ocid))
}
@Post('/start/{ocid}') (4)
InstanceData start(String ocid) {
log.info('Starting Instance: {}', ocid)
Instance instance = getInstance(ocid)
if (instance.lifecycleState == Stopped) {
InstanceActionResponse 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 new InstanceData(instance)
}
@Post('/stop/{ocid}') (5)
InstanceData stop(String ocid) {
log.info('Stopping Instance: {}', ocid)
Instance instance = getInstance(ocid)
if (instance.lifecycleState == Running) {
InstanceActionResponse 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 new InstanceData(instance)
}
private InstanceActionResponse instanceAction(String ocid, String action) {
InstanceActionRequest request = InstanceActionRequest.builder()
.instanceId(ocid)
.action(action)
.build()
return computeClient.instanceAction(request)
}
private Instance getInstance(String ocid) {
GetInstanceRequest getInstanceRequest = GetInstanceRequest.builder()
.instanceId(ocid)
.build()
return computeClient.getInstance(getInstanceRequest).instance
}
}
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
class MockData {
public static String instanceOcid = 'test-instance-id'
public static LifecycleState instanceLifecycleState
static void 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 jakarta.inject.Singleton
@AuthCachingPolicy(cacheKeyId = false, cachePrivateKey = false) (1)
@Singleton
@Replaces(ConfigFileAuthenticationDetailsProvider) (2)
class MockAuthenticationDetailsProvider implements BasicAuthenticationDetailsProvider {
String keyId
InputStream privateKey
String passPhrase
char[] passphraseCharacters
}
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.model.Instance.LifecycleState
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
import static com.oracle.bmc.core.model.Instance.LifecycleState.Starting
import static com.oracle.bmc.core.model.Instance.LifecycleState.Stopping
@Singleton
@Replaces(ComputeClient) (1)
class MockComputeClient extends ComputeClient { (2)
MockComputeClient(BasicAuthenticationDetailsProvider provider) { (3)
super(provider)
}
@Override
GetInstanceResponse getInstance(GetInstanceRequest request) {
return GetInstanceResponse.builder()
.instance(buildInstance(MockData.instanceLifecycleState))
.build()
}
@Override
InstanceActionResponse instanceAction(InstanceActionRequest request) {
LifecycleState lifecycleState = 'START' == request.getAction() ? Starting : Stopping
return InstanceActionResponse.builder()
.instance(buildInstance(lifecycleState))
.build()
}
private Instance buildInstance(LifecycleState lifecycleState) {
return 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 example.micronaut.mock.MockData
import io.micronaut.http.HttpRequest
import io.micronaut.http.HttpResponse
import io.micronaut.oraclecloud.function.http.test.FnHttpTest
import io.micronaut.test.extensions.spock.annotation.MicronautTest
import spock.lang.Specification
import static com.oracle.bmc.core.model.Instance.LifecycleState.Running
import static com.oracle.bmc.core.model.Instance.LifecycleState.Stopped
import static io.micronaut.http.HttpStatus.OK
@MicronautTest
class MicronautguideControllerSpec extends Specification {
private static final List<Class<?>> SHARED_CLASSES = [MockData, LifecycleState] (1)
void 'test status'() {
given:
String instanceOcid = UUID.randomUUID()
MockData.instanceOcid = instanceOcid (2)
MockData.instanceLifecycleState = Running
when:
HttpResponse<String> response = FnHttpTest.invoke( (3)
HttpRequest.GET("/compute/status/$instanceOcid"),
SHARED_CLASSES)
then:
response.status() == OK
response.body() == '{"lifecycleState":"Running","ocid":"' + instanceOcid + '"}' (4)
}
void 'test start'() {
given:
String instanceOcid = UUID.randomUUID()
MockData.instanceOcid = instanceOcid
MockData.instanceLifecycleState = Stopped
when:
HttpResponse<String> response = FnHttpTest.invoke(
HttpRequest.POST("/compute/start/$instanceOcid", null),
SHARED_CLASSES)
then:
response.status() == OK
response.body() == '{"lifecycleState":"Starting","ocid":"' + instanceOcid + '"}'
}
void 'test stop'() {
given:
String instanceOcid = UUID.randomUUID()
MockData.instanceOcid = instanceOcid
MockData.instanceLifecycleState = Running
when:
HttpResponse<String> response = FnHttpTest.invoke(
HttpRequest.POST("/compute/stop/$instanceOcid", null),
SHARED_CLASSES)
then:
response.status() == OK
response.body() == '{"lifecycleState":"Stopping","ocid":"' + instanceOcid + '"}'
}
void cleanup() {
MockData.reset()
}
}
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.