Deploy SAP Build Apps web applications easy to SAP BTP, Kyma runtime.

SAP Build Apps and SAP Appgyver community editions are build-apps platforms featuring Composer Pro – a browser-based application designer with an integrated build service.


This brief is to explain how to make use of the SAP Build Apps build service and have your SAP Build Apps web applications deployed directly to SAP BTP, Kyma runtime. With little effort.

That is leveraging k8s volumes and a public SAP Approuter image deployed to a kyma cluster and acting as a multi-tenant web application server.

There is a number of build-apps solutions on the market that allow to create and build (export) static web applications content.

In particular, SAP Build Apps offers an integrated build service which features both manual and automated deployment capability into SAP BTP CF runtime environment as well as it enables custom deployments to third-party hosting services.

Necessity is the mother of invention

I am a SAP BTP solution architect and am mostly working on business solutions that involve k8s/kyma runtime environments. And, I am eagerly using SAP Build Apps services in my daily work as well.

As I wanted to have my frontends and backends on the same side of the fence, I needed both simple and reliable way of deploying my SAP Build apps into SAP Kyma.

Q. What have I done?

A. In a nutshell, I extended the aforementioned SAP build Apps custom deployment pattern  to SAP BTP, Kyma runtime.  And that, without any reliance on additional BTP services.

I opted for a native k8s/kyma volume binding mechansim with SAP Approuter acting as an application server.

Once downloaded from the SAP Build Apps build service, a static web app is “injected” directly into a running SAP approuter context via a standard k8s volume binding mechanism.

Here goes my story…

Table of Contents
  1. Build-apps solutions with SAP BTP, Kyma runtime
  2. Solution brief.
  3. Deploying SAP Build apps into SAP Kyma.
    1. Solution highlights
  4. Conclusion
  5. Appendix.

Build-apps with SAP Kyma runtime.

The promise of build-apps (low/no/pro-code) is to enable developers to craft business-oriented applications. And that, with whatever runtime engine under the bonnet.

As of such, tools like SAP Build Apps can be quite prominent when it comes to help crafting such fine business apps, without being a car mechanic…

However, build-apps is not only about tools. It is also a powerful design paradigm behind many software solutions,

In particular this bodes well with the kubernetes environments where the desired state of a deployed solution is represented by a bunch of manifest templates – text files – which are a notary contract between a solution designer and k8s cluster.

Solution brief.

a. We need a static web application (build-app) and an application web server (approuter) to server it. Both are to be deployed to the same kyma cluster.

b. We can use SAP Build Apps designer to create a build-app and we deploy an approuter using the public SAPSE approuter docker image.

c. Then, we an use SAP Build Apps build service to download (to a local disk storage) a build-app packaged as a ZIP file.

d. Once downloaded, a build-app ZIP file can either be deployed to an external hosting service (github,  firebase , BTP HTML5 repo) or bound as a volume of a SAP Approuter running on Kyma.

  • In order to be able to use a ZIP file as a volume, first the file needs to be uploaded into a cloud storage service, for instance into an objectstore.
  • However, I’ve chosen to upload it into a private github repository (cf. the following community post here.)

Let’s summarize the list of the recipe ingredients:

Deploying SAP Build apps to SAP Kyma.

There are many ways to upload static content into kubernetes clusters and workloads.

(An obvious solution would be to leverage a SAP BTP HTML5 repository service. However, that would imply having a public internet route towards the HTML5 repo content. And that’s not what I wanted with a SAP approuter deployed to a kyma environment.)

From the moment, a ZIP file is uploaded to a cloud storage, it can be made available to a kyma cluster, as follows:

a. Pod’s in-memory pod ephemeral storage as a volume to host a static web application package developed with SAP Build Apps (index.html)

The following initContainers snippet demonstrates how to populate an emptyDir volume with data coming from a secure private github repository used as a cloud storage to an approuter:

      initContainers:
        - name: install
          image:  alpine/curl 
          securityContext:
            runAsUser: 1337   
          command:
           - sh 
           - -c
           - >-
               curl -H 'Authorization: token {{ $token }}' -H 'Accept: application/vnd.github.v4.raw' -o /app/resources-dir/{{ $webappname }}  -L  {{ $image }}{{ $webappname }} &&

               unzip /app/resources-dir/{{ $webappname }} -d /app/resources-dir
          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir
      volumes:
        - name: resources-dir
          emptyDir:
            medium: "Memory"
            sizeLimit: 50Mi

