Skip to content

Part 2: Build and deploy a Brewlet workload

Audience: Java application developers.

Goal: build a JAR, publish it as a runnable OCI image, and deploy it to a Brewlet-enabled Kubernetes cluster without creating a Dockerfile or bundling a JDK.

1. Receive the platform handoff

Ask the Ops participant for:

export BREWLET_CONTEXT="<kubernetes-context>"
export BREWLET_NAMESPACE="<developer-namespace>"
export BREWLET_JDK="21"
export BREWLET_VERSION="<version-from-ops-handoff>"
export BREWLET_REGISTRY="<registry-host>/<team>"

Use the concrete version Ops resolved during installation, not the literal latest or a new latest-release lookup. The CLI, example source, Maven plugin, and cluster components must use the same release.

You also need JDK 21+, Maven 3.10+, Git, kubectl, curl, tar, and credentials for your application registry. Clone the example source from the release tag matching the platform handoff:

git clone --depth 1 --branch "v${BREWLET_VERSION}" https://github.com/microsoft/brewlet.git
cd brewlet

If Ops used custom source-built components, use the source revision they supply instead. Brewlet's source and release downloads do not require repository credentials.

The registry repository must be readable by the cluster nodes.

Select the provided context and confirm your access:

kubectl config use-context "$BREWLET_CONTEXT"
kubectl auth can-i create deployments -n "$BREWLET_NAMESPACE"
kubectl auth can-i create javaapplications.apps.brewlet.sh -n "$BREWLET_NAMESPACE"
kubectl get runtimeclass brewlet

Install the released CLI with the checksum-verifying installer:

curl -fsSL https://brewlet.sh/install.sh | sh -s -- --version "$BREWLET_VERSION" --install-dir "$HOME/.local/bin"
export PATH="$HOME/.local/bin:$PATH"
brewlet version

The CLI must report BREWLET_VERSION. For custom source-built components, use the matching source-built CLI instead. Run the same readiness check used by Ops:

brewlet k8s doctor \
  --context "$BREWLET_CONTEXT" \
  --namespace "$BREWLET_NAMESPACE"

If your RBAC permits reading nodes, you can also inspect the available JDKs:

kubectl get nodes -l brewlet.sh/runtime=ready \
  -o custom-columns=NAME:.metadata.name,JDKS:.metadata.annotations.brewlet\\.sh/jdks

2. Build the example JAR

The included dependency-free application exposes /hello, /healthz, and /info.

mvn -f integration-tests/fixtures/demo-app/pom.xml clean package
jar --describe-module \
  --file integration-tests/fixtures/demo-app/target/app.jar

The output is an ordinary executable JAR. It contains neither Linux nor a JDK.

3. Configure and exercise the Maven plugin

Maven downloads the released plugin from Maven Central. No manual installation, plugin source build, GitHub token, or custom repository is needed. Use the same concrete BREWLET_VERSION exported in the Ops handoff; do not resolve a separate latest plugin version.

Add this plugin alongside the existing entries under <build><plugins> in integration-tests/fixtures/demo-app/pom.xml:

<plugin>
  <groupId>sh.brewlet</groupId>
  <artifactId>brewlet-maven-plugin</artifactId>
  <version>${env.BREWLET_VERSION}</version>
  <configuration>
    <appName>hello</appName>
    <namespace>${env.BREWLET_NAMESPACE}</namespace>
    <jdkFeature>${env.BREWLET_JDK}</jdkFeature>
    <ports>
      <port><name>http</name><containerPort>8080</containerPort></port>
    </ports>
    <probes>
      <readiness><path>/healthz</path></readiness>
    </probes>
    <cpuRequest>100m</cpuRequest>
    <cpuLimit>1</cpuLimit>
    <memoryRequest>128Mi</memoryRequest>
    <memoryLimit>256Mi</memoryLimit>
  </configuration>
</plugin>

This enables mvn brewlet:... without binding publishing to the build lifecycle. The plugin configuration supplies development defaults for brewlet:manifest: the namespace and JDK from Ops, port 8080, the application's actual /healthz endpoint, and resource limits. Manifest generation is explicitly invoked and never deploys; no named execution is needed. For reproducible builds outside the workshop, pin the handoff version in your project POM or CI environment. Use a platform release available on Central (starting with 0.5.1). For a just-published release, wait until the matching plugin is available on Central rather than changing the plugin version.

