Managing SAP CAP Applications with Kubernetes
In this blog post, we will explore how to manage SAP Cloud Application Programming (CAP) applications using Kubernetes. We will cover deployment strategies, scaling, and monitoring to ensure your CAP applications run efficiently in a containerized environment. If you're new to the underlying technologies, check out the Kubernetes for Developers series and the Docker for Web Development series for a solid foundation.
The goal is to build 2 CAP applications serving 2 services. Both applications will share a SQLite database. In front of it we will have an AppRouter application that will serve as a reverse proxy and route the requests to the correct CAP application.
Each CAP application will be deployed in its own pod, and the AppRouter will also run in a separate pod. We will use Kubernetes services to expose the applications within the cluster and manage the communication between them. The SQLite database will be shared between the CAP applications in a hostPath volume mount, which is good for local development, but expecting another persistent layer (like HANA Cloud instance) for productive scenarios.
Architecture Overview
┌──────────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
├──────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ │
│ │ AppRouter Pod │ │
│ │ │ │
│ │ - Express.js │ │
│ │ - Proxy Routes │ │
│ └────────┬─────────┘ │
│ │ │
│ ├──────────────────┬──────────────────┐ │
│ │ │ │ │
│ ┌────────▼──────────┐ ┌────▼─────────────-┐ │ │
│ │ CAP App Pod 1 │ │ CAP App Pod 2 │ │ │
│ │ │ │ │ │ │
│ │ - Service A │ │ - Service B │ │ │
│ │ - Business Logic │ │ - Business Logic │ │ │
│ └────────┬──────────┘ └────┬──────────────┘ │ │
│ │ │ │ │
│ └──────────────────┼──────────────────┘ │
│ │ │
│ ┌─────────▼────────┐ │
│ │ Persistent Vol. │ │
│ │ │ │
│ │ SQLite Database │ │
│ │ │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘Creating the SAP CAP applications
Since this is not a tutorial on how to create CAP applications, Let's go fast on this part. We will create 2 CAP applications, one for each service. You can use the cds init command to create a new CAP project and then define your services and entities in the db and srv folders.
First app will serve a bookshop admin service to manage books and authors, and the second app will serve a bookshop shop service to manage customer orders and inventory. Then we will create an AppRouter application to route the requests to the correct CAP application based on the URL path.
All apps will be created in a folder called /apps. The structure will look like this:
/apps
├── bookshop-admin
│ ├── db
│ └── srv
├── bookshop-shop
│ ├── db
│ └── srv
└── approuterSetting up the SAP AppRouter as reverse proxy
Let's start creating the /apps folder:
mkdir apps
cd appsNow we will create the AppRouter application. The AppRouter is a Node.js application that acts as a reverse proxy and routes the requests to the correct CAP application based on the URL path. We will create a folder called approuter and inside it, we will create a package.json file and an xs-app.json file.
mkdir approuter
touch approuter/package.json
touch approuter/xs-app.jsonNow copy the content of the package.json and xs-app.json files from the SAP Approuter documentation into the respective files.
package.json:
{
"name": "approuter",
"version": "1.0.0",
"description": "AppRouter for CAP applications",
"main": "approuter.js",
"scripts": {
"start": "node node_modules/@sap/approuter/approuter.js"
},
"dependencies": {
"@sap/approuter": "^22.0.0"
}
}xs-app.json:
{
"welcomeFile": "/index.html",
"authenticationMethod": "none",
"routes": [
{
"source": "^/admin/(.*)$",
"target": "/odata/v4/admin/$1",
"destination": "bookshop-admin"
},
{
"source": "^/shop/(.*)$",
"target": "/odata/v4/shop/$1",
"destination": "bookshop-shop"
}
]
}Last add the Dockerfile for the AppRouter application:
# Approuter image.
# Node modules for @sap/approuter are baked into the image so the pod doesn't
# need network access at start.
FROM node:lts
WORKDIR /approuter
# Install @sap/approuter once at build time.
COPY package.json ./
RUN npm install --no-fund --no-audit \
&& npm cache clean --force
# Bake the route config into the image.
COPY xs-app.json ./
EXPOSE 5000
CMD ["sh", "-c", "npm start"]The Bookshop-Admin CAP Application
Now we will create the first CAP application called bookshop-admin. This application will serve a bookshop admin service to manage books and authors.
Execute the following command within the /apps folder:
cds init bookshop-adminLet's continue creating the db schema for the bookshop-admin service. Create a file called schema.cds inside the db folder with the following content:
namespace bookshop.admin;
entity Authors {
key ID : UUID;
name : String;
}
entity Books {
key ID : UUID;
title : String;
author : Association to Authors;
price : Decimal(10,2);
}And now the service definition. Create a file called admin-service.cds inside the srv folder with the following content:
using bookshop.admin as admin from '../db/schema';
service AdminService {
entity Authors as projection on admin.Authors;
entity Books as projection on admin.Books;
}Since we are executing in localhost we will use SQLite as our database and set authentication to none. The SQLite db file will be stored in a common location for both CAP applications. Edit the package.json file to include the following dependencies and settings:
{
"name": "bookshop-admin",
"version": "1.0.0",
"description": "Bookshop Admin CAP Application",
"scripts": {
"start": "cds-serve",
"watch": "cds watch --port 4004"
},
"dependencies": {
"@sap/cds": "^9",
"express": "^4",
"@cap-js/sqlite": "^2"
},
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": {
"url": "./db/bookshop.db"
}
},
"auth": {
"kind": "dummy"
}
}
}
}Last add the Dockerfile for the bookshop-admin application:
# Thin runtime image for both CAP services.
FROM node:lts-alpine
RUN npm install -g @sap/cds-dk@^9 \
&& npm cache clean --force
WORKDIR /app
# Install the CAP service dependencies.
COPY package.json ./
RUN npm install --no-fund --no-audit \
&& npm cache clean --force
EXPOSE 4004
# The Deployment overrides command/args; default here is `cds watch` which is
# what the demo uses for live-update.
CMD ["sh", "-c", "cds watch --port 4004"]The Bookshop-Shop CAP Application
Now we will create the second CAP application called bookshop-shop. This application will serve a bookshop shop service to manage customer orders and inventory.
Execute the following command within the /apps folder:
cds init bookshop-shopAnd continue creating the db schema for the bookshop-shop service. Create a file called schema.cds inside the db folder with the following content:
namespace bookshop.shop;
using {managed} from '@sap/cds/common';
entity Orders : managed{
key ID : UUID;
customerName : String;
}
entity OrderItems {
key ID : UUID;
order : Association to Orders;
book_ID : UUID;
quantity : Integer;
}The service definition will be created in a file called shop-service.cds inside the srv folder with the following content:
using bookshop.shop as shop from '../db/schema';
service ShopService {
entity Orders as projection on shop.Orders;
entity OrderItems as projection on shop.OrderItems;
}The package.json file will be updated to include the following dependencies and settings:
{
"name": "bookshop-shop",
"version": "1.0.0",
"description": "Bookshop Shop CAP Application",
"scripts": {
"start": "cds-serve",
"watch": "cds watch --port 4004"
},
"dependencies": {
"@sap/cds": "^9",
"express": "^4",
"@cap-js/sqlite": "^2"
},
"cds": {
"requires": {
"db": {
"kind": "sqlite",
"credentials": {
"url": "./db/bookshop.db"
}
},
"auth": {
"kind": "dummy"
}
}
}
}And finally, the Dockerfile for the bookshop-shop application will be as follows:
# Thin runtime image for both CAP services.
FROM node:lts-alpine
RUN npm install -g @sap/cds-dk@^9 \
&& npm cache clean --force
WORKDIR /app
# Install the CAP service dependencies.
COPY package.json ./
RUN npm install --no-fund --no-audit \
&& npm cache clean --force
EXPOSE 4004
# The Deployment overrides command/args; default here is `cds watch` which is
# what the demo uses for live-update.
CMD ["sh", "-c", "cds watch --port 4004"]Building the Kubernetes deployment manifests
Now that we have our Docker images, we need to create Kubernetes manifests to deploy our applications. We will use Deployments to manage the application pods and Services to expose them. We will create a folder called /k8s and inside it, we will create the following files:
approuter-deployment.yamlbookshop-admin-deployment.yamlbookshop-shop-deployment.yamlservices.yaml
AppRouter Deployment
The approuter-deployment.yaml file will contain the following:
apiVersion: apps/v1
kind: Deployment
metadata:
name: approuter-deployment
labels:
app: approuter
spec:
replicas: 1
selector:
matchLabels:
app: approuter
template:
metadata:
labels:
app: approuter
spec:
containers:
- name: approuter
image: approuter:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 5000
env:
# @sap/approuter looks up route `destination` names here.
# In a real BTP deployment this comes from the destination service;
# locally we point each name at the matching in-cluster Service.
- name: destinations
value: |
[
{ "name": "bookshop-admin", "url": "http://bookshop-admin-service:4004", "forwardAuthToken": false },
{ "name": "bookshop-shop", "url": "http://bookshop-shop-service:4004", "forwardAuthToken": false }
]Bookshop Admin Deployment
The bookshop-admin-deployment.yaml file will contain the following:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookshop-admin-deployment
labels:
app: bookshop-admin
spec:
replicas: 1
selector:
matchLabels:
app: bookshop-admin
template:
metadata:
labels:
app: bookshop-admin
spec:
initContainers:
# Seed an empty volume with the image's own node_modules so the main
# container's /app/node_modules stays Linux-native even though /app is
# bind-mounted from a macOS host.
- name: seed-node-modules
image: bookshop-admin:latest
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "cp -a /app/node_modules/. /seed/"]
volumeMounts:
- name: node-modules
mountPath: /seed
containers:
- name: bookshop-admin
image: bookshop-admin:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4004
volumeMounts:
- name: bookshop-admin-volume
mountPath: /app
- name: node-modules
mountPath: /app/node_modules
- name: sqlite-volume
mountPath: /db
volumes:
- name: bookshop-admin-volume
hostPath:
path: {your_absolute_path_to_project_root}/test/apps/bookshop-admin
type: DirectoryOrCreate
- name: node-modules
emptyDir: {}
- name: sqlite-volume
hostPath:
path: {your_absolute_path_to_project_root}/test/apps/db
type: DirectoryOrCreateBookshop Shop Deployment
The bookshop-shop-deployment.yaml file will contain the following:
apiVersion: apps/v1
kind: Deployment
metadata:
name: bookshop-shop-deployment
labels:
app: bookshop-shop
spec:
replicas: 1
selector:
matchLabels:
app: bookshop-shop
template:
metadata:
labels:
app: bookshop-shop
spec:
initContainers:
# Seed an empty volume with the image's own node_modules so the main
# container's /app/node_modules stays Linux-native even though /app is
# bind-mounted from a macOS host.
- name: seed-node-modules
image: bookshop-shop:latest
imagePullPolicy: IfNotPresent
command: ["sh", "-c", "cp -a /app/node_modules/. /seed/"]
volumeMounts:
- name: node-modules
mountPath: /seed
containers:
- name: bookshop-shop
image: bookshop-shop:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 4004
volumeMounts:
- name: bookshop-shop-volume
mountPath: /app
- name: node-modules
mountPath: /app/node_modules
- name: sqlite-volume
mountPath: /db
volumes:
- name: bookshop-shop-volume
hostPath:
path: {your_absolute_path_to_project_root}/test/apps/bookshop-shop
type: DirectoryOrCreate
- name: node-modules
emptyDir: {}
- name: sqlite-volume
hostPath:
path: {your_absolute_path_to_project_root}/test/apps/db
type: DirectoryOrCreateExposing CAP apps with Kubernetes Services
To expose the applications within the Kubernetes cluster, we will create services for each application. The services will allow the applications to communicate with each other and be accessible from outside the cluster. For this let's create a services.yaml file with the following content:
apiVersion: v1
kind: Service
metadata:
name: approuter-service
spec:
selector:
app: approuter
ports:
- protocol: TCP
port: 5000
targetPort: 5000
type: LoadBalancer
---
apiVersion: v1
kind: Service
metadata:
name: bookshop-admin-service
spec:
selector:
app: bookshop-admin
ports:
- protocol: TCP
port: 4004
targetPort: 4004
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: bookshop-shop-service
spec:
selector:
app: bookshop-shop
ports:
- protocol: TCP
port: 4004
targetPort: 4004
type: ClusterIPBuilding Docker images and deploying to Kubernetes
It's time now to build the Docker images and deploy the applications to Kubernetes. Make sure you have Docker and Kubernetes installed and running on your machine.
# Build the Docker images
docker build -t approuter:latest ./apps/approuter
docker build -t bookshop-admin:latest ./apps/bookshop-admin
docker build -t bookshop-shop:latest ./apps/bookshop-shop
# Apply the Kubernetes manifests
kubectl apply -f k8s/Deploying CAP schemas to the shared SQLite database
Both CAP applications share a common SQLite database. To be able to consume data we should deploy the schemas to the database. For this we will use the cds deploy command, which will inherit the folder to save the sqlite database from the cds configuration in the package.json.
To do so run the following commands:
# Deploy the bookshop-admin schema to the database
cd apps/bookshop-admin
cds deploy
# Deploy the bookshop-shop schema to the database
cd ../bookshop-shop
cds deployNote: The
cds deploycommand will create the SQLite database file. Depending on your environment configuration you might need to install the npm dependencies in the folder, if so, donpm installbefore running thecds deploycommand.
Now the application is ready to be consumed. You can access the AppRouter application using the URL http://localhost:5000. You can use the following routes to access the CAP applications:
# Access the bookshop-admin service
http://localhost:5000/admin/Authors
http://localhost:5000/admin/Books
# Access the bookshop-shop service
http://localhost:5000/shop/Orders
http://localhost:5000/shop/OrderItemsConclusion: running SAP CAP on Kubernetes
In this blog post, we have explored how to manage SAP Cloud Application Programming (CAP) applications using Kubernetes. We created two CAP applications, bookshop-admin and bookshop-shop, and an AppRouter application to route requests to the correct CAP application.
We built Docker images for each application and created Kubernetes manifests to deploy them in a cluster. The CAP Deployments share a common SQLite database, stored in a common directory that is mounted as a volume in both CAP application pods. We also set up Kubernetes services to expose the applications within the cluster and allow communication between them.
Finally, the AppRouter application is exposed to the outside world using a LoadBalancer service, allowing users to access the CAP applications through the AppRouter routes. The destinations in the AppRouter configuration point to the internal Kubernetes services for each CAP application, enabling seamless routing of requests. Those destinations are passed as environment variables to the AppRouter container, allowing it to dynamically route requests based on the defined routes.
