APEX + DB Operator: Deploying Customer Managed ORDS for ADB with the Oracle Database Operator on OKE

Sydney NurseSydney Nurse
13 min read

When you provision an Autonomous Database instance, by default Oracle REST Data Services (ORDS) is preconfigured and available for the instance. When you use the default ORDS on Autonomous Database, you cannot modify any of the ORDS configuration options.

About Customer Managed Oracle REST Data Services on Autonomous Database

Use a customer managed environment if you want manual control of the configuration and management of Oracle REST Data Services. For example, use this option when your applications require larger connection pools or if you need more control over the ORDS configuration options.

When ORDS runs in a customer managed environment, you are responsible for configuration, patching, and maintenance of ORDS

See Installing and Configuring Customer Managed ORDS on Autonomous Database for more information.

Well, that’s according to the documentation and for most part customers should really test and verify that is required for this stateless component.

💡
When you use the default ORDS on Autonomous Database, the default configuration of the JDBC connection pools have a maximum of 100 connections and the connections for ORDS are preconfigured to use the LOW database service.

Performance or specific ORDS configuration options may require a Customer Managed ORDS deployment.

About this Post

Irrespective of the reason, self-managing resources, installing, patching, scaling, etc. appears to be a bit counter to the cloud philosophy.

During a discussion with a Partner this was a topic of discussion, and though they had experience in deploying APEX and ORDS, improving efficiency through automation is a top mandate in today’s fast release cycles.

After making the recommendation to review the capabilities of the Oracle Database Operator (a.k.a Operator) and OCI Kubernetes Engine (OKE), which includes a controller for ORDS Services. I went through the steps to see if the rubber meets the road, so to speak.

Getting Started - Meet the PreReqs

I used OKE for my cluster environment along with the Operator itself and the container images to rollout this solution.

The Prerequisites should be somewhat clear but here are the highlights:

  1. Provision an OCI Kubernetes Engine (OKE)

  2. Configure OCI Credential, Dynamic Group, & Policies for all tasks and compartments as required. API Key authentication or granted with Instance Principal

  3. Installation of the Oracle Database Operator into the cluster (ReadMe)

  4. Create namespaces to group resources

  5. Ensure that your Oracle Container Registry User, has a Token generated.

  6. Create secret to store Container Registry Credentials (ReadMe)

  7. Provisioned an Autonomous Database (this can also be provisioned with the Operator - example included)

Many of these steps have individual requirements, so follow the documentation for each of those. For more robust or better hardening follow the Best Practices for OKE to start.

💡
The Database Operator is available as an optional cluster add-on, but you can only configure cluster add-ons when using enhanced clusters. See Working with Enhanced Clusters and Basic Clusters.
💡
Note: When using namespaces it is recommended to add these to the Operator WATCH_NAMESPACE. This is a mandatory requirement for the ORDSSRVS controller, if left out creating the ORDSSRVS will end in Error in ConfigMapReconcile (init-script), leaving the POD in a Preparing state.

The above is not a comprehensive list, so review and follow all of the documentation

PreReqs for the examples

I am using the sidb-ns namespace, create you own or use the ones provided in the Operator instructions.

# Create my custom namespace sidb-ns
kubectl create namespace sidb-ns

# Set this as the context default namespace just in case
kubectl config set-context --current --namespace=sidb-ns

Add the namespace to WATCH_NAMESPACE of the Operator

💡
If you installed the Operator as an OKE Add-On, this can only be updated via cli + custom YAML. Unfortunately, if you management nodes are restarted, the Operator will be running in a new pod with the settings set via the OKE Manage Add-Ons page only. WATCH_NAMESPACE is currently not available, so this must be re-applied for each new Operator pod.

Create a YAML file to update/patch the operator configuration

# patch-oracle-database-operator.yaml
# Copyright (c) 2022, 2024, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
#
apiVersion: apps/v1
kind: Deployment
metadata:
  name: oracle-database-operator-controller-manager
  namespace: oracle-database-operator-system
