Thanks to Matt Vander Vliet for reviewing and suggesting improvements.
In this post we will create a simple backend node.js application on Oracle Container Engine for Kubernetes(OKE) which connects to OCI Autonomous transaction processing(ATP) instance created via OCI service broker.
We will use oracledb node.js package and oracle instant client for connecting to ATP. We will also create a docker container for DB app built using node.js and store the container image on Oracle Container Registry(OCIR). And finally, expose the deployment (by creating a load balancer service) and test the functionality.
This procedure does not demonstrate a complete list of cloud native services offered on OCI . Would recommend reading MuShop demo which has more advanced use cases of cloud native services on OCI.
Most steps discussed here are manual. The idea is to understand the steps in detail. The node.js app that we create uses the in-built libraries to listen and serve the http requests. This document tries to explore on how different OCI cloud native services fit together in creating application.
We will not be going through the installation of service catalog and service broker in this document to avoid duplication, the installation instructions are provided here Once you follow the above guide and install, verify the service broker status is ready.
$ kubectl get clusterservicebrokers NAME URL STATUS AGE oci-service-broker http://oci-service-broker.default:8080 Ready 8dThe clusterservicebroker holds the api endpoint information of your service broker. This is a resource within service catalog.
# # Copyright (c) 2019, 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: v1 kind: Secret metadata: name: atp-secret data: # {"password":"s123456789S@"} password: eyJwYXNzd29yZCI6InMxMjM0NTY3ODlTQCJ9 # {"walletPassword":"Welcome_123"} walletPassword: eyJ3YWxsZXRQYXNzd29yZCI6IldlbGNvbWVfMTIzIn0KCreate a file named atp-instance.yaml with the following details:
# # Copyright (c) 2019, 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: servicecatalog.k8s.io/v1beta1 kind: ServiceInstance metadata: name: demodb spec: clusterServiceClassExternalName: atp-service clusterServicePlanExternalName: standard parameters: name: demodb compartmentId: "ocid1.compartment.oc1..xxxx" dbName: demodb cpuCount: 1 storageSizeTBs: 1 licenseType: NEW autoScaling: false freeFormTags: testtag: demodb parametersFrom: - secretKeyRef: name: atp-secret key: passwordCreate a file named atp-binding.yaml with the following details:
# # Copyright (c) 2019, 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: servicecatalog.k8s.io/v1beta1 kind: ServiceBinding metadata: name: atp-demo-binding spec: instanceRef: name: demodb parametersFrom: - secretKeyRef: name: atp-secret key: walletPasswordApply the config:
$ kubectl apply -f atp-secret.yaml $ kubectl apply -f atp-instance.yaml $ kubectl apply -f atp-binding.yamlEnsure that service instance is in Ready state:
192-168-1-8:db-app vsnaik$ kubectl get serviceinstance NAME CLASS PLAN STATUS AGE demodb ClusterServiceClass/atp-service standard Ready 2d5hatp-demo-binding secret automatically gets created which has the wallet details (tnsnames.ora, cwallet.sso etc.) of ATP database we created. we are now ready to use it in our application.
$ kubectl get secrets NAME TYPE DATA AGE atp-demo-binding Opaque 9 37h atp-demo-credentials Opaque 4 42h atp-secret Opaque 2 2d14h To get more details, perform the below: $ kubectl get secrets atp-demo-binding -o yaml
apiVersion: v1 kind: Pod metadata: name: test-pod labels: app: test spec: containers: - name: test-pod image: oraclelinux:7-slim command: ["/bin/sh","-c"] args: - sleep 10000s volumeMounts: - name: wallet-raw mountPath: /tmp/wallet readOnly: false - name: atp-secret mountPath: /tmp/wallet2 readOnly: false volumes: - name: wallet-raw secret: secretName: atp-demo-binding - name: atp-secret secret: secretName: atp-secret
$ kubectl apply -f test-db.yamlLogin to test-pod and bootstrap the DB manually:
$ kubectl exec -it test-pod -- bash # Decode wallet bash# mkdir -p /wallet bash# for i in `ls -1 /tmp/wallet | grep -v user_name`; do cat /tmp/wallet/$i | base64 --decode > /wallet/$i; done; ls -l /wallet/* # Install oracle-instantclient bash# yum -y upgrade bash# yum -y update bash# yum -y install oracle-release-el7 bash# yum-config-manager --enable ol7_oracle_instantclient bash# yum -y install oracle-instantclient19.5-sqlplus bash# yum -y install oracle-instantclient19.5-tools bash# yum install -y jq # copy the wallet files to sql client path bash# mkdir -p /usr/lib/oracle/19.5/client64/lib/network/admin/ bash# cp /wallet/* /usr/lib/oracle/19.5/client64/lib/network/admin/ bash# export PATH=/usr/lib/oracle/19.5/client64/bin/:$PATH bash# ATP_PW=`cat /tmp/wallet2/password | jq -r ".password"` bash# ATP_UNAME=`cat /tmp/wallet/user_name` bash# sqlplus ${ATP_UNAME}/\"${ATP_PW}\"@demodb_tp SQL*Plus: Release 19.0.0.0.0 - Production on Wed May 13 01:02:00 2020 Version 19.5.0.0.0 Copyright (c) 1982, 2019, Oracle. All rights reserved. Last Successful login time: Wed May 13 2020 01:01:21 +00:00 Connected to: Oracle Database 18c Enterprise Edition Release 18.0.0.0.0 - Production Version 18.4.0.0.0 SQL>Once you have successfully connected to DB, lets go ahead and create a user schema and tables required:
CREATE USER test_user IDENTIFIED BY default_Password1; GRANT CREATE SESSION TO test_user; GRANT UNLIMITED TABLESPACE TO test_user; CREATE ROLE test_role; GRANT test_role TO test_user; CREATE TABLE test_user.products ( brand VARCHAR2(20), title VARCHAR2(40), description VARCHAR2(500) ); GRANT SELECT, INSERT, UPDATE, DELETE ON test_user.products TO test_role; INSERT INTO test_user.products VALUES ('Nike','Tech Fleece Joggers','Jogger pants for men'); INSERT INTO test_user.products VALUES ('Apple','iwatch','Apple series 5 iwatch'); INSERT INTO test_user.products VALUES ('Sony','Bravia','Sony Bravia Television'); SQL> select brand,title from test_user.products; BRAND TITLE -------------------- ---------------------------------------- Nike Tech Fleece Joggers Apple iwatch Sony Bravia SQL> exit To exit out of the pod: bash# exitCleanup: Delete the bootstrap pod.
$ kubectl delete pod test-pod
module.exports = { user : process.env.OADB_USER , password : process.env.OADB_PW, connectString : process.env.OADB_SERVICE, externalAuth : process.env.NODE_OADB_EXTERNALAUTH ? true : false };Create a file named db.js and add the following details:
const http = require('http') const oracledb = require('oracledb'); const dbconfig = require('./dbconfig.js'); let connection; const server = http.createServer(async (req, res) => { //console.log(req.url) res.setHeader('Content-Type','text/html'); if (req.url === '/home'){ res.write("In home page, Welcome...") res.end() } if (req.url === '/products') { console.log("In products page...") try { connection = await oracledb.getConnection(dbconfig); const result = await connection.execute( `SELECT brand, title, description FROM products` ); //console.log(result.rows); res.write('The results fetched from DB are :' + ' '); for(let results in result.rows){ const [brand,title,description] = result.rows[results]; console.log(brand,title,description); res.write('Brand: '+ brand + ' Title: '+ title +' Description: '+ description); res.write(' ') } res.end() } catch (err) { console.error(err); } finally { if (connection) { try { await connection.close(); } catch (err) { console.error(err); } } } } }); server.listen(process.env.PORT); function terminate(){ console.log("\nTerminating"); if (connection) { try { connection.close(); } catch (err) { console.error(err); } } process.exit(0); } process .on('SIGTERM',terminate) .on('SIGINT', terminate)Create a file named package.json and add the following details:
{ "name": "db-app", "version": "1.1.0", "description": "A Simple DB application", "main": "db.js", "dependencies": { "oracledb": "^4.2.0" } }Create a Dockerfile, In which we are trying to create a container with oracle-instantclient19.5, node.js and all its dependencies. Application will be listening on 8080.
FROM oraclelinux:7-slim as db-app RUN yum -y upgrade && \ yum -y update && \ yum -y install oracle-release-el7 && \ yum-config-manager --enable ol7_oracle_instantclient && \ yum -y install oracle-instantclient19.5-sqlplus && \ yum -y install oracle-instantclient19.5-tools && \ yum -y install oracle-nodejs-release-el7 oracle-release-el7 && \ yum install -y nodejs RUN mkdir -p /home/node/app WORKDIR /home/node/app COPY * ./ RUN npm install ENV NODE_ENV "production" ENV PORT 8080 ENV ORACLE_HOME /usr/lib/oracle/19.5/ RUN export PATH=$ORACLE_HOME:$PATH RUN export PATH=/usr/lib/oracle/19.5/client64/bin/:$PATH ENV LD_LIBRARY_PATH /usr/lib/oracle/19.5/ EXPOSE 8080 CMD [ "node", "db.js" ]
$ docker build -t syd.ocir.io/your_tenancy_namespace/db-app:latest .To push an image to OCIR, you need to login to the registry(using your user id and auth token), steps are mentioned here
$ docker login syd.ocir.io $ docker push syd.ocir.io/your_tenancy_namespace/db-app:latest
$ kubectl create secret generic atp-demo-credentials --from-literal=oadb_service=demodb_tp --from-literal=oadb_user='test_user' --from-literal=oadb_pw='default_Password1'To pull an image from OCIR registry, we also need to create docker-registry secret which will be used in our deployment spec:
$ kubectl create secret docker-registry secret-name --docker-server=region-key.ocir.io --docker-username='tenancy-namespace/oci-username' --docker-password='oci-auth-token' --docker-email='email-address
apiVersion: apps/v1 kind: Deployment metadata: name: db-app spec: replicas: 1 selector: matchLabels: name: db-app template: metadata: labels: name: db-app spec: initContainers: - name: decode-wallet image: oraclelinux:7-slim command: ["/bin/sh","-c"] args: - for i in `ls -1 /tmp/wallet | grep -v user_name`; do cat /tmp/wallet/$i | base64 --decode > /wallet/$i; done; ls -l /wallet/*;sleep 10s; volumeMounts: - name: wallet-raw mountPath: /tmp/wallet - name: wallet mountPath: /wallet containers: - name: db-app image: syd.ocir.io/ociateam/db-app:latest imagePullPolicy: Always env: - name: OADB_USER valueFrom: secretKeyRef: name: atp-demo-credentials key: oadb_user - name: OADB_PW valueFrom: secretKeyRef: name: atp-demo-credentials key: oadb_pw - name: OADB_SERVICE valueFrom: secretKeyRef: name: atp-demo-credentials key: oadb_service volumeMounts: - name: wallet mountPath: /usr/lib/oracle/19.5/client64/lib/network/admin/ volumes: - name: wallet-raw secret: secretName: atp-demo-binding - name: wallet emptyDir: {} imagePullSecrets: - name: ocirsecretApply the configuration:
$ kubectl apply -f db-app.yamlAs you can see we are automating the wallet decoding with the help of "initContainers". This is necessary to overcome the double encoding bug with service broker.
kubectl expose deployment db-app --port=80 --target-port=8080 --type=LoadBalancerNote: A new load Balancer with public IP will get created on OCI with --type=LoadBalancer. You can also use the --type as ClusterIP to avoid external access.
$ kubectl delete svcNote: This deletes your load balancer created on OCI
$ kubectl delete deployment db-app
$ kubectl delete servicebinding atp-demo-binding Note: The above deletes your atp-demo-binding secrets as well. $ kubectl delete serviceinstance atp-instance Note: This deletes the ATP database on OCI $ kubectl delete secret atp-secret $ kubectl delete secret atp-demo-credentials $ kubectl delete secret ocirsecretNote: This document does not cover clean up of service broker deployed during the start of this procedure, refer https://github.com/oracle/oci-service-broker for additional information.
Principal Cloud Solutions Architect