Deploy a Micronaut Microservices Application to Oracle Cloud Infrastructure Container Engine for Kubernetes

This guide shows how to deploy a Micronaut microservices application to the Oracle Cloud Infrastructure Container Engine for Kubernetes (OKE) using the Micronaut Kubernetes module.

OKE is a managed Kubernetes service for deploying containerized applications to the cloud.

The guide shows how to use Kubernetes Service Discovery and Distributed Configuration to connect several microservices, and discover how Micronaut integration with Kubernetes simplifies deployment to OKE.

Prerequisites #

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GCN guides are compatible with Gradle only. Maven support is coming soon.

1. Create or Download a Microservices Application #

You can create a microservices application from scratch by following this guide, or you can download the completed example in Java:

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.

The application consists of three microservices:

  • users - contains customer data that can place orders on items, also a new customer can be created. It requires HTTP basic authentication to access it.
  • orders - contains all orders that customers have created as well as available items that customers can order. This microservice also enables the creation of new orders. It requires HTTP basic authentication to access it.
  • api - acts as a gateway to the orders and users microservices. It combines results from both microservices and checks data when a customer creates a new order.

Note: By default, a Micronaut application detects its runtime environment. A detected environment (in this case, k8s) overrides the default specified environment (in this case, oraclecloud). This means that you should locate your configuration in the application-k8s.properties and bootstrap-k8s.properties files. Alternatively, you can specify the oraclecloud environment passing it as a command-line option (-Dmicronaut.environments=oraclecloud) or via an environment variable (MICRONAUT_ENVIRONMENTS=oraclecloud).

2. Create an Oracle Cloud Infrastructure Kubernetes Cluster #

You will use Quick Create cluster option in OKE to create Kubernetes cluster.

Create a Kubernetes cluster with default settings using the Quick Create workflow:

  1. Login to the Oracle Cloud console, and browse Quick Kubernetes Clusters (OKE).
  2. Choose your compartment in the Compartment dropdown.
  3. Under Containers, click Kubernetes Clusters (OKE).
  4. Then click Create Cluster and choose the Quick Create option.
  5. Enter the name for your cluster, for example, gcn-k8s. Select Public Endpoint for the Kubernetes API endpoint; Private Workers for worker nodes; and the shape (choose the default). Then confirm cluster creation by clicking Next.
  6. It may take a few minutes to create all the resources. When they are all complete, click Close.
  7. From the Cluster Details screen click Copy to copy the Cluster Id (you will need it later).

3. Prepare to Deploy Microservices #

3.1. Export Environment Variables #

Define some environment variables to make deploying process easier:

  • OCI_USER_ID to store your user OCID, which you can find in your Oracle Cloud Infrastructure configuration file.
  • OCI_TENANCY_NAMESPACE to store the tenancy namespace (retrieve your tenancy namespace using the Oracle Cloud Infrastructure CLI).
  • OCIR_USERNAME store your username in the format <tenancy_namespace>/<username>. You can reuse OCI_TENANCY_NAMESPACE and only edit the <username> part.
  • OCI_REGION to store your cloud region code, for example, “us-phoenix-1”.
  • OCI_CLUSTER_ID to store the Cluster Id that you copied earlier.
  • OCI_COMPARTMENT_ID to store your compartment id.

For example:

export OCI_USER_ID="ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
export OCI_TENANCY_NAMESPACE=$(oci os ns get | jq .data -r)
export OCIR_USERNAME="$OCI_TENANCY_NAMESPACE/<username>"
export OCI_REGION="<region-key>"
export OCI_CLUSTER_ID="ocid1.cluster.oc1.iad.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
The export command syntax varies per platform. Expand to learn more.

If you use Linux/macOS, the export command is:

export OCI_USER_ID=<id>

If you use Windows, change export to set if using the cmd prompt:

set OCI_USER_ID=<id>

If you use PowerShell, change export to $ and use quotes around the value, for example:

$OCI_USER_ID="<id>"

To dereference a value in Linux/macOS or Powershell, use $, for example: /some/command -option=$OCI_USER_ID

If you use cmd, use % before and after the name, for example: /some/command -option=%OCI_USER_ID%

