diff --git a/examples/serverless-js-configmap/.gitignore b/examples/serverless-js-configmap/.gitignore new file mode 100644 index 00000000..9c7b8803 --- /dev/null +++ b/examples/serverless-js-configmap/.gitignore @@ -0,0 +1,2 @@ +# Build artifacts produced locally; not committed. +.unikraft/ diff --git a/examples/serverless-js-configmap/Dockerfile b/examples/serverless-js-configmap/Dockerfile new file mode 100644 index 00000000..ea29502e --- /dev/null +++ b/examples/serverless-js-configmap/Dockerfile @@ -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 diff --git a/examples/serverless-js-configmap/Kraftfile b/examples/serverless-js-configmap/Kraftfile new file mode 100644 index 00000000..2a55dd16 --- /dev/null +++ b/examples/serverless-js-configmap/Kraftfile @@ -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 /v1 --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"] diff --git a/examples/serverless-js-configmap/README.md b/examples/serverless-js-configmap/README.md new file mode 100644 index 00000000..92ea4da2 --- /dev/null +++ b/examples/serverless-js-configmap/README.md @@ -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:/// # -> Hello from a ConfigMap-mounted function — v1 +curl https:///healthz # -> ok + +# UKC instance: confirm the JS mount is a rom at /app and the VM booted: +kraft cloud --metro /v1 --token "$TOKEN" instance get -o json # roms[].at == /app, boot_time_us > 0 +kraft cloud --metro ash-sore-hamster --token "$TOKEN" instance logs # "mount /dev/ukp_romN -> /app" + "serverless-js: vN listening on :8080" +``` + +Derive `` 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:/// # -> ... — 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`. diff --git a/examples/serverless-js-configmap/index.js b/examples/serverless-js-configmap/index.js new file mode 100644 index 00000000..6e462d89 --- /dev/null +++ b/examples/serverless-js-configmap/index.js @@ -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); +}); diff --git a/examples/serverless-js-configmap/kustomization.yaml b/examples/serverless-js-configmap/kustomization.yaml new file mode 100644 index 00000000..cc775990 --- /dev/null +++ b/examples/serverless-js-configmap/kustomization.yaml @@ -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 diff --git a/examples/serverless-js-configmap/workload.yaml b/examples/serverless-js-configmap/workload.yaml new file mode 100644 index 00000000..3f01a08d --- /dev/null +++ b/examples/serverless-js-configmap/workload.yaml @@ -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