Monday 13 September 2021

Dynamic OData from “any” API through SAP BTP Kyma

SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Guides, SAP HANA Career

Greetings! Today we will be bridging 3rd-party and SAP applications easily with the help of SAP BTP Kyma runtime. Let’s go! ๐Ÿƒ

Content

Sometimes you may have the need to provide an OData v4 or v2 service towards SAP S/4HANA, SAP ECC or any other system. In my case I needed to connect to SAP Analytics Cloud to provide reporting data.

However, the data source provided only regular REST APIs. To make it worse, the underlying data model was configurable (NoSQL DB). Therefore my OData output should be able to adapt accordingly – ideally without development work.

SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Guides, SAP HANA Career
Picture 1: High level problem statement

Solution approach


I knew that maybe the fastest way to get compliant OData as output towards SAP applications is by developing with the SAP CAP model. But I also knew, that I will be much faster to get data from that 3rd-party REST API if I could develop the extraction and formatting part in Python.

With SAP BTP Kyma I was able to quickly develop a working setup: A Python application that would read the data from 3rd-party, and a node.js CAP application that would publish the needed OData service. Since the call timings are asynchronous (e.g. SAP SAC would call the OData service every hour) I decided to use SQLite3 as the binding element between the two applications.

SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Guides, SAP HANA Career
Solution architecture: Containerized applications with shared volume.

Benefits and potential drawbacks


Before getting to the technical details some considerations. Benefits of this setup are:

◉ Complete flexibility towards 3rd-party application’s API through language of choice (in my case: Python).

◉ The Container 1 application manages access and data handling. After its first run it generates a data-model.cds file that describes the OData output structure at the time it ran. A restart of the pod will lead to an automatic redetermination of the “by-then-valid” structure.

◉ Container 2 is just a generated node.js app. There was (almost) no development work that went into it.

◉ SQLite3 is perfectly supported through SAP CAP CDS aside of SAP HANA. It is also very well supported in Python.

◉ SQLite3 can easily handle multi-GB size databases with concurrent read access.

◉ Connection from SAP CAP to the outside world is secured through SAP Kyma with OAuth2 being used to authenticate from SAP SAC.

Potential drawbacks/ things to check beforehand:

◉ SQLite3 is not built for concurrent write access. You can open multiple connections for write to the database but access is granted only on the complete database (exclusive lock). Since a write cycle is in the milliseconds, this might not be an issue but please check that for your setup.

◉ Nothing is said about securing the container 1 API communication. Kyma provides the same security with OAuth2 if you set up the OAuth2 client, though. In my case the 3rd party app is part of the same Kyma namespace, so I didn’t care about security since the API is not exposed to the internet anyway.

Finally let’s have a look into how to define such a pod on Kyma!

The deployment-yaml explained


I just focus on the yaml since the application itself is specific to my needs. Feel free to ask if you want more details!

# This deploys the data retrieval as well as the publisher part in one pod
apiVersion: apps/v1
kind: Deployment
metadata:
  name: multicontainer-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: multicontainer-app
  template:
    metadata:
      labels:
        app: multicontainer-app
        version: v1
    spec:
      volumes:
      - name: dbdata
        emptyDir: {}
      - name: container1-configmap
        configMap: 
          name: container1-configfile
      containers:
      # This container retrieves data from 3rd party app
      - name: data-retrieval-container # This is container 1
        image: <container1 image goes here>
        imagePullPolicy: Always
        env:
        - name: PYTHONUNBUFFERED
          value: "1"
        - name: USERNAME
          valueFrom:
            secretKeyRef:
              name: container1-secret
              key: username
        - name: PASSWORD
          valueFrom:
            secretKeyRef:
              name: container1-secret
              key: password
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
          requests:
            memory: "64Mi"
            cpu: "250m"
        volumeMounts:
        - mountPath: /code/dbdata
          name: dbdata    
        - mountPath: /code/config
          name: container1-configmap   
      # OData publisher container
      - name: data-publisher-container # This is container 2
        image: <container2 image goes here>
        imagePullPolicy: Always
        resources:
          limits:
            memory: "128Mi"
            cpu: "500m"
          requests:
            memory: "64Mi"
            cpu: "250m"
        ports:
        - containerPort: 4004
          name: http
        volumeMounts:
        - mountPath: /usr/src/app/dbdata
          name: dbdata
        command: ['sh', '-c', "while [ ! -f /usr/src/app/dbdata/data-model.cds ] ; do echo Waiting for data model; sleep 5; done; mv /usr/src/app/dbdata/data-model.cds /usr/src/app/db/data-model.cds; echo Updated db model.; cd /usr/src/app; npm start"]
---
apiVersion: v1
kind: Service
metadata:
  name: odata-publisher-service
  labels:
    app: multicontainer-app
spec:
  ports:
  - port: 4004
    name: http
    targetPort: 4004
  selector:
    app: multicontainer-app
---
apiVersion: gateway.kyma-project.io/v1alpha1
kind: APIRule
metadata:
  name: odata-publisher-api-rule
spec:
  gateway: kyma-gateway.kyma-system.svc.cluster.local
  service:
    name: odata-publisher-service
    port: 4004
    host: odata-publisher-api
  rules:
    - accessStrategies:
        - config:
            required_scope:
            - publisherscope #This is what you set on Kyma's OAuth2 client
          handler: oauth2_introspection
      methods:
      - GET
      path: /.*
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: container1-configfile
data:
  settings.cfg: |
    # Server related information
    # --- Deleted the config file contents for this blog ---
---
apiVersion: v1
kind: Secret
metadata:
  name: container1-secret
type: kubernetes.io/basic-auth
stringData:
  username: yyyyyyyyyy
  password: xxxxxxxxxx

I think we need a yaml code formatting ⇧ Sorry for the wild coloring! So, what do we have here? Some explanations:

1. Deployment:

◉ It defines one volume dbdata which is of type emptydir. With this we get an empty volume that containers in the same pod can mount onto different mount paths and have read/write-access to. Very handy! This volume is no PVC – data stored in it will vanish once the pod is deleted. A container crash will not delete the data.

◉ Next, it defines the containers. A container1 which is my Python app, and a container2 which is my SAP CAP app. You see that both mount the dbdata to different mountpoints. Container1 also mounts a config map which contains settings – I have deleted the contents in above copy.

◉ Note the command in container2. It tells the container to wait until container1 has delivered the data-model.cds file in the shared volume. Only then npm start will be executed to run the application and start serving OData.

2. Service:

◉ Defined to allow exposure of the OData service to the internet in the API rule definition.

3. API Rule:

◉ Creates the OAuth2 secured API for (in my case) SAP SAC.

4. Config Map:

◉ Contains parameters for container 1. I deleted them but you see the template in case you want to use it. It provides a file called settings.cfg that will be later copied into the mounted folder.

5. Secret:

◉ It holds the username and password to authenticate towards the 3rd-party app.
To make OAuth work you need to set up an instance on Kyma like this:

SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Guides, SAP HANA Career
Creation of an OAuth client in Kyma UI

and it is here where you define the scope name that we refer to in the yaml above (called publisherscope):

SAP HANA Exam Prep, SAP HANA Learning, SAP HANA Tutorial and Material, SAP HANA Guides, SAP HANA Career
Definition of OAuth client in Kyma UI.

No comments:

Post a Comment