Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/serverless-js-configmap/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Build artifacts produced locally; not committed.
.unikraft/
28 changes: 28 additions & 0 deletions examples/serverless-js-configmap/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Multi-stage build for the GENERIC serverless Node.js runtime image.
#
# This is the key difference from examples/hello-node: NO application/JS code is
# baked in. The final scratch stage carries ONLY the Node runtime -- the `node`
# interpreter plus every shared library it links against. The function itself
# (index.js) is supplied at deploy time via a ConfigMap mounted at /app, so one
# image serves any number of functions and code swaps need no rebuild.
#
# Node is NOT a static-PIE binary -- it is a dynamic musl ELF that needs the
# musl loader and a handful of shared libraries at boot. So this rootfs ships
# the node interpreter AND every .so it links, and the unikernel runs on the
# base-compat:latest runtime (the binary-compatibility / dynamic-loader
# elfloader variant), NOT base:latest. A scratch image missing those libs won't
# boot (library-not-found).
FROM node:22-alpine AS build
# Record node's dynamic-library requirements in the build log for auditing.
RUN echo "=== ldd /usr/local/bin/node ===" && ldd /usr/local/bin/node || true

FROM scratch
# The node interpreter -- the entire "function runtime". No app code here.
COPY --from=build /usr/local/bin/node /usr/bin/node
# musl dynamic loader + libc (ld-musl is both loader and libc on alpine).
COPY --from=build /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1
# C++/GCC runtime libraries node links against.
COPY --from=build /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1
COPY --from=build /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6
# os-release lets node/libc identify the platform cleanly.
COPY --from=build /etc/os-release /etc/os-release
27 changes: 27 additions & 0 deletions examples/serverless-js-configmap/Kraftfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Kraftfile for the generic serverless Node.js runtime image.
#
# spec v0.7 + rootfs.format: erofs is what makes Datum ConfigMap/Secret file
# mounts materialize into the unikraft microVM (validated 2026-06-03 on the
# ash-sore-hamster metro). base-compat:latest is required because node is a
# dynamic musl ELF (see Dockerfile).
#
# The cmd points node at /app/index.js -- a MOUNTED path, not a baked one. The
# image contains no index.js; the Workload mounts the `js-function` ConfigMap at
# /app, so /app/index.js is the function supplied at deploy time.
#
# Build/push (push-only, do not start):
# kraft cloud --metro <metro>/v1 --token <token> \
# --buildkit-host docker-container://buildkit \
# deploy --no-start -M 512 --name serverless-js-runtime \
# --runtime base-compat:latest --rootfs ./Dockerfile .
spec: v0.7

name: serverless-js-runtime

runtime: base-compat:latest

rootfs:
source: ./Dockerfile
format: erofs

cmd: ["/usr/bin/node", "/app/index.js"]
97 changes: 97 additions & 0 deletions examples/serverless-js-configmap/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# serverless-js-configmap

A serverless pattern on Datum compute: a **generic Node.js runtime image with no
application code baked in**, where the function (`index.js`) is supplied at
deploy time via a **ConfigMap mounted at `/app`**, and Node executes the mounted
file. Swapping the function is a ConfigMap edit + restart — **no image rebuild**.

This works because Datum ConfigMap/Secret file mounts materialize into the
unikraft microVM as read-only `rom` devices when the image is packaged with
`spec: v0.7`, `runtime: base-compat:latest`, and `rootfs.format: erofs`.

## Files

| File | Purpose |
|------|---------|
| `Dockerfile` | Multi-stage build of the **generic** runtime: a `scratch` image carrying only the `node` interpreter + its musl/libgcc/libstdc++ `.so` files. No app code. |
| `Kraftfile` | `spec: v0.7`, `base-compat:latest`, `rootfs.format: erofs`, `cmd: ["/usr/bin/node", "/app/index.js"]` (a **mounted** path). |
| `index.js` | The function and the **single source of truth** for the ConfigMap: a dependency-free Node stdlib HTTP server on `$PORT` (default 8080). Returns a versioned message; logs a versioned startup line. Mounted into the image, **not** baked in. |
| `kustomization.yaml` | A Kustomize `configMapGenerator` that generates the `js-function` ConfigMap from `index.js` (so there is no hand-maintained copy of the function to drift). `disableNameSuffixHash: true` keeps the name stable for the Workload reference. |
| `workload.yaml` | A `compute.datumapis.com/v1alpha` Workload mounting `js-function` at `/app`, port 8080, `d1-standard-2`, DFW, `minReplicas: 1`. Env intentionally empty (see Pitfalls). |

