Skip to content

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:

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 }]
kubectl apply -f my-deployment.yaml
kubectl get pods -l app=hello
kubectl logs -l app=hello

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 work;
  • 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.yaml and the reconciling controller (JavaApplicationReconciler, SPECIFICATION §8.2) runs in the operator: apply a JavaApplication and it manages the Deployment (+ Service, + optional HPA) for you, garbage-collecting them when the descriptor is deleted. The kubernetes/charts/brewlet Helm 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:

mvn package brewlet:push -Dbrewlet.registry=registry.example.com/team

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 replicas so the HPA is the sole owner of the replica count (no fighting between the two controllers).
  • Flip enabled back to false and the managed HPA is deleted and the Deployment returns to spec.replicas (default 1).
  • The HPA is owned via a controller reference, so it is garbage-collected when the JavaApplication is 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