3.2. Authenticate to Oracle Cloud Infrastructure Registry #

  1. Create an AUTH_TOKEN to authenticate to Oracle Cloud Infrastructure Registry (also known as Container Registry). (Oracle Cloud Infrastructure allows you to have only two authentication tokens at the same time. If you already have two, use the existing one or delete the one that you are not using.)

  2. Export the AUTH_TOKEN variable:
     export AUTH_TOKEN=$(oci iam auth-token create --user-id $OCI_USER_ID --description k8s-gcn | jq -r '.data.token')
    
  3. Run the next command to log in to ocir.io (Container Registry):
     docker login $OCI_REGION.ocir.io -u $OCIR_USERNAME -p $AUTH_TOKEN
    

3.3. Build a Container Image of the Native Users Microservice #

To build a container image of the native users microservice named “users”, run the following command from the users/ directory:

./gradlew dockerBuildNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems building a container image, try the following:

  • Gradle: from the users/build/docker/native-main/ directory, run docker build . -t users-oci -f DockerfileNative.
  • Maven: from the users/target/ directory, run docker build . -t users-oci -f Dockerfile.

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

3.4. Update the Users Microservice #

Edit the file named users/k8s.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gcn-k8s
  name: "users"
spec:
  selector:
    matchLabels:
      app: "users"
  template:
    metadata:
      labels:
        app: "users"
    spec:
      serviceAccountName: gcn-service
      containers:
        - name: "users"
          image: <region-key>.ocir.io/<tenancy-namespace>/gcn-k8s/users-oci:latest # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
      imagePullSecrets:
        - name: ocirsecret # <3>
---
apiVersion: v1
kind: Service
metadata:
  namespace: gcn-k8s
  name: "users"
spec:
  selector:
    app: "users"
  type: NodePort
  ports:
    - protocol: "TCP"
      port: 8080

1 The container image name that exists in Container Registry. Change the <region-key> to your region and change the <tenancy-name> to your tenancy name.

2 Change imagePullPolicy to Always.

3 A secret needed to pull container images from Container Registry.

3.5. Create a Container Repository for the Users Microservice #

Your next step is to prepare a container repository in Container Registry.

  1. Create a Container Repository in your compartment:

     export USERS_REPOSITORY=$(oci artifacts container repository create --display-name gcn-k8s/users-oci --compartment-id $OCI_COMPARTMENT_ID | jq .data.id -r)
    
  2. Tag an existing users microservice container image:

     docker tag users-oci:latest $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/users-oci:latest
    
  3. Push the tagged users microservice container image to the remote repository:

     docker push $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/users-oci:latest
    

3.6. Build a Container Image of the Native Orders Microservice #

To build a container image of the native orders microservice named “orders”, run the following command from the orders/ directory:

./gradlew dockerBuildNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems building a container image, try the following:

  • Gradle: from the orders/build/docker/native-main/ directory, run docker build . -t orders-oci -f DockerfileNative.
  • Maven: from the orders/target/ directory, run docker build . -t orders-oci -f Dockerfile.

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

3.7. Update the Orders Microservice #

Edit the file named orders/k8s.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gcn-k8s
  name: "orders"
spec:
  selector:
    matchLabels:
      app: "orders"
  template:
    metadata:
      labels:
        app: "orders"
    spec:
      serviceAccountName: gcn-service
      containers:
        - name: "orders"
          image: <region-key>.ocir.io/<tenancy-namespace>/gcn-k8s/orders-oci:latest # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
      imagePullSecrets:
        - name: ocirsecret # <3>
---
apiVersion: v1
kind: Service
metadata:
  namespace: gcn-k8s
  name: "orders"
spec:
  selector:
    app: "orders"
  type: NodePort
  ports:
    - protocol: "TCP"
      port: 8080

1 The container image name that exists in Container Registry. Change the <region-key> to your region and change the <tenancy-name> to your tenancy name.

2 Change imagePullPolicy to Always.

3 A secret needed to pull container images from Container Registry.

3.8. Create a Container Repository for the Orders Microservice #

  1. Create a container repository in your compartment.

     export ORDERS_REPOSITORY=$(oci artifacts container repository create --display-name gcn-k8s/orders-oci --compartment-id $OCI_COMPARTMENT_ID | jq .data.id -r)
    
  2. Tag an existing orders microservice container image:

     docker tag orders-oci:latest $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/orders-oci:latest
    
  3. Push the tagged orders microservice container image to the remote repository:

     docker push $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/orders-oci:latest
    

3.9. Build a Container Image of the Native API (Gateway) Microservice #

To build a container image of the native api microservice named “api”, run the following command from the api/ directory:

