LiteFunctions
What - serverless on your cluster: Lambda-shaped, without the vendor
Why - building from first principles — cuz we can 😉
What - serverless on your cluster: Lambda-shaped, without the vendor
Why - building from first principles — cuz we can 😉
helm install litefunctions oci://registry-1.docker.io/ashupednekar535/litefunctions
curl -sS -X POST "https://lambda.us-east-1.amazonaws.com/2015-03-31/functions/Spacelift_Test_Lambda_Function/invocations" \
-H "Content-Type: application/json" \
-d '{"key1":"world"}'
Clients hit the ingestor first—sync calls map to a straight HTTP request/response path.
Async work goes over NATS (our pub/sub): the ingestor stays fast and dumb, publishes off the HTTP hot path, and runtimes subscribe and do the work—so the transport stays abstracted from your function code, same idea as in the lambda FaaS write-up.
The ingestor can also request the operator to warm a function, basically set is_active on the Function CRD to true so reconcile runs.
Req + Produce · subject …exec.lang.reqIdpackage broker
type Req struct {
Project, Name, Lang, ReqId string
}
func Produce(
nc *nats.Conn,
w http.ResponseWriter,
r *http.Request,
lang string,
) (*websocket.Conn, *Req, error) {
// Upgrade; goroutine: ReadMessage → Publish
}
Read r.Body, then write headers and the response body on w in one goroutine—no channels; the “plate” is what you Encode or Write.
package pkg
import (
"encoding/json"
"net/http"
)
func Handle(w http.ResponseWriter, r *http.Request) {
var req map[string]any
_ = json.NewDecoder(r.Body).Decode(&req)
resp := map[string]any{"ok": true, "echo": req}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_ = json.NewEncoder(w).Encode(resp) // response body
}
Chunks arrive on input; you spin a goroutine that sends on out—like plates from the belt.
Runtime / WS can drain out to the wire—decoupled from one HTTP read.
package pkg
func StreamHandler(input <-chan []byte) <-chan []byte {
out := make(chan []byte)
go func() {
defer close(out)
for raw := range input {
c := transform(raw)
out <- c // may block (backpressure)
}
}()
return out
}
func transform(b []byte) []byte { return b }
FROM scratch COPY consumer /consumer USER 65532:65532 ENTRYPOINT ["/consumer"]
Ship small static binaries in distroless or scratch—images stay tiny (often tens of MB, few layers). Each function gets its own dedicated consumer pod that starts fast.
Go wants a single static Linux binary by default—drop-in for scratch. Rust can do the same (--target …-musl / static link), or emit a cdylib (.so) for plugins—then you’re in dynamic-linking / loader land. For these pods we stick to a static bin in both languages so the deploy story matches Go’s “one artifact.”
Scale each function up or down independently without dragging a shared interpreter along.
GOMAXPROCS follows Linux cgroup CPU limits—great in containers, but it breaks old assumptions that GOMAXPROCS tracked the whole host. Re-check load tests and pprof if you sized workers as “all CPUs.”FROM scratch means no libc—for HTTPS you need pure Go TLS (crypto/tls) and bundle CA certs if you verify remotes. Avoid CGO wrappers (e.g. libgit2): we used go-git with a billy-style filesystem so Git stays pure Go and the binary stays static-friendly. Same idea for any dep: prefer stdlib / pure Go over .so shims.FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY runtime/ ./runtime/ COPY functions/ ./functions/ USER 65532:65532 CMD ["python", "-m", "runtime"]
Python / JS share a long-lived runtime per project: pull sources from VCS, then hot-reload into the running process—iteration without a full image rebuild every time.
Tradeoff: larger images (hundreds of MB, many layers) and slower cold paths than static binaries—balanced by that reload loop.
importlib + sys.modules—stale modules after hot swap; invalidate deliberately. GIL doesn’t fix races on shared globals across concurrent requests. JS (Node): require cache—bust or version modules on reload; one event loop, but shared mutable state still bites under load.
Function CRD.package controller
type FunctionReconciler struct {
client.Client
Scheme *runtime.Scheme
}
func (r *FunctionReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
// Get Function; early-exit on NotFound
// If !IsActive: maybe delete Deployment+Service (skip if shared runtime still in use)
// If active: NewDeployment / NewService; SetControllerReference; Create or Update
// Patch Function.Spec.DeProvisionTime (keep-warm)
}