Auto-Instrumenting Java Apps with Kubernetes Image Volumes
Auto-Instrumenting Java Apps with Kubernetes Image Volumes
The Kubernetes v1.33 release marks an important milestone with the Image Volumes feature graduating to beta status. This feature allows you to mount container images directly as read-only volumes in your Kubernetes pods, creating exciting new possibilities for software delivery patterns.
In this blog post, I'll walk through a practical implementation of this feature using Kind (Kubernetes in Docker) to demonstrate how to auto-instrument a Java application with OpenTelemetry without modifying the application container itself.
📁 Complete Code Available: All the code and configuration files for this tutorial are available in the k8s-oci-volume-source-demo GitHub repository. You can clone it and follow along or use it as a reference.
What are Image Volumes?
Image Volumes were introduced as an alpha feature in Kubernetes v1.31 as part of
KEP-4639.
With v1.33, they have graduated to beta, adding support for subPath and subPathExpr mounts, along with new
metrics for tracking image volume usage.
This feature allows you to reference container images as volumes in Kubernetes pods, giving you direct access to the container image's filesystem. The volumes are mounted read-only, maintaining security and immutability.
Note: The feature is still disabled by default, as not all container runtimes fully support it yet. Containerd v2.1.0 supports this feature, which is what we'll be using in this tutorial.
Why Image Volumes Matter
This feature can revolutionize how we deliver and manage certain types of content in Kubernetes:
- Separate content from application containers - Keep your application containers lean while mounting heavy dependencies separately
- Agent distribution - Distribute monitoring agents without modifying application images
- Simplified versioning - Update content independently from application code
- Reduced deployment complexity - Avoid custom init containers and sidecars
In our example, we'll use Image Volumes to mount an OpenTelemetry Java agent into a Spring Boot application without embedding it in the application container.
Prerequisites
To follow along with this tutorial, you'll need:
- Docker installed on your machine
gitfor cloning repositoriescurlfor downloading fileskubectlfor interacting with Kubernetes- Basic understanding of Kubernetes concepts
Getting Started
Clone the demo repository to get started:
The repository contains everything needed to run the complete demo, including:
- Setup scripts:
01-build-custom-kind-image.sh,02-kind-with-registry.sh - OCI artifact creation:
03-artifact-javaagent-upload.sh - Application deployments:
04-deploy-spring-hello-world.sh,04a-deploy-aspire-dashboard.sh - Kubernetes manifests:
spring-hello-world/,aspire-dashboard/ - Cleanup script:
05-cleanup.sh
Step 1: Building a Custom Kind Cluster with Image Volumes Support
First, we need to build a custom Kind node image with a sufficiently recent version of containerd (v2.1.0) that supports the Image Volumes feature.
The complete script is available as 01-build-custom-kind-image.sh
in the repository:
#!/usr/bin/env bash
# Working directory
WORKDIR=
KIND_REPO="kind"
K8S_TAR="kubernetes-server-linux-amd64.tar.gz"
K8S_VERSION="v1.33.0"
CONTAINERD_VERSION="v2.1.0"
TAG="oci-source-demo"
# Step 1: Verify if kind repo is already cloned, if not clone it
if [; then
else
fi
# Step 2: Build the base image with custom containerd version
# Return to the working directory
# Step 3: Check if kubernetes tarball exists, if not download it
if [; then
else
fi
# Step 4: Build the node image using the custom base image
This script performs the following operations:
- Clones the Kind repository and checks out version v0.27.0
- Builds a custom base image with containerd v2.1.0
- Downloads the Kubernetes v1.33.0 server tarball if needed
- Builds a Kind node image using the custom base image and Kubernetes binaries
Step 2: Creating a Kind Cluster with a Local Registry
Next, we'll create a Kind cluster with our custom image and set up a local registry for our OCI artifacts using the
02-kind-with-registry.sh script:
#!/usr/bin/env bash
# 1. Create registry container unless it already exists
reg_name='kind-registry'
reg_port='5001'
if [; then
# Wait for the registry to be ready
while ; do
done
else
fi
# 2. Create kind cluster with containerd registry config dir enabled
# 3. Add the registry config to the nodes
REGISTRY_DIR="/etc/containerd/certs.d/localhost:"
for; do
done
# 4. Connect the registry to the cluster network if not already connected
if [; then
fi
# 5. Document the local registry
This script:
- Creates a local Docker registry container
- Creates a Kind cluster with the custom node image and enables the
ImageVolumefeature gate - Configures containerd in each node to use the local registry
- Connects the registry to the Kind network
- Creates a ConfigMap to document the local registry
Step 3: Creating an OCI Artifact for the OpenTelemetry Java Agent
Now we'll package the OpenTelemetry Java agent
as an OCI artifact and push it to our local registry using the
03-artifact-javaagent-upload.sh script:
#!/usr/bin/env bash
# Create a temporary directory for the OCI image
TMP_DIR=""
# Constants
GITHUB_REPO="open-telemetry/opentelemetry-java-instrumentation"
AGENT_VERSION="v2.15.0"
AGENT_URL="https://github.com//releases/download//opentelemetry-javaagent.jar"
AGENT_JAR_FILE="opentelemetry-javaagent.jar"
AGENT_DIR="/opentelemetry-javaagent"
AGENT_FILE="/"
AGENT_FILE_DOWNLOAD_HEADERS="/.curl.headers"
REGISTRY="localhost:5001"
ARTIFACT_NAME="opentelemetry-javaagent"
# Create agent directory if it doesn't exist
# Extract the Last-Modified date from headers
AGENT_PUBLISH_DATE=""
# Set the file's modification time to match the server's Last-Modified time
if TOUCH_DATE= && [; then
else
fi
TAR_LAYER_PATH="/layer.tar"
# Create a tar layer from the agent directory and make it reproducible
# Calculate layer diff
TAR_LAYER_DIFF=""
# Compress layer and make it reproducible
# Create config
CONFIG_PATH="/config.json"
# Create layout
LAYOUT_REF="/layout:latest"
# Create OCI image with annotations
IMAGE_CREATED_DATE=
(; )
# Push image to local registry
# Clean up the temporary directory
This script:
- Downloads the OpenTelemetry Java agent JAR file from the official GitHub releases
- Creates a reproducible OCI image layer containing the agent using ORAS (OCI Registry As Storage)
- Configures the OCI image with appropriate metadata
- Pushes the image to our local registry
Step 4: Deploying a Java Application with Image Volume Mount
Now we'll deploy a Spring Boot application that mounts the OpenTelemetry agent
from the OCI image using the 04-deploy-spring-hello-world.sh script:
#!/usr/bin/env bash
# Constants
NAMESPACE="spring-hello-world"
# Create namespace if it doesn't exist, or delete and recreate it
if ; then
fi
# Get the NodePort for Spring Hello World
SPRING_NODE_PORT=
# Get the node IP from the Spring Hello World pod
NODE_IP=
The deployment YAML for this application includes the critical Image Volume configuration. You can find the complete
Kubernetes manifests in the
spring-hello-world directory:
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-hello-world
namespace: spring-hello-world
spec:
replicas: 1
selector:
matchLabels:
app: spring-hello-world
template:
metadata:
labels:
app: spring-hello-world
spec:
containers:
- name: spring-hello-world
image: springio/hello-world:0.0.1-SNAPSHOT
ports:
- name: http
containerPort: 8080
protocol: TCP
env:
- name: JAVA_TOOL_OPTIONS
value: -javaagent:/mnt/javaagent/opentelemetry-javaagent.jar
- name: OTEL_SERVICE_NAME
value: spring-hello-world
- name: OTEL_METRICS_EXPORTER
value: otlp
- name: OTEL_LOGS_EXPORTER
value: otlp
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: http://aspire-dashboard-service.aspire-dashboard:4317
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumeMounts:
- name: otel-agent
mountPath: /mnt/javaagent
readOnly: true
volumes:
- name: otel-agent
image:
reference: localhost:5001/opentelemetry-javaagent:v2.15.0
Note the key sections:
volumessection defines an image volume referencing our OpenTelemetry agent OCI imagevolumeMountsmounts this image volume at/mnt/javaagentin the containerJAVA_TOOL_OPTIONSenvironment variable configures Java to use the agent from the mounted path - see the JVM documentation for more details
Step 5: Deploying the Aspire Dashboard for Observability
Finally, we'll deploy the .NET Aspire Dashboard
to visualize the telemetry data collected by the OpenTelemetry agent using the
04a-deploy-aspire-dashboard.sh script:
#!/usr/bin/env bash
# Constants
NAMESPACE="aspire-dashboard"
# Create namespace if it doesn't exist, or delete and recreate it
if ; then
fi
# Get the NodePort for Aspire Dashboard
ASPIRE_NODE_PORT=
# Get the node IP from the Aspire Dashboard pod
NODE_IP=
Benefits of Using Image Volumes for Auto-Instrumentation
This approach offers several significant advantages:
- Separation of Concerns: The instrumentation agent is completely decoupled from the application container
- Immutability: The agent is delivered as an immutable OCI artifact
- Versioning: The agent can be updated independently of the application
- Consistency: All applications can use the same agent without duplicating it in each image
- Zero Application Changes: No need to modify application Dockerfiles or rebuild images
Conclusion
Kubernetes v1.33's Image Volumes beta feature provides a powerful new way to manage content delivery to containers. By leveraging this feature for auto-instrumentation, we've demonstrated a clean, efficient approach to adding observability to applications without modifying their container images.
This pattern can be extended to other use cases such as:
- Mounting configuration files or scripts
- Adding shared libraries or dependencies
- Distributing ML models to inference containers
- Sharing static assets across multiple applications
As container runtimes continue to improve their support for this feature, we can expect to see widespread adoption of these patterns in production Kubernetes environments.
Further Reading
- Complete Demo Repository — All the code and configuration files used in this tutorial
- Kubernetes v1.33 Image Volumes Beta Announcement
- Using OCI Volume Source in Kubernetes Pods
- OpenTelemetry Java Instrumentation
- Kind: Kubernetes in Docker
- OCI Image Specification
- Containerd Documentation
- ORAS CLI Documentation