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). Build tooling publishes the image; deployment tooling applies that immutable image.
For outbound TLS to private services, see Custom CA certificates. The recommended shared-trust approach uses a custom JDK distribution; per-application truststore mounts currently require a raw Pod or Deployment.
The optional managed-dependency admission integration
provides a policy for gating either form in a disposable evaluation cluster. The
Gatekeeper policy applies to every pod with runtimeClassName: brewlet,
including pods generated by JavaApplication, and requires each image to be
digest-pinned with a trusted final-image attestation. See the
live-validation runbook for source-built enforcement
coverage and fixture-specific settings.
Raw Deployment¶
The image field references the runnable OCI image for the Java application,
not the native Brewlet artifact. The only Brewlet-specific line is
runtimeClassName: brewlet.
Use an immutable reference such as
registry.example.com/demo/hello@sha256:<digest>. The Brewlet shim rejects
tag-only Kubernetes image requests because a tag does not identify the exact
manifest carrying the launch configuration. Managed-dependency admission
enforcement also denies tag-based images earlier.
apiVersion: apps/v1
kind: Deployment
metadata: { name: hello }
spec:
replicas: 1
selector: { matchLabels: { app: hello } }
template:
metadata: { labels: { app: hello } }
spec:
runtimeClassName: brewlet
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile: { type: RuntimeDefault }
containers:
- name: hello
image: registry.example.com/demo/hello@sha256:REPLACE_WITH_IMAGE_DIGEST
securityContext:
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
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 execwork;- readiness/liveness/startup probes (
httpGet,tcpSocket,exec) work; - CPU HPA configuration is available; see autoscaling requirements and validation limits.
Ordinary-image ephemeral debug containers are not supported by the current
Brewlet handler. Use kubectl exec with tools present in the selected runtime,
or a separate ordinary-runtime Pod for debugging.
Shells and OS utilities are supplied by the selected JDK source image's
userland, not by the host OS; minimal or distroless sources may omit them.
See Where shells and diagnostic tools come from.
The Pod securityContext is the sole source of process UID/GID for raw
workloads. Brewlet preserves the identity CRI places in the OCI spec; artifact
launch metadata cannot override it.
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 and valid NodeProfiles, injects nodeAffinity,
and the shim reads the propagated JDK/launcher annotations at launch while still
resolving the workload image digest from containerd metadata. A bare
brewlet.sh/jdk: "21" accepts any distribution of that feature; the shim picks
the lexicographically first active, compatible inventory name on the selected
node. If the annotation is absent, the shim defaults to feature 21 and uses the
same selection rule. Pin a name such as "temurin-21" or "temurin-canary-21"
when the choice matters (see JDK selection).
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@sha256:REPLACE_WITH_IMAGE_DIGEST
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 and NodeProfiles; injects nodeAffinity. If none compatible → pod rejected with NoCompatibleJDK; if only a NodeProfile provides it (pool at zero) → admitted with a warning and stays Pending until scale-up. |
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 → admitted with a warning and stays Pending until one is provisioned. Omit for arch-neutral bytecode. |
brewlet.sh/artifact-container |
container name | Selects which regular container's image the webhook mirrors into Pod-wide compatibility hints. The webhook normalizes this value to the selected container name; other tasks ignore the shared hints and resolve their own CRI image independently. |
If you set no annotation, the pod is admitted (the webhook still overwrites its compatibility hints) and the shim performs its own runtime identity and JDK compatibility checks. 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
Generate a development manifest¶
Recommended for Maven development: let brewlet:manifest generate the
JavaApplication rather than copying YAML or substituting an image digest.
Declare the plugin first and
configure your application's ports, health probes and runtime defaults as shown
in the developer workshop.
mvn package brewlet:push brewlet:manifest \
-Dbrewlet.registry=registry.example.com/team -Dbrewlet.appName=hello
Review target/brewlet/javaapplication.yaml, then apply it and wait separately:
kubectl apply -f target/brewlet/javaapplication.yaml
brewlet k8s app wait hello --namespace default --wait-timeout 5m
Generation uses this invocation's actual built image digest. It accepts no
independent image input and cannot consume an earlier push.json. Repeat the
publish-and-generate command after an application change, then apply the
regenerated file; no manual image update is needed. Keep development defaults
in the plugin configuration, since regeneration overwrites the YAML.
With brewlet:build instead of brewlet:push, generation does not publish;
make the exact built image available to the cluster before applying.
Generation itself neither deploys nor waits. See the
development manifest options.
Minimal example¶
This illustrates the resource schema; Maven developers can generate their own file using the preceding commands.
apiVersion: apps.brewlet.sh/v1alpha1
kind: JavaApplication
metadata: { name: hello }
spec:
artifact: { image: registry.example.com/demo/hello@sha256:REPLACE_WITH_IMAGE_DIGEST }
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1"
memory: "1Gi"
ports: [{ name: http, containerPort: 8080 }]
The generated Deployment defaults to runAsNonRoot: true with UID/GID
65532:65532, RuntimeDefault seccomp, privilege escalation disabled, and all
Linux capabilities dropped.
Deploy a published image¶
For CI and production, build and publication are separate from deployment. Maven projects publish a runnable image and a machine-readable handoff without requiring cluster access:
Alternatively, use brewlet push app.jar registry.example.com/team/app:1
--push-result push.json. Both publishers record image, digest, deployImage
and format. Only format=image is runnable by Kubernetes.
In the deployment stage, set spec.artifact.image in your reviewed
JavaApplication manifest to the deployImage from that handoff. Keep namespace,
JDK selection, replicas, ports, resources and probes in production deployment configuration,
not the POM. For example, starting from the minimal hello.yaml above (requires
jq and an existing payments namespace):
DEPLOY_IMAGE="$(jq -er 'select(.format == "image") | .deployImage' target/brewlet/push.json)" || exit 1
kubectl patch --local -f hello.yaml --type merge \
-p "{\"spec\":{\"artifact\":{\"image\":\"$DEPLOY_IMAGE\"}}}" \
-o yaml > hello-release.yaml
kubectl apply -n payments -f hello-release.yaml
brewlet k8s app wait hello --namespace payments --wait-timeout 5m
kubectl and the optional Brewlet CLI use your kubeconfig; select the same
context for both when overriding it. Helm or GitOps can perform the deployment
instead. Promote the same digest between environments without rebuilding or
republishing. See the plugin reference
for publishing and deployment handoff details.
Full example¶
apiVersion: apps.brewlet.sh/v1alpha1
kind: JavaApplication
metadata:
name: orders-api
namespace: payments
spec:
artifact:
image: registry.example.com/team/orders@sha256:REPLACE_WITH_IMAGE_DIGEST
pullPolicy: IfNotPresent
pullSecrets: [regcred]
replicas: 3
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { cpu: "2", memory: "1Gi" }
jvm:
version: 21 # JDK feature version
distribution: temurin # selects the node's temurin-21 inventory entry
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 runnable OCI image ref + pull policy/secrets. |
replicas / autoscaling |
Deployment replica count / HPA. |
resources |
Requests → scheduling/HPA; limits → sandbox cgroup ceilings (Resource requests, limits & JVM tuning). |
jvm.version |
JDK feature version to run on (e.g. 21); must match a node-installed JDK. |
jvm.distribution |
Optional administrator-defined inventory name (temurin, microsoft, temurin-canary). With jvm.version selects an exact <distribution>-<feature> entry; omit to choose the lexicographically first active, compatible entry of that feature on each node. See JDK selection. |
jvm.launcher |
java (default) or jaz (Launchers). |
jvm.args |
Your JVM tuning flags, delivered as argv via the brewlet.sh/jvm-args pod annotation and applied after the artifact's own launch knobs (so they win on conflict). May not select the entrypoint (-jar, -cp, -p, -m, @argfile). Omit under jaz. |
jvm.cds.regenerate |
Request node-side AppCDS regeneration (AppCDS §4.3). Default false. Requires an otherwise-compatible ready node or NodeProfile whose spec.appCDS.regenerationEnabled is true; otherwise admission denies with AppCDSRegenerationDisabled, and the shim independently enforces the host policy. The private cache key includes the trusted namespace, the verified platform manifest derived from the CRI/containerd-resolved image, the JDK build, and the CRI process UID. Any shipped cds.archive becomes optional seed data. |
arch |
Optional architecture constraint (amd64, arm64). Only for non-portable JARs bundling JNI native libraries; steers scheduling to matching-arch nodes; with no ready node of that arch, the pod is admitted with a warning and stays Pending. 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.
JDK selection¶
A pool can install multiple JDKs of the same feature version. Set
spec.jvm.distribution to the name the platform team registered in
NodeProfile.spec.jdks[].distribution, together with spec.jvm.version.
For example, version: 21 and distribution: temurin-canary select
temurin-canary-21, without falling back to another distribution.
Leaving distribution unset means any compatible distribution, not "stable"
or "latest". Each node chooses its lexicographically first active, compatible
inventory entry for the requested feature. Adding temurin-canary-21 alongside
temurin-stable-21 therefore changes the selection for future version-only
launches to the canary. Replicas may choose differently on nodes with different
inventories; pin the distribution when that is undesirable.
There is no patch/build selector in spec.jvm: version is an integer feature
version, and the platform team's inventory entry pins the source-image digest.
See Multiple JDKs of the same feature version
for installation and application examples, including two builds from one vendor.
Rollout readiness¶
Ready=True with reason Reconciled means the controller has observed the
current Deployment generation, with exactly the desired number of updated,
ready, and available replicas and no extra active or unavailable replicas.
Old healthy replicas do not make a failed replacement rollout ready.
Ready=False/Progressing includes rollout counts or the Deployment's
progress-failure diagnostic; it does not necessarily mean the old Pods stopped
serving traffic.
readyReplicas remains the Deployment's observed ready count, which can include
old replicas. Do not use that number alone to decide whether a new revision
succeeded. Status is asynchronous: require the Ready condition's
observedGeneration to match the JavaApplication's metadata.generation.
The controller reads Deployment status directly after reconciliation and does
not certify a cached previous revision or a concurrently changed pod template.
With autoscaling enabled, readiness uses the live Deployment replica target
owned by the HPA, not spec.replicas on the JavaApplication. A completed
scale-to-zero operation may be Ready with zero replicas; that means the desired
state was reached, not that a serving endpoint exists.
selectedJdk describes the requested JDK selector, not a measurement of each
running JVM's distribution or patch version.
Deployment configuration and health probes¶
Neither a configured port nor the application's framework establishes an HTTP health
contract. In particular, a healthy application may return 404 at /; a
generated liveness probe must not restart it for that.
Declare probes that match endpoints or commands the application actually
provides in the JavaApplication deployment configuration:
spec:
probes:
readiness:
httpGet: { path: /actuator/health/readiness, port: 8080 }
liveness:
httpGet: { path: /actuator/health/liveness, port: 8080 }
For a development manifest generated by brewlet:manifest, configure the
plugin's <ports> and <probes> in the POM; the goal translates them into
this YAML. See the Maven development manifest configuration
for the inputs. Those values are not embedded in the image and do not update a
running application. For production, maintain probes and other deployment
settings in your deployment repository or Helm/GitOps configuration instead.
Use httpGet, exec, or tcpSocket with the application's actual health
contract. For Spring Boot Actuator, enable the health endpoints shown above.
Without a readiness probe, Kubernetes does not wait for application-specific
readiness.
Set spec.jvm.version, optional spec.jvm.distribution, and spec.jvm.launcher
in the deployment manifest to request a compatible node runtime. Maven's
brewlet.jdkFeature checks managed-dependency compatibility and supplies the
runtime feature in the optional development manifest. It never changes an
already-deployed workload or provisions a node JDK.
Environment references and resource ownership¶
spec.env supports literal value entries and Kubernetes valueFrom references
to Secrets, ConfigMaps, pod fields, and container resources. References remain
references in the generated Deployment; Brewlet does not read or embed secret
values:
spec:
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: database
key: password
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
Environment references require the JavaApplication CRD matching the installed components. For release changes, follow safe teardown/reinstallation.
Disabling service.enabled (or removing all ports) and disabling autoscaling
remove only the corresponding resources controlled by the current
JavaApplication. Unowned resources, resources controlled by another application,
and resources belonging to an older same-name application are not deleted.
Autoscaling¶
Preview: evaluate only in a disposable cluster
Source-built live tests exercise real CPU-driven scale-up/down, Ready Pods, Service endpoints and HPA ownership. The GC scenario covers verified warm reuse, not cold startup with missing source layers. See the runbook and evidence for the tested revision and fixture settings. These results are not validation of published release artifacts or production certification.
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.
Brewlet's runtime exporter is a separate telemetry path; enabling it does not provide the Kubernetes resource metrics required by CPU HPA.
Disabling autoscaling resumes Brewlet's replica ownership; it does not safely
hand a JavaApplication to an external HPA. For advanced scaling, use a separately
managed ordinary Deployment with runtimeClassName: brewlet.
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 requests, limits & JVM tuning — get heap/GC/CPU right.
- Launchers —
javavsjaz. - Observability & day‑2 — logs, metrics, probes, upgrades.
- Troubleshooting — when a pod won't schedule/start.