spec:
  replicas: 1
  selector:
    matchLabels:
      control-plane: controller-manager
  template:
    metadata:
      labels:
        control-plane: controller-manager
    spec:
      containers:
      - args:
        name: manager
        env:
        - name: WATCH_NAMESPACE
          value: "default,sidb-ns"
        image: container-registry.oracle.com/database/operator:1.2.0

Apply the configuration changes to the Operator

# Patch the running Operator with the WATCH_NAMESPACE configuration
kubectl apply -f patch-oracle-database-operator.yaml

Create secret to store Container Registry Credentials

Go to the Oracle Container Registry site and review the Alert message

To generate a Token, signin and select AuthToken from the dropdown menu under your username

This will take you to the page to manage or generate a Token

# Save the Secret, so you can donwload container images that require authorization
#Create container registry secret in the namespaces it will be used
kubectl create secret docker-registry ocr-reg-cred \
 --docker-server=container-registry.oracle.com \
 --docker-username='sydney.nurse@oracle.com' \
 --docker-password='ImNotTellingYou'   \
 --docker-email='sydney.nurse@oracle.com' -n sidb-ns

# Don't forget to specify the namespace "-n YourNamespace"

Setup an Autonomous Database

The Operator supports the following operations:

  • Provision an Autonomous Database

  • Bind to an existing Autonomous Database

Being pessimistic, I provisioned a new instance.

Provisioning an Autonomous Database

Follow the steps for the ADB_PREREQUISITES and Provision an Autonomous Database in your compartment

An example .yaml file is available here: config/samples/adb/autonomousdatabase_create.yaml

💡
OCI ADB now supports ECPU and provisioning new OCPU maybe restricted in regions or tenants.

The following table is an excerpt from the ADB (ReadMe) describing the attributes. It takes several reads to understand the correct combination as so much details are provided and the sample uses cpuCoreCount which is no longer recommended.

Spec Attributes

spec.details.cpuCoreCountintThe number of CPU cores to be made available to the database.Note: This parameter cannot be used with the ocpuCount parameter.
spec.details.computeModelstringThe compute model of the Autonomous Database, ECPU compute model is the recommended model and OCPU compute model is legacy

If using cpuCoreCount then it is an error to specify computeModel to a non-null value. | **Note: Providing computeModel and computeCount is the preferred method for both OCPU and ECPU.

Note: This is required if using the computeCount parameter.** | | spec.details.computeCount | int | The compute amount (CPUs) available to the database.

For an Autonomous Database Serverless instance, the 'ECPU' compute model requires a minimum value of one

When using cpuCoreCount parameter, it is an error to specify computeCount to a non-null value. | Note: Providing computeModel and computeCount is the preferred method for both OCPU and ECPU.

Note: Required when using the computeModel parameter. | | spec.details.ocpuCount | float32 | The number of OCPU cores to be made available to the database.

- For Autonomous Database Serverless instances, this parameter is not used. | Note: This parameter cannot be used with the cpuCoreCount parameter. |

In the my YAML sample, I have added the custom namespace, supplied the reference to my K8S secret for the Admin password, and used computeModel and computeCount.

# adb_snodbodb23ai_create.yaml
# Copyright (c) 2022, 2024, Oracle and/or its affiliates.
# Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
#
apiVersion: database.oracle.com/v1alpha1
kind: AutonomousDatabase
metadata:
  name: snodbodb23ai
  namespace: sidb-ns