./gradlew dockerBuildNative
./mvnw package -Dpackaging=docker-native -Pgraalvm

Note: If you encounter problems building a container image, try the following:

  • Gradle: from the api/build/docker/native-main/ directory, run docker build . -t api-oci -f DockerfileNative.
  • Maven: from the api/target/ directory, run docker build . -t api-oci -f Dockerfile.

Note: Ensure that you construct container images for the correct CPU architecture. For instance, if you are using AArch64, modify the DOCKER_DEFAULT_PLATFORM environment variable to the value linux/amd64. Alternatively, you have the option to use AArch64 instances within your Kubernetes cluster.

3.10. Update the API Microservice #

Edit the file named api/k8s.yml as follows:

apiVersion: apps/v1
kind: Deployment
metadata:
  namespace: gcn-k8s
  name: "api"
spec:
  selector:
    matchLabels:
      app: "api"
  template:
    metadata:
      labels:
        app: "api"
    spec:
      serviceAccountName: gcn-service
      containers:
        - name: "api"
          image: <region-key>.ocir.io/<tenancy-namespace>/gcn-k8s/api-oci:latest # <1>
          imagePullPolicy: Always # <2>
          ports:
            - name: http
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /health/readiness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
          livenessProbe:
            httpGet:
              path: /health/liveness
              port: 8080
            initialDelaySeconds: 5
            timeoutSeconds: 3
            failureThreshold: 10
      imagePullSecrets:
        - name: ocirsecret # <3>
---
apiVersion: v1
kind: Service
metadata:
  namespace: gcn-k8s
  name: "api"
  annotations: # <4>
    oci.oraclecloud.com/load-balancer-type: "lb"
    service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "10"
spec:
  selector:
    app: "api"
  type: LoadBalancer
  ports:
    - protocol: "TCP"
      port: 8080

1 The container image name that exists in Container Registry. Change the <region-key> to your region and change the <tenancy-name> to your tenancy name.

2 Change imagePullPolicy to Always.

3 A secret needed to pull container images from Container Registry.

4 Metadata annotations for an Oracle Cloud Infrastructure Load Balancer.

3.11. Create a Container Repository for the API Microservice #

  1. Create a container repository in your compartment:

     export API_REPOSITORY=$(oci artifacts container repository create --display-name gcn-k8s/api-oci --compartment-id $OCI_COMPARTMENT_ID | jq .data.id -r)
    
  2. Tag an existing api microservice container image:

     docker tag api-oci:latest $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/api-oci:latest
    
  3. Push the tagged api microservice container image to the remote repository:

     docker push $OCI_REGION.ocir.io/$OCI_TENANCY_NAMESPACE/gcn-k8s/api-oci:latest
    

4. Deploy Microservices to OKE #

  1. Create a directory for a kubectl configuration:

     mkdir -p $HOME/.kube
    
  2. Generate a kubectl configuration for authentication to OKE:

     oci ce cluster create-kubeconfig --cluster-id $OCI_CLUSTER_ID --file $HOME/.kube/config --region $OCI_REGION --token-version 2.0.0  --kube-endpoint PUBLIC_ENDPOINT
    
  3. Set KUBECONFIG to the created config file, as shown below. (This variable is consumed by kubectl.)

     export KUBECONFIG=$HOME/.kube/config
    

    Note: On Windows, kubectl is typically installed within the Kubernetes extension and is not added to the system path. Windows users should configure a proxy in .kube/config to successfully deploy a project to Oracle Cloud Infrastructure: add a proxy-url property under cluster.

     clusters:
     - name: "dev"
       cluster:
         proxy-url: http://user:password@proxy:port
         ...
    
  4. Deploy the auth.yml file that you created in the Deploy a Micronaut Microservices Application to a Local Kubernetes Cluster guide:

     kubectl apply -f auth.yml
    
  5. Create an ocirsecret secret that will be used for authentication to Container Registry. The secret is a object to store user credential data (encrypted data), for example, the database username and password. In this case, OKE uses it to authenticate to Container Registry to be able to pull microservices container images.

     kubectl create secret docker-registry ocirsecret \
           --docker-server=$OCI_REGION.ocir.io \
           --docker-username=$OCIR_USERNAME \
           --docker-password=$AUTH_TOKEN \
           --namespace=gcn-k8s
    
  6. Deploy the users microservice:

     kubectl apply -f users/k8s.yml
    
  7. Deploy the orders microservice:

     kubectl apply -f orders/k8s.yml
    
  8. Run the next command to deploy the api microservice:

     kubectl apply -f api/k8s.yml
    