The build-app package is stored in a private github repository and is being pulled programmatically from there using the Github REST API to download a zip archive for a repository. (The entire approuter deployment file can be looked up in the appendix.)

b. ReadWriteMany persistent volume claims (for now this is only supported on AZURE Kyma clusters)

values.yaml

clusterDomain: 
gateway:
ttlDaysAfterFinished: 0

services:
  app:
    name: faas-appgyver
  appgyver:
    webappname: app-*****_web_build-****.zip
    token: ghp_*******************************
    webapppath: https://github.com/api/v3/repos/<>/<>/contents/appgyver/ 

pvc.yaml

kind: PersistentVolumeClaim
apiVersion: v1
metadata:
  name: resources-dir
  labels:
    {{- include "app.labels" . | nindent 4 }}
    app: {{ .Values.services.app.name }}

spec:
  accessModes:
    - ReadWriteMany 
  resources:
    requests:
      storage: 1Gi
  storageClassName: 'files'
  volumeMode: Filesystem

job.yaml

{{- $deployment := .Values.services | default dict }}
{{- $image := $deployment.appgyver.webapppath | default "/" }} 
{{- $webappname := $deployment.appgyver.webappname | default "app-*****_web_build-****.zip" }} 
{{- $token := $deployment.appgyver.token | default "token" }}

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ .Values.services.app.name }}
  labels:
    {{- include "app.labels" . | nindent 4 }}
    app: {{ .Values.services.app.name }}
spec:
  ttlSecondsAfterFinished: 100 ## {{ mul .Values.ttlDaysAfterFinished 24 60 60 }}
  parallelism: 1
  completions: 1
  manualSelector: false

  template:
    metadata:
      labels: 
        {{- include "app.selectorLabels" . | nindent 8 }}
        sidecar.istio.io/inject: 'false'

    spec:
      restartPolicy: OnFailure

      containers:
        - image: busybox
          name: busybox
          command: ['sh', '-c', 'echo The job is running! && sleep 1']

          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir

      initContainers:
        - name: install000 
          image:  alpine 
          command:
           - sh 
           - -c
           - >-

               chmod -R 777 /app && ls -lh -d /app/resources-dir

          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir

        - name: install001
          image:  alpine/curl 
   
          command:
           - sh 
           - -c
           - >-

               curl -H 'Authorization: token {{ $token }}' -H 'Accept: application/vnd.github.v4.raw' -o /app/resources-dir/{{ $webappname }}  -L  {{ $image }}{{ $webappname }} &&

               ls -l app/resources-dir && 

               unzip -o /app/resources-dir/{{ $webappname }} -d /app/resources-dir &&

               ls -l /app/resources-dir &&
               ls -lh -d /app/resources-dir

          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir

      volumes:
        - name: resources-dir
          persistentVolumeClaim:
            claimName: resources-dir

Solution highlights

  • deployments of static web applications directly to an approuter running on kyma clusters is done with a bunch of manifest text files with a few lines of github API automation code inside
  • deployments can be fully automated for instance with the github actions
  • no need to build custom docker images
  • no downtime when updating the static build-app content
  • build-app static content can be mixed up with sone other local static content (implemented as a config map to serve a default.html page, cf appendix for details). That would not be possible in the HTML5 repo scenario as documented here

Conclusion

Any caveats? As of now, there is not  much way to automate the SAP Build Apps build service download option. On the other hand, once a ZIP file has been downloaded and then committed to a github repository, the commit itself can trigger a github action to start an automated deployment to kyma.

Nonetheless, I am truly pleased with the outcome. Now, anyone can deploy build apps straight to k8s/kyma environments with little effort.

Last but not least, I hope you have enjoyed reading this blog. For the sake of time, I have offloaded the implementation notes to the appendix section below.

Feel free to provide your feedback and comments.

 


Appendix

Table of Contents
  1. Implementation and troubleshooting notes.
    1. Building web application packages with SAP Build Apps community edition platform.
      1. Examples of SAP Build Apps frontends on Kyma
      2. Integration with SAP Workzone and MS Teams
    2. SAP Build Apps web applications build service
    3. SAP Approuter – a multi-tenant web application server
    4. Approuter deployment manifest
    5. Pre-requisites/Disclaimers/Additional resources/Who am I? section

Implementation and troubleshooting notes

Building web application packages with SAP Build Apps community edition platforms

SAP Appgyver development team has eventually published an iFrame component (for “WebView support for web”) that allows for easy iframe embedding in web applications.

Let’s create a custom component which integrates the iFrame component. That way one can create fairly easily compositions of iframe-embedded widgets.