spec:
  action: Create
  details:
    # Update compartmentOCID with your compartment OCID.
    compartmentId: ocid1.compartment.oc1..sydney.nurse
    # The dbName must begin with an alphabetic character and can contain a maximum of 14 alphanumeric characters. Special characters are not permitted. The database name must be unique in the tenancy.
    dbName: snodbodb23ai
    displayName: snodbodb23ai
    computeModel: ECPU
    computeCount: 4
    adminPassword:
      # Comment out k8sSecret and uncomment ociSecret if you pass the admin password using OCI Secret.
      k8sSecret:
        # The Name of the K8s secret where you want to hold the password of the ADMIN account.
        name: adb-admin-secret
      # ociSecret:
      #   # The OCID of the OCI Secret that holds the password of the ADMIN account. It should start with ocid1.vaultsecret... .
      #   ocid: ocid1.vaultsecret...
    dataStorageSizeInTBs: 1

    # networkAccess:
    #   # Uncomment this block to configure the network access type with the PUBLIC option, which allows secure access from everywhere.
    #   accessType: PUBLIC

    #   # Uncomment this block to configure the network access type with the RESTRICTED option.
    #   # This option lets you restrict access by defining access control rules in an Access Control List (ACL).
    #   # By specifying an ACL, the database will be accessible from a whitelisted set of IP addresses, CIDR (Classless Inter-Domain Routing) blocks, or VCNs.
    #   # Use a semicolon (;) as a deliminator between the VCN-specific subnets or IPs.
    #   accessType: RESTRICTED
    #   accessControlList:
    #   - 1.1.1.1
    #   - 1.1.0.0/16
    #   - ocid1.vcn...
    #   - ocid1.vcn...;1.1.1.1
    #   - ocid1.vcn...;1.1.0.0/16
    #   isMTLSConnectionRequired: true

    #   # Uncomment this block to configure the network access type with the PRIVATE option.
    #   # This option assigns a private endpoint, private IP, and hostname to your database.
    #   # Specifying this option allows traffic only from the VCN you specify.
    #   # This allows you to define security rules, ingress/egress, at the Network Security Group (NSG) level and to control traffic to your Autonomous Database.
    #   accessType: PRIVATE
    #   privateEndpoint:
    #     subnetOCID: ocid1.subnet...
    #     nsgOCIDs:
    #     - ocid1.networksecuritygroup...
    #   isMTLSConnectionRequired: true

    #   # Uncomment this block to configure the network access of an dedicated Autonomous Database (ADB-D) with an access control list.
    #   isAccessControlEnabled: true
    #   accessControlList:
    #   - 1.1.1.1
    #   - 1.1.0.0/16

  # Authorize the operator with API signing key pair. Comment out the ociConfig fields if your nodes are already authorized with instance principal.
  #ociConfig:
  ##  configMapName: oci-cred
    # Comment out secretName if using OKE workload identity
  ##  secretName: oci-privatekey

The instructions will have you create the secrete before running the apply.

# Create a Kubernetes secret to store the database Admin user password
kubectl create secret generic -n sidb-ns adb-admin-secret \
 --from-literal=adb-admin-secret=...uSEaN_ADB_9a55W0rd!

# Provision ADB instance and create a ADB reference that can be used by the operator
kubectl apply -f adb_snodbodb23ai_create.yaml 

# Watch the ADB instance in the namespace
kubectl get adb -n sidb-ns snodbodb23ai -w

NAME           DISPLAY NAME   DB NAME        STATE       DEDICATED   OCPUS   STORAGE (TB)   WORKLOAD TYPE   CREATED
snodbodb23ai   snodbodb23ai   snodbodb23ai   AVAILABLE   false       0       1              OLTP            2025-07-11 08:06:47 UTC

Get the ADB Wallet and Save it as a Secret

The Operator allows you to download the wallet to support client connections. This is needed for the remote or customer managed ORDS pool.

# adb_snodbodb23ai_getWallet.yaml
# Copyright (c) 2022, Oracle and/or its affiliates. 
# Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
#
apiVersion: database.oracle.com/v1alpha1
kind: AutonomousDatabase
metadata:
  name: snodbodb23ai
  namespace: sidb-ns
spec:
  action: Update
  details:
    id: ocid1.autonomousdatabase.oc1.snodbodb23ai
  wallet:
    name: snodbodb23ai-wallet
    password:
      k8sSecret:
        name: snodbodb23ai-wallet-password
  # Authorize the operator with API signing key pair. Comment out the ociConfig fields if your nodes are already authorized with instance principal.
  #ociConfig:
  ##  configMapName: oci-cred
  ##  secretName: oci-privatekey
# Create the secret to store the Wallet
kubectl create secret generic snodbodb23ai-wallet-password \
 --from-literal=snodbodb23ai-wallet-password='Wallet-very53CR3T9a55' \
 -n sidb-ns