## 1. Build + push the generic runtime image (push-only)

```sh
export KRAFTKIT_NO_CHECK_UPDATES=true
TOKEN=... # ash-sore-hamster metro token (ukc-credentials secret)

kraft cloud --metro https://api.ash-sore-hamster.unikraft.cloud/v1 --token "$TOKEN" \
--buildkit-host docker-container://buildkit \
deploy --no-start -M 512 --name serverless-js-runtime \
--runtime base-compat:latest --rootfs ./Dockerfile .
```

Packages as an EroFS archive and pushes to
`index.unikraft.io/datum/serverless-js-runtime:latest`. This image is built
**once** and reused for every function version.

## 2. Generate + apply the ConfigMap on the PROJECT control plane

`kubectl apply -k` runs the `configMapGenerator`, building the `js-function`
ConfigMap from `index.js` and applying it. The ReferencedData resolver that
turns ConfigMap data into the unikernel mount runs project-side, so apply it on
the project plane — applying on the cell is a dead end.

```sh
kubectl --context datum-project-datum-cloud -n default apply -k .
```

(`kubectl kustomize .` renders the generated ConfigMap to stdout if you want to
inspect it first.)

## 3. Deploy the Workload

```sh
datumctl compute deploy -f workload.yaml -y
```

## 4. Validate

```sh
# External FQDN (also wakes the instance from scale-to-zero standby):
curl https://<external-fqdn>/ # -> Hello from a ConfigMap-mounted function — v1
curl https://<external-fqdn>/healthz # -> ok

# UKC instance: confirm the JS mount is a rom at /app and the VM booted:
kraft cloud --metro <metro>/v1 --token "$TOKEN" instance get <ukc> -o json # roms[].at == /app, boot_time_us > 0
kraft cloud --metro ash-sore-hamster --token "$TOKEN" instance logs <ukc> # "mount /dev/ukp_romN -> /app" + "serverless-js: vN listening on :8080"
```

Derive `<ukc>` from the cell Pod annotation
`cloud.unikraft.v1.instances/fqdns` → first DNS label of `privateFqdn`.

## 5. Swap the function (the headline)

Edit `index.js` directly (bump `VERSION` to `v2` and the message) — it is the
only copy. Re-apply with `-k` to regenerate the ConfigMap, then recreate the
instance so the new read-only `rom` is baked at boot:

```sh
kubectl --context datum-project-datum-cloud -n default apply -k .
datumctl compute restart serverless-js-fn # rolling restart -> recreate instance
curl https://<external-fqdn>/ # -> ... — v2 (same image, no rebuild)
```

Mounts are read-only roms baked at boot, so a content change only takes effect
once the instance is **recreated** — restarting the workload is the realistic
"redeploy the function" path. The runtime image is never rebuilt or repushed.

## Pitfalls

- **`base-compat:latest`, not `base:latest`** — `node` is a dynamic musl ELF and
needs the dynamic-loader runtime plus the copied `.so` files. A `scratch`
image missing those libs won't boot.
- **Keep `env` empty.** In a busy namespace, kraftlet-injected k8s service-link
env can overflow the UKC kernel-cmdline buffer (`InvalidKernelCommandLine`,
boot `0.00 ms` / standby). The mount needs no env; `PORT` defaults to 8080.
- **The mount is read-only.** The function must not write into `/app`.
29 changes: 29 additions & 0 deletions examples/serverless-js-configmap/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// The serverless "function" -- supplied at deploy time via a ConfigMap, NOT
// baked into the runtime image. Datum mounts the ConfigMap at /app, so this
// file lands at /app/index.js and the Kraftfile's cmd points node at it.
//
// Dependency-free (Node stdlib only): the generic runtime image ships no
// node_modules, so the function must not `require` anything outside core.
//
// The VERSION string is the swap marker: change it here, re-apply with
// `kubectl apply -k .` (Kustomize regenerates the js-function ConfigMap from
// this file), restart the workload, and the HTTP response proves the new code
// took effect with no image rebuild -- the core serverless value prop.
const http = require('http');

