![SAP_Business_Technology_Platform_R.png](https://quantinsightsnetwork.com/wp-content/uploads/2023/11/SAP_Business_Technology_Platform_R-678x145.png)
![]() |
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.
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…
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.
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:
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
- 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
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.
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.
SAP Build Apps web applications build service
Please find below a comprehensive summary of SAP Build Apps build service capabilities.
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.
Be the first to comment