# Download and save the ADB wallet in the secret
kubectl apply -f adb_snodbodb23ai_getWallet.yaml

Provisioning ORDS Services instance

The prerequisites finally completed, of course we could have simply ran this in the OCI console but what was the fun in that?!

The next step is to setup another YAML file for the ORDSSRVS by following the example ADB using the OraOperator where it creates the encrypted password.

# Encrypt and Store the DB User Passwords for ORDSSRVS
echo ${DB_PWD} > sidb-db-auth

openssl genpkey -algorithm RSA \
 -pkeyopt rsa_keygen_bits:2048 \
 -pkeyopt rsa_keygen_pubexp:65537 > ca.key

openssl rsa -in ca.key -outform PEM  -pubout -out public.pem

# Save the Certificate as a Secret
kubectl create secret generic prvkey \
 --from-file=privateKey=ca.key \
 -n sidb-ns

openssl rsautl -encrypt -pubin -inkey public.pem \
 -in sidb-db-auth |base64 > e_sidb-db-auth

# Save the Encrypted Password
kubectl create secret generic sidb-db-auth-enc \
 --from-file=password=e_sidb-db-auth -n  sidb-ns

# Remove the evidence
rm sidb-db-auth e_sidb-db-auth

The documentation retrieves the DB service name for the transactional processing with the expectations that these will be fast and short connections. I’ve added replicas for future testing and used a tns connection type.

Enumerated list of supported values can be found in the CRD file database.oracle.com_ordssrvs.yaml. For Production or load testing environments, review all the available attributes for the global caching & security, and pool. The image provides a minimal linux cli tooling, so editing the configuration, identifying processing or using normal cli will be limited.

💡
CDB configurations are also supported in the Pool settings
# adb_snodbodb23ai_create_ordsSrvs.yaml
# Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at http://oss.oracle.com/licenses/upl.
#

apiVersion: database.oracle.com/v4
#kind: OracleRestDataService
kind: OrdsSrvs
metadata:
  name: snordsodbo
  namespace: sidb-ns
spec:
  ## ORDS image details
  image: container-registry.oracle.com/database/ords:25.2.0
  imagePullSecrets: ocr-reg-cred
  forceRestart: true
  replicas: 1
  encPrivKey:
    secretName: prvkey
    passwordKey: privateKey
  globalSettings:
    database.api.enabled: true
  poolSettings:
    - poolName: snodbodb23ai
      restEnabledSql.active: true
      feature.sdw: true
      plsql.gateway.mode: proxied
      jdbc.MaxConnectionReuseCount: 5000
      jdbc.MaxConnectionReuseTime: 900
      jdbc.SecondsToTrustIdleConnection: 1
      jdbc.InitialLimit: 100
      jdbc.MaxLimit: 100
      db.connectionType: tns
      db.tnsAliasName: snodbodb23ai_TP
      tnsAdminSecret:
        secretName: snodbodb23ai-wallet
      db.username: ORDS_PUBLIC_USER_OPER
      db.secret:
        secretName:  sidb-db-auth-enc
      db.adminUser: ADMIN
      db.adminUser.secret:
        secretName:  sidb-db-auth-enc
# Provision the Customer Managed ORDS instance
kubectl apply -f adb_snodbodb23ai_create_ordsSrvs.yaml

# Check the State 
kubectl get ordssrvs -n sidb-ns snordsodbo

NAME         STATUS    WORKLOADTYPE   ORDSVERSION   HTTPPORT   HTTPSPORT   MONGOPORT   RESTARTREQUIRED   AGE    ORDSINSTALLED
snordsodbo   Healthy   Deployment     25.2.0        8080       8443                    false             2d1h   true

# Status should updte from Preparing and eventually Healthy. 
## If it stays in Preparing, most likely you will also not be listed as a POD

### 0.0 Oh No!!! Time ⏰ to Troubleshot!

# List the PODS
kubectl get pods -n sidb-ns

NAME                          READY   STATUS    RESTARTS       AGE
snordsodbo-858b4576df-cd2sp   1/1     Running   0              2d1h