const VERSION = 'v1';
const port = parseInt(process.env.PORT, 10) || 8080;

const server = http.createServer((req, res) => {
if (req.url === '/healthz') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok\n');
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from a ConfigMap-mounted function — ' + VERSION + '\n');
});

server.listen(port, () => {
console.log('serverless-js: ' + VERSION + ' listening on :' + port);
});
26 changes: 26 additions & 0 deletions examples/serverless-js-configmap/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generates the `js-function` ConfigMap from index.js, so index.js is the single
# source of truth -- there is no second, hand-maintained copy of the function to
# drift. The `index.js` file becomes the ConfigMap key `index.js`, which Datum
# mounts to /app/index.js inside the microVM.
#
# Apply on the PROJECT control plane (the ReferencedData resolver runs there):
# kubectl --context datum-project-datum-cloud -n default apply -k .
#
# To swap the function: edit index.js and re-apply with `-k .` -- Kustomize
# regenerates the ConfigMap from the new file. Then `datumctl compute restart
# serverless-js-fn` to recreate the instance and pick up the new read-only rom.
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

configMapGenerator:
- name: js-function
files:
- index.js

generatorOptions:
# Keep a stable name: the Workload references `js-function` by name and is
# deployed out-of-band via `datumctl compute deploy`, not through Kustomize, so
# Kustomize cannot rewrite the reference to a hashed name. A content-hash suffix
# would therefore break the mount and buys us nothing here (the instance is
# recreated by `datumctl compute restart`, not by a ConfigMap name change).
disableNameSuffixHash: true
60 changes: 60 additions & 0 deletions examples/serverless-js-configmap/workload.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Datum Cloud Workload: serverless Node.js function via ConfigMap mount.
#
# The image is the GENERIC runtime (node + libs, no code). The function arrives
# by mounting the `js-function` ConfigMap at /app, so /app/index.js is the file
# the Kraftfile's cmd runs. Swap the function by editing the ConfigMap and
# restarting -- no rebuild of this image.
#
# Field shapes (validated against examples/config-secret-probe on this branch):
# - spec.template.spec.volumes[]: {name, configMap:{name}} -> mounted as a dir
# - container.volumeAttachments[]: {name, mountPath} -> k8s VolumeMount
# - instanceType lives at runtime.resources (NOT container resources; the
# validating webhook rejects container-level resources on this branch).
#
# Apply the ConfigMap on the PROJECT plane FIRST (see configmap.yaml), then:
# datumctl compute deploy -f workload.yaml -y
#
# NOTE: env is intentionally EMPTY. In a busy namespace, kraftlet-injected k8s
# service-link env can overflow the UKC kernel-cmdline buffer
# (InvalidKernelCommandLine, boot 0.00 ms / standby). The mount needs no env and
# PORT defaults to 8080 in index.js, so we keep env empty to stay under budget.
apiVersion: compute.datumapis.com/v1alpha
kind: Workload
metadata:
name: serverless-js-fn
labels:
app: serverless-js-fn
spec:
template:
metadata:
labels:
app: serverless-js-fn
spec:
volumes:
- name: fn-vol
configMap:
name: js-function
runtime:
resources:
instanceType: datumcloud/d1-standard-2
sandbox:
containers:
- name: app
image: index.unikraft.io/datum/serverless-js-runtime:latest
volumeAttachments:
- name: fn-vol
mountPath: /app
ports:
- name: http
port: 8080
protocol: TCP
networkInterfaces:
- network:
name: default
placements:
- name: default
cityCodes:
- DFW
scaleSettings:
minReplicas: 1
instanceManagementPolicy: OrderedReady
Loading