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:
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:
In another:
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¶
This removes the generated Deployment and Service but leaves the shared Brewlet platform intact. See Building and publishing and Deploying workloads for production options.