Build a registry-free runnable OCI layout first:

mvn -f integration-tests/fixtures/demo-app/pom.xml \
  brewlet:config brewlet:build \
  -Dbrewlet.image=demo/hello:workshop

test -f integration-tests/fixtures/demo-app/target/brewlet/jvm-config.json
test -f integration-tests/fixtures/demo-app/target/brewlet/oci/index.json

The image layout contains standard OCI layers for amd64 and arm64. Each platform manifest carries the Brewlet launch contract in brewlet.sh/jvm-config; the image does not contain a base image.

4. Publish and deploy

Log in with your normal registry tooling first, for example docker login or az acr login --name <registry>. The plugin reads ~/.docker/config.json, including credential helpers (credsStore/credHelpers) and identity tokens, so no extra environment variables are needed.

Publish the application and generate its development manifest without accessing the cluster:

mvn -f integration-tests/fixtures/demo-app/pom.xml \
  package brewlet:push brewlet:manifest \
  -Dbrewlet.registry="$BREWLET_REGISTRY"

The image defaults to $BREWLET_REGISTRY/<artifactId>:<version>. The plugin pushes it, then manifest writes target/brewlet/javaapplication.yaml using that exact image digest in the same Maven invocation. No copying digests, writing YAML by hand, or passing an image to manifest is needed.

Review the generated file, then apply it and wait using separate tools:

cat integration-tests/fixtures/demo-app/target/brewlet/javaapplication.yaml
kubectl apply --context "$BREWLET_CONTEXT" \
  -f integration-tests/fixtures/demo-app/target/brewlet/javaapplication.yaml
brewlet k8s app wait hello --context "$BREWLET_CONTEXT" \
  --namespace "$BREWLET_NAMESPACE" --wait-timeout 5m

The operator creates the Deployment and ClusterIP Service from this manifest. For production, maintain environment-specific manifests in your deployment repository and promote the same published digest with kubectl, Helm, or GitOps.

The generator always uses this invocation's built image, not an explicit image or an earlier push result. push.json is still available as a CI handoff, but this development loop does not need it. For other ports, health probes or runtime settings, see the development manifest options.

Private registry pull authentication for the nodes can be configured with spec.artifact.pullSecrets; this workshop assumes the nodes can read the selected repository directly.

5. Inspect what was deployed

Watch the higher-level resource and the Kubernetes objects it owns:

kubectl get javaapplication,deployment,pod,service -n "$BREWLET_NAMESPACE"
kubectl logs deployment/hello -n "$BREWLET_NAMESPACE"

The generated pod uses runtimeClassName: brewlet. The node-resident JDK launches the JAR directly under the pod's CPU and memory cgroups.

6. Call the application

In one terminal:

kubectl port-forward service/hello 8080:8080 -n "$BREWLET_NAMESPACE"

In another:

curl http://127.0.0.1:8080/hello
curl http://127.0.0.1:8080/info

The /info response shows the selected Java runtime and the CPU and memory limits observed by the JVM.

7. Make and deploy a change

Change the response in integration-tests/fixtures/demo-app/src/com/example/Hello.java, then run the publish-and-generate command again, then apply the regenerated file:

mvn -f integration-tests/fixtures/demo-app/pom.xml \
  package brewlet:push brewlet:manifest \
  -Dbrewlet.registry="$BREWLET_REGISTRY"
kubectl apply --context "$BREWLET_CONTEXT" \
  -f integration-tests/fixtures/demo-app/target/brewlet/javaapplication.yaml
brewlet k8s app wait hello --context "$BREWLET_CONTEXT" \
  --namespace "$BREWLET_NAMESPACE" --wait-timeout 5m

The new digest is filled in automatically. Kubernetes rolls out the updated image like any other application update. Keep development defaults in the plugin configuration rather than editing the generated file, which is overwritten on regeneration.

Your own applications work the same way: add the plugin with a development configuration, then run mvn package brewlet:push brewlet:manifest. Configure the ports, resources and health endpoints your app actually uses (for example /actuator/health/readiness with Spring Boot Actuator). Keep production deployment configuration separate.

Cleanup

kubectl delete javaapplication hello -n "$BREWLET_NAMESPACE"

This removes the generated Deployment and Service but leaves the shared Brewlet platform intact. See Building and publishing and Deploying workloads for production options.