5. Test Integration Between the Microservices Deployed to OKE #

  1. Run the following command to check the status of the pods and make sure that all of them have the status “Running”:
     kubectl get pods -n=gcn-k8s
    
     NAME                      READY   STATUS    RESTARTS   AGE
     api-6fb4cd949f-kxxx8      1/1     Running   0          2d1h
     orders-595887ddd6-6lzp4   1/1     Running   0          2d1h
     users-df6f78cd7-lgnzx     1/1     Running   0          2d1h
     
  2. Run this command to check the status of the microservices:

     kubectl get services -n=gcn-k8s
    
     NAME         TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)             AGE
     api          LoadBalancer   10.96.70.48    129.159.92.209   8080:31690/TCP      2d1h
     orders       NodePort       10.96.94.130   <none>           8080:31245/TCP      2d1h
     users        NodePort       10.96.34.174   <none>           8080:30790/TCP      2d1h
     

    If EXTERNAL-IP is in a <pending> state, wait a couple of seconds and then run the command again.

  3. Retrieve the URL of the api microservice and set it as the value of the $API_URL environment variable:

     export API_URL=http://$(kubectl get svc api -n=gcn-k8s -o json | jq -r .status.loadBalancer.ingress[0].ip):8080
    
  4. Run a curl command to create a new user via the api microservice:

     curl -X "POST" "$API_URL/api/users" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "first_name": "Nemanja", "last_name": "Mikic", "username": "nmikic" }'
    

    Your output should look like:

     {
       "id":1,
       "username":"nmikic",
       "first_name":"Nemanja",
       "last_name":"Mikic"
     }
    
  5. Run a curl command to create a new order via the api microservice:

     curl -X "POST" "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "user_id": 1, "item_ids": [1,2] }'
    

    Your output should include details of the order, as follows:

     {
       "id": 1,
       "user": {
         "first_name": "Nemanja",
         "last_name": "Mikic",
         "id": 1,
         "username": "nmikic"
       },
       "items": [
         {
           "id": 1,
           "name": "Banana",
           "price": 1.5
         },
         {
           "id": 2,
           "name": "Kiwi",
           "price": 2.5
         }
       ],
       "total": 4.0
     }
    
  6. Run a curl command to list the orders:

     curl "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8'
    

    You should see output that is similar to the following:

     [
       {
         "id": 1,
         "user": {
           "first_name": "Nemanja",
           "last_name": "Mikic",
           "id": 1,
           "username": "nmikic"
         },
         "items": [
           {
             "id": 1,
             "name": "Banana",
             "price": 1.5
           },
           {
             "id": 2,
             "name": "Kiwi",
             "price": 2.5
           }
         ],
         "total": 4.0
       }
     ]
    
  7. Try to place an order for a user who does not exist (with id 100). Run a curl command:

     curl -X "POST" "$API_URL/api/orders" \
          -H 'Content-Type: application/json; charset=utf-8' \
          -d '{ "user_id": 100, "item_ids": [1,2] }'
    

    You should see the following error message:

     {
       "message": "Bad Request",
       "_links": {
         "self": [
           {
             "href": "/api/orders",
             "templated": false
           }
         ]
       },
       "_embedded": {
         "errors": [
           {
             "message": "User with id 100 doesn't exist"
           }
         ]
       }
     }
    

6. Delete Kubernetes Resources #

To delete all Kubernetes resources that were created in this guide, run this command:

kubectl delete namespaces gcn-k8s

Delete the OKE cluster.

oci ce cluster delete --cluster-id $OCI_CLUSTER_ID --force

Delete the gcn-k8s/users-oci artifacts container repository:

oci artifacts container repository delete --repository-id $USERS_REPOSITORY --force

Delete the gcn-k8s/order-oci artifacts container repository:

oci artifacts container repository delete --repository-id $ORDERS_REPOSITORY --force

Delete the gcn-k8s/api-oci artifacts container repository:

oci artifacts container repository delete --repository-id $API_REPOSITORY --force

Summary #

This guide demonstrated how to use Kubernetes Service Discovery and Distributed Configuration, provided with the Micronaut Kubernetes integration, to connect three microservices, and to deploy these microservices to a Kubernetes cluster in Oracle Cloud Infrastructure Container Engine for Kubernetes.