Deploying workloads¶
Once Brewlet is installed on your cluster (Installation) and your
application is published (Building & publishing), deploying is
just standard Kubernetes with one extra line: runtimeClassName: brewlet.
Two ways to deploy:
- Raw
Deployment/Pod— works today, nothing but the RuntimeClass. JavaApplicationCRD — a higher-level descriptor; the controller reconciles it into aDeployment(+Service, + optionalHPA).
Raw Deployment¶
The image field references an OCI artifact containing a Java application, not a
container image. The only
Brewlet-specific line is runtimeClassName: brewlet.
apiVersion: apps/v1
kind: Deployment
metadata: { name: hello }
spec:
replicas: 1
selector: { matchLabels: { app: hello } }
template:
metadata: { labels: { app: hello } }
spec:
runtimeClassName: brewlet
containers:
- name: hello
image: registry.example.com/demo/hello:1.0.0 # the OCI artifact
resources:
limits: { cpu: "1", memory: "512Mi" } # → cgroup limits
ports: [{ containerPort: 8080 }]
Because the shim is runc-backed, this pod is a first-class Kubernetes citizen:
- real pod IP via CNI → Services/Ingress/NetworkPolicy work;
kubectl logs/kubectl exec/ ephemeral debug containers work;- readiness/liveness/startup probes (
httpGet,tcpSocket,exec) work; - HPA and metrics-server work.
Add a Service exactly as usual:
apiVersion: v1
kind: Service
metadata: { name: hello }
spec:
selector: { app: hello }
ports: [{ name: http, port: 80, targetPort: 8080 }]
Requesting a specific JDK or launcher¶
For raw Kubernetes workloads, request the JDK/launcher with pod annotations. The
admission webhook validates them against the ready fleet, injects nodeAffinity,
and the shim reads the same propagated annotations at launch. If brewlet.sh/jdk
is absent, the shim defaults to feature 21 and picks the lexically-first
installed distribution for it; omit brewlet.sh/launcher for
vanilla java.
apiVersion: apps/v1
kind: Deployment
metadata: { name: hello }
spec:
template:
metadata:
annotations:
brewlet.sh/jdk: "21" # bare feature (any distro) or "temurin-21"
brewlet.sh/launcher: "jaz" # optional; omit/"java" = vanilla launcher
spec:
runtimeClassName: brewlet
containers:
- name: hello
image: registry.example.com/demo/hello:1.0.0
resources: { limits: { cpu: "2", memory: "1Gi" } }
| Annotation | Values | Effect |
|---|---|---|
brewlet.sh/jdk |
21 (any distro of that feature) or temurin-21 (exact) |
Validated against ready nodes; injects nodeAffinity. If none compatible → pod rejected with NoCompatibleJDK. |
brewlet.sh/launcher |
jaz, or empty/java |
Same, but for launchers → NoCompatibleLauncher. |
brewlet.sh/arch |
amd64, or amd64,arm64 |
Optional; only for non-portable JARs bundling JNI natives. Injects kubernetes.io/arch nodeAffinity; if no ready node of a required arch exists → NoCompatibleArch. Omit for arch-neutral bytecode. |
brewlet.sh/artifact-container |
container name | Which container's image is the OCI artifact (defaults to the brewlet container). |
If you set no annotation, the pod is admitted (its artifact ref/digest are still stamped) and the shim performs its own runtime JDK compatibility check. See Launchers and Troubleshooting.
JavaApplication CRD¶
The JavaApplication CRD is the developer-facing "deployment descriptor" — a single
manifest that a controller reconciles into a Deployment (+ Service, + optional
HPA) with runtimeClassName: brewlet wired in.
Status. The CRD ships in
deploy/javaapplication-crd.yamland the reconciling controller (JavaApplicationReconciler, SPECIFICATION §8.2) runs in the operator: apply aJavaApplicationand it manages theDeployment(+Service, + optionalHPA) for you, garbage-collecting them when the descriptor is deleted. Thekubernetes/charts/brewletHelm chart installs the CRD automatically.
kubectl apply -f kubernetes/deploy/javaapplication-crd.yaml
kubectl apply -f kubernetes/deploy/sample-javaapplication.yaml
Minimal example¶
apiVersion: apps.brewlet.sh/v1alpha1
kind: JavaApplication
metadata: { name: hello }
spec:
artifact: { image: registry.example.com/demo/hello:1.0.0 }
resources:
limits: { cpu: "1", memory: "512Mi" }
ports: [{ name: http, containerPort: 8080 }]
Full example¶
apiVersion: apps.brewlet.sh/v1alpha1
kind: JavaApplication
metadata:
name: orders-api
namespace: payments
spec:
artifact:
image: registry.example.com/team/orders:1.4.2 # digest-pinned recommended
pullPolicy: IfNotPresent
pullSecrets: [regcred]
replicas: 3
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "2", memory: "1Gi" }
jvm:
version: 21 # JDK feature version
launcher: java # vanilla OpenJDK (default); or "jaz"
args: # YOUR tuning — Brewlet injects none
- "-XX:MaxRAMPercentage=75.0"
- "-XX:+UseZGC"
- "-XX:+ExitOnOutOfMemoryError"
cds:
regenerate: true # opt into node-side AppCDS regeneration (fleet choice)
env:
- name: SPRING_PROFILES_ACTIVE
value: prod
ports:
- name: http
containerPort: 8080
service:
enabled: true
type: ClusterIP
probes:
readiness: { httpGet: { path: /actuator/health/readiness, port: 8080 } }
liveness: { httpGet: { path: /actuator/health/liveness, port: 8080 } }
autoscaling:
enabled: true
minReplicas: 3
maxReplicas: 10
targetCPUUtilizationPercentage: 70
arch: [amd64] # optional; only for non-portable JARs (JNI natives)
| Field group | Purpose |
|---|---|
artifact |
The OCI artifact ref + pull policy/secrets. |
replicas / autoscaling |
Deployment replica count / HPA. |
resources |
Requests/limits → sandbox cgroup (Resource tuning). |
jvm.version |
JDK feature version to run on (e.g. 21); must match a node-installed JDK. |
jvm.distribution |
Optional JDK distribution (temurin, microsoft). With jvm.version pins an exact <distribution>-<feature> node JDK; omit to accept any distribution of that feature. |
jvm.launcher |
java (default) or jaz (Launchers). |
jvm.args |
Your JVM tuning flags. Omit under jaz. |
jvm.cds.regenerate |
Opt into node-side AppCDS regeneration (AppCDS §4.3). When true the controller stamps the brewlet.sh/cds-regenerate pod annotation and the node maintains a per-(artifact, JDK-build) archive cache via -XX:+AutoCreateSharedArchive (JDK 19+), self-healing on every central JDK patch. Fleet/operational choice (depends on your JDK patch cadence), so it lives here rather than in the artifact; any shipped cds.archive becomes optional seed data. Default false. |
arch |
Optional architecture constraint (amd64, arm64). Only for non-portable JARs bundling JNI native libraries; steers scheduling to matching-arch nodes and denies admission with NoCompatibleArch when unsatisfiable. Omit for arch-neutral bytecode (runs on any arch). |
env / ports / service / probes |
Wired through to the generated objects. |
The status subresource surfaces readyReplicas, the selectedJdk, and Ready
conditions.
Autoscaling¶
Set spec.autoscaling.enabled: true and the controller manages a
HorizontalPodAutoscaler (autoscaling/v1) targeting the generated Deployment:
spec:
# replicas is ignored while autoscaling is enabled — the HPA owns scaling
autoscaling:
enabled: true
minReplicas: 3 # lower bound
maxReplicas: 10 # upper bound (required)
targetCPUUtilizationPercentage: 70
- While autoscaling is on, the controller does not reconcile the Deployment's
replicasso the HPA is the sole owner of the replica count (no fighting between the two controllers). - Flip
enabledback tofalseand the managed HPA is deleted and the Deployment returns tospec.replicas(default1). - The HPA is owned via a controller reference, so it is garbage-collected when the
JavaApplicationis deleted. - Requires a running metrics-server in the cluster for CPU metrics.
Coexisting with regular containers¶
Brewlet is additive. Only pods that set runtimeClassName: brewlet go through
the shim; every other pod runs on the default runtime unchanged. You can freely mix
Brewlet workloads and ordinary containers in the same namespace and cluster.
Next steps¶
- Resource tuning — get heap/GC/CPU right.
- Launchers —
javavsjaz. - Observability & day‑2 — logs, metrics, probes, upgrades.
- Troubleshooting — when a pod won't schedule/start.