To make it simple and easy to consume I created a shared container as depicted below:

Published! Share token: ewDn4jhGHlkbzGbhAP1F7Q

Then, one can start using this component with a simple drag-and-drop in SAP Appgyver community editions apps.

On a side note the iFrame web component can be used to inject other React Native components, as explained in this blogpost:

SAP Build Apps on Kyma

Just a couple of examples of good looking apps running direclty on kyma.

Your virtual Musee du Louvre visit and mug painting powered by DALL·E:

or, eventually, an excursion (if ever), to Mars:

Last but not least, why not having a business application leveraging SAP HANA and SAP Analytics Cloud as well. All on Kyma folks.

Integration with SAP Build Workzone and MS Teams

That may be an interesting publishing option to many of you, especially it does not require much effort.

This is a SAP Fiori Launchpad. embedded into MS Teams as Teams app, where each tile is a SAP Build web-app running on Kyma

 

 

SAP Build Apps web applications build service

Please find below a comprehensive summary of SAP Build Apps build service capabilities.

SAP Build Apps can produce ready-to-deploy MTAR files which can be deployed to a BTP sub-account’s HTML5 repository.


Headline:

  • MTAR deployments can be either manual or fully automated. CF runtime only, SAP managed approuter only.

SAP Build Apps can produce ready-to-deploy static web applications content packaged as ZIP files.


Headlines:

  • ZIP deployments are manual only. SAP BTP CF and Kyma runtime environments are supported as well as any 3rd party hosting service (Github, Firebase, etc).
  • Both SAP managed (CF-only) and self-managed, multi-tenant (CF&Kyma) SAP Approuters supported.
Subsequently, static web-app ZIP files can be:

SAP Approuter as a multi-tenant web application server

SAP Approuter can be used not only as an application router. It can also act as a multi-tenant web application server by serving either:

It is also complementing the subscription-based SAP managed approuter , namely:

  • it can be deployed to either CF or Kyma runtime environments or both at a time
  • it does not require a BTP subscription to a Launchpad/Portal/SAP Build Workzone service
  • it does support a BTP [sub-account based] multi-tenancy model with custom domains
  • it can act as a web application hosting static web app resources
  • it is available as a ready-to-deploy SAPSE public docker file.

Good to know:

  • SAP approuter could be replaced by any other web-app hosting and routing service….For instance, (Google Firebase or Github hosting or any other third party hosting service)
  • However, one prominent advantage the SAP Approuter has is that it supports destinations including SAP BTP destinations, as well as it does support the BTP multi-tenancy model.
  • That means, one can get a multi-tenant web-app on SAP BTP platform out-of the-box with the full access to the entitled BTP services (with the BTP platform built-in authentication and authorization mechanism at hand).
  • Last but not least, any static web-app can be deployed to a BTP sub-account HTML5 repo as well, for instance with the help of the public SAP HTML5 deployer.

Approuter deployment manifest

SAPSE Approuter deployment.yaml