# Check the Operator POD logs
kubectl logs -f -n oracle-database-operator-system \
 $(kubectl get pod -o name -n oracle-database-operator-system|tail -1)

# Check the POD init script logs
## kubectl logs -n <NAMESPACE> <ORDSSRVSPOD> -c <ordssrvs_name>-init
kubectl logs -n sidb-ns pod/snordsodbo-74bb4b7558-bm2ll -c snordsodbo-init

If things did work out then you can port forward the HTTPs port and test from localhost

Don’t be like me and forget to include the pool name in the url. If you do you will see this instead.

APEX works as well

Tips

Error in ConfigMapReconcile

2025-07-15T14:43:19Z    INFO    SetStatus    Deployment not ready    {"controller": "ordssrvs", "controllerGroup": "database.oracle.com", "controllerKind": "OrdsSrvs", "OrdsSrvs": {"name":"snordsodbo","namespace":"sidb-ns"}, "namespace": "sidb-ns", "name": "snordsodbo", "reconcileID": "a174c231-71f6-4bcf-a620-7cf220b6713d"}
2025-07-15T14:43:19Z    ERROR    Error in ConfigMapReconcile (init-script)    {"controller": "ordssrvs", "controllerGroup": "database.oracle.com", "controllerKind": "OrdsSrvs", "OrdsSrvs": {"name":"snordsodbo","namespace":"sidb-ns"}, "namespace": "sidb-ns", "name": "snordsodbo", "reconcileID": "a174c231-71f6-4bcf-a620-7cf220b6713d", "error": "expected pointer, but got nil"}
github.com/oracle/oracle-database-operator/controllers/database.(*OrdsSrvsReconciler).Reconcile
    /workspace/controllers/database/ordssrvs_controller.go:163

If this is found in the Operator log, run the delete for the create YAML file, check and/or update the Operator WATCH_NAMESPACE as described above.

# Remove the Customer Managed ORDS instance registration
kubectl delete -f adb_snodbodb23ai_create_ordsSrvs.yaml

# Patch the running Operator with the WATCH_NAMESPACE configuration
kubectl apply -f patch-oracle-database-operator.yaml

# Provision the Customer Managed ORDS instance
kubectl apply -f adb_snodbodb23ai_create_ordsSrvs.yaml

# Check the State 
kubectl get ordssrvs -n sidb-ns snordsodbo

# List the PODS
kubectl get pods -n sidb-ns

# Check the Operator POD logs
kubectl logs -f -n oracle-database-operator-system \
 $(kubectl get pod -o name -n oracle-database-operator-system|tail -1)

POD stat in Init:Error

# POD stat in Init:Error
kubectl get all -n sidb-ns

NAME                              READY   STATUS       RESTARTS      AGE
pod/snordsodbo-74bb4b7558-bm2ll   0/1     Init:Error   1 (16s ago)   33s

# Check the POD init script logs
## kubectl logs -n <NAMESPACE> <ORDSSRVSPOD> -c <ordssrvs_name>-init
kubectl logs -n sidb-ns pod/snordsodbo-74bb4b7558-bm2ll -c snordsodbo-init

Check if there are any issues listed, connectivity, access denied or invalid login, etc.

Conclusion

The operator is appealing for repeatable deployment scenarios and for some complex tasks. Patching and redeployment will be simplified, though I have yet to test this use case. It is on my list along with leveraging replicas and load balancer with OKE.

All of the Operator code and resources are available, OpenSource on GitHub and open for contributions.

From my side and organisation we will be testing the Operator’s usefulness in Database DevOps and CICD Pipelines for the Oracle Database with APEX and ORDS.

Are you deploying Oracle components in K8s or considering the Database Operator?

Let me know and thanks for the feedback

0
Subscribe to my newsletter

Read articles from Sydney Nurse directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sydney Nurse
Sydney Nurse

I work with software but it does not define me and my constant is change and I live a life of evolution. Learning, adapting, forgetting, re-learning, repeating I am not a Developer, I simply use software tools to solve interesting challenges and implement different use cases.