{{- $deployment := .Values.services | default dict }}
{{- $image := $deployment.appgyver.webapppath | default "/" }} 
{{- $webappname := $deployment.appgyver.webappname | default "app-*****_web_build-****.zip" }}
{{- $token := $deployment.appgyver.token | default "token" }} 
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ .Values.services.app.name }}
  labels:
    {{- include "app.labels" . | nindent 4 }}
    app: {{ .Values.services.app.name }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels: 
      {{- include "app.selectorLabels" $ | nindent 6 }}

  template:
    metadata:
      labels: 
        {{- include "app.selectorLabels" . | nindent 8 }}

    spec:
      {{- if (include "app.ha" .) }}
      topologySpreadConstraints:

      {{- range $constraint := .Values.availability.topologySpreadConstraints }}
      - maxSkew: {{ $constraint.maxSkew }}
        topologyKey: {{ $constraint.topologyKey }}
        whenUnsatisfiable: {{ $constraint.whenUnsatisfiable }}
        labelSelector:
          matchLabels: 
            {{- include "app.selectorLabels" $ | nindent 12 }}

      {{- end }}
      {{- end }}

      containers:
        - image: "{{ .Values.services.app.image.dockerID }}/{{ .Values.services.app.image.repository }}:{{ .Values.services.app.image.tag }}"
          name: {{ .Values.services.app.name }}
          imagePullPolicy: {{ .Values.services.app.image.pullPolicy }}
          resources:
            limits:
              memory: 512Mi
              cpu: "1"
            requests:
              memory: 128Mi
              cpu: "0.1"
          ports:
            - name: http
              containerPort: {{ .Values.services.app.image.port }}
          env:
            - name: SERVICE_BINDING_ROOT
              value: /bindings
            - name: PORT
              value: '{{ .Values.services.app.image.port }}'

            - name: XS_APP_LOG_LEVEL
              value: debug

            - name: DEBUG
              value: '*' ## xssec:* ### https://www.npmjs.com/package/@sap/xssec

            - name: COOKIES
              value: "{ "SameSite":"None" }" ### https://me.sap.com/notes/0002953730
            - name: SEND_XFRAMEOPTIONS
              value: 'false' ###https://www.npmjs.com/package/@sap/approuter#x-frame-options-configuration
            - name: ENABLE_X_FORWARDED_HOST_VALIDATION
              value: 'true' ### https://www.npmjs.com/package/@sap/approuter#configurations
            - name: INCOMING_CONNECTION_TIMEOUT
              value: '1800000'  # 180 seconds, the default value is 120 seconds
            - name: CORS
              value: |-
                [
                  {
                    "uriPattern": "^(.*)$", 
                    "allowedOrigin": [
                      {"host":"*", "protocol":"https"}
                   ], 
                    "allowedMethods": ["GET", "POST", "HEAD", "OPTIONS", "PUT", "DELETE"],
                    "allowedHeaders": ["Origin", "Accept", "X-Requested-With", "Content-Type", "Access-Control-Request-Method", "Access-Control-Request-Headers", "Authorization", "X-Sap-Cid", "X-Csrf-Token", "Accept-Language"],
                    "exposeHeaders": ["Accept", "Authorization", "X-Requested-With", "X-Sap-Cid", "Access-Control-Allow-Origin", "Access-Control-Allow-Credentials", "X-Csrf-Token", "Content-Type"]
                  }
                ]

          envFrom:
            - configMapRef:
                name: {{ .Values.services.app.name }}
          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir

            - name: xs-app
              mountPath: "/app/xs-app.json"
              subPath: "xs-app.json"
              readOnly: true
            - name: resources
              mountPath: "/app/resources-dir/default.html" ##"/app/resources/default.html"
              subPath: "default.html"
              readOnly: true
            - name: faas-uaa
              mountPath: "/bindings/faas-uaa"
              readOnly: true
            - name: faas-dest
              mountPath: "/bindings/faas-dest"
              readOnly: true

      initContainers:
        - name: install
          image:  alpine/curl
          securityContext:
            runAsUser: 1337 
          command:
           - sh 
           - -c
           - >-
               curl -H 'Authorization: token {{ $token }}' -H 'Accept: application/vnd.github.v4.raw' -o /app/resources-dir/{{ $webappname }}  -L  {{ $image }}{{ $webappname }} &&

               ls -l app/resources-dir && 

               unzip /app/resources-dir/{{ $webappname }} -d /app/resources-dir &&

               ls -l /app/resources-dir &&
               ls -lh -d /app/resources-dir
          volumeMounts:
            - name: resources-dir
              mountPath: /app/resources-dir
              subPath: resources-dir

      volumes:
        - name: resources-dir
          emptyDir:
            medium: "Memory"
            sizeLimit: 70Mi 

        - name: xs-app
          configMap:
            name:  {{ .Values.services.app.xsapp }}
        - name: resources
          configMap:
            name: {{ .Values.services.app.resources }}
        - name: faas-uaa
          secret:
            secretName: {{ .Values.services.uaa.bindingSecretName }}
        - name: faas-dest
          secret:
            secretName: {{ .Values.services.dest.bindingSecretName }}

Please note the usage of topology constraints in the above manifest. This is to make sure at least one replica is deployed to all three Availability Zones of a kyma cluster.

Per aspera ad astra. Who am I?

You can follow me in SAP Community: Piotr Tesny

Pre-requisites:

  • Access to SAP Build Apps community edition
  • Access to SAP BTP global account with Kyma runtime environment (a SAP BTP-free tier or trial account will do.)
  • Access to a private github repository

Disclaimer:

  • The ideas presented in this blog are personal insights thus not necessarily endorsed by SAP.
  • I have no affiliation with any of the non-SAP brand names quoted in this brief.
  • Please note all the code snippets or gists are provided “as is”.
  • Images/data in this blog post is from SAP internal sandbox, sample data, or demo systems. Any resemblance to real data is purely coincidental.
  • Access to some online resources referenced in this blog may be subject to a contractual relationship with SAP and a S-user login may be required. Always refer to T&C.

Additional resources

 

Scroll to Top