Knative Serverless on Kubernetes for Java Applications

Knative is a Kubernetes-based platform for deploying and managing serverless workloads. It provides auto-scaling (including to zero), request-driven computation, and simplified deployment abstractions for Java applications.


Knative Core Components

  1. Serving - Request-driven compute, auto-scaling, routing
  2. Eventing - Event-driven architecture with event sources and brokers
  3. Build (deprecated in favor of Tekton) - Pipeline-based builds

Architecture Overview

HTTP Request → Knative Ingress → Activator → Autoscaler → Java Application Pod
↓
Configuration → Revision → Route

Prerequisites

  • Kubernetes cluster (1.15+)
  • kubectl and kn CLI
  • Istio, Contour, or Kourier as network layer
  • Java 11+ application

1. Knative Installation

Install Knative Serving

# Install Knative Serving CRDs
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.10.0/serving-crds.yaml
# Install Knative Serving Core
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.10.0/serving-core.yaml
# Install networking layer (Istio example)
kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.10.0/istio.yaml
kubectl apply -f https://github.com/knative/net-istio/releases/download/knative-v1.10.0/net-istio.yaml
# Verify installation
kubectl get pods -n knative-serving

Install Knative CLI (kn)

# Linux
curl -Lo kn-linux-amd64 https://github.com/knative/client/releases/download/knative-v1.10.0/kn-linux-amd64
sudo install kn-linux-amd64 /usr/local/bin/kn
# Verify
kn version

2. Java Application Setup

Spring Boot Knative Application

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>knative-java-app</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.0</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2022.0.3</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.3.1</version>
<configuration>
<to>
<image>myregistry/knative-java-app:${project.version}</image>
</to>
</configuration>
</plugin>
</plugins>
</build>
</project>

Application.java

package com.example.knative;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
@RestController
@RequestMapping("/api")
class KnativeController {
private final AtomicLong requestCount = new AtomicLong(0);
private final Map<String, String> dataStore = new ConcurrentHashMap<>();
@GetMapping("/hello")
public Map<String, Object> hello() {
long count = requestCount.incrementAndGet();
return Map.of(
"message", "Hello from Knative Java App!",
"timestamp", LocalDateTime.now().toString(),
"requestCount", count,
"instanceId", System.getenv().getOrDefault("HOSTNAME", "unknown"),
"version", "1.0.0"
);
}
@PostMapping("/data")
public Map<String, Object> storeData(@RequestBody Map<String, String> data) {
String key = data.get("key");
String value = data.get("value");
if (key != null && value != null) {
dataStore.put(key, value);
return Map.of(
"status", "success",
"key", key,
"value", value
);
} else {
return Map.of("status", "error", "message", "Key and value required");
}
}
@GetMapping("/data/{key}")
public Map<String, Object> getData(@PathVariable String key) {
String value = dataStore.get(key);
if (value != null) {
return Map.of("key", key, "value", value);
} else {
return Map.of("status", "not_found");
}
}
@GetMapping("/health")
public Map<String, String> health() {
return Map.of("status", "healthy", "timestamp", LocalDateTime.now().toString());
}
@GetMapping("/metrics/info")
public Map<String, Object> metrics() {
return Map.of(
"requestCount", requestCount.get(),
"dataStoreSize", dataStore.size(),
"memoryUsage", Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory(),
"maxMemory", Runtime.getRuntime().maxMemory()
);
}
}
@Component
class StartupPreloader {
@EventListener
public void onApplicationEvent(org.springframework.boot.context.event.ApplicationReadyEvent event) {
System.out.println("🚀 Knative Java Application Started Successfully!");
System.out.println("✅ Environment: " + System.getenv().getOrDefault("KNATIVE_ENV", "default"));
System.out.println("✅ Java Version: " + System.getProperty("java.version"));
System.out.println("✅ Available Processors: " + Runtime.getRuntime().availableProcessors());
}
}

3. Knative Service Configuration

Basic Knative Service

service.yaml

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: java-knative-service
namespace: default
spec:
template:
metadata:
annotations:
# Auto-scaling configuration
autoscaling.knative.dev/target: "10"
autoscaling.knative.dev/target-utilization-percentage: "70"
autoscaling.knative.dev/window: "60s"
# Enable graceful shutdown
features.knative.dev/grace-termination: "enabled"
spec:
containers:
- image: myregistry/knative-java-app:1.0.0
name: java-app
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
- name: SPRING_PROFILES_ACTIVE
value: "knative"
- name: KNATIVE_ENV
value: "production"
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 20
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
traffic:
- latestRevision: true
percent: 100

Deploy with kn CLI

# Deploy using kn CLI
kn service create java-knative-service \
--image myregistry/knative-java-app:1.0.0 \
--port 8080 \
--env JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0" \
--env SPRING_PROFILES_ACTIVE=knative \
--request memory=256Mi,cpu=100m \
--limit memory=512Mi,cpu=500m \
--annotation autoscaling.knative.dev/target=10
# Verify deployment
kn service list
kn revision list
kn route list

4. Advanced Knative Features

Blue-Green Deployment

blue-green-service.yaml

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: java-app-blue-green
spec:
template:
metadata:
name: java-app-v2
spec:
containers:
- image: myregistry/knative-java-app:2.0.0
env:
- name: APP_VERSION
value: "2.0.0"
traffic:
- tag: current
revisionName: java-app-v1
percent: 50
- tag: candidate
revisionName: java-app-v2
percent: 50
- tag: latest
latestRevision: true
percent: 0

Using kn CLI for Traffic Management

# Split traffic between revisions
kn service update java-knative-service \
--traffic java-knative-service-00001=70 \
--traffic java-knative-service-00002=30
# Tag a specific revision
kn service update java-knative-service --tag java-knative-service-00001=stable
# Access specific revision
curl -H "Host: stable-java-knative-service.default.example.com" http://${INGRESS_IP}

5. Auto-scaling Configuration

Custom Auto-scaling

autoscaling-service.yaml

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: java-app-autoscaling
spec:
template:
metadata:
annotations:
# Scale based on concurrency
autoscaling.knative.dev/metric: "concurrency"
autoscaling.knative.dev/target: "10"
# Scale based on RPS
# autoscaling.knative.dev/metric: "rps"
# autoscaling.knative.dev/target: "100"
# Scale bounds
autoscaling.knative.dev/min-scale: "1"
autoscaling.knative.dev/max-scale: "10"
# Scale behavior
autoscaling.knative.dev/panic-window: "60s"
autoscaling.knative.dev/panic-threshold: "200.0"
# Enable scale-to-zero
autoscaling.knative.dev/initial-scale: "1"
autoscaling.knative.dev/scale-to-zero-pod-retention-period: "5m"
spec:
containers:
- image: myregistry/knative-java-app:1.0.0
env:
- name: K_REVISION
value: "java-app-autoscaling-001"
resources:
requests:
memory: "128Mi"
cpu: "50m"

6. Knative Eventing with Java

Event Consumer Application

EventProcessor.java

package com.example.knative.eventing;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import java.util.function.Consumer;
import java.util.logging.Logger;
@SpringBootApplication
public class EventProcessorApplication {
public static void main(String[] args) {
SpringApplication.run(EventProcessorApplication.class, args);
}
}
@Component
class CloudEventProcessor {
private static final Logger logger = Logger.getLogger(CloudEventProcessor.class.getName());
@Bean
public Consumer<CloudEvent<String>> processOrder() {
return event -> {
logger.info("📦 Received CloudEvent: " + event);
logger.info("🔧 Event ID: " + event.getId());
logger.info("🔧 Event Type: " + event.getType());
logger.info("🔧 Event Source: " + event.getSource());
logger.info("🔧 Event Data: " + event.getData());
// Process the event
processEventData(event.getData());
};
}
@Bean
public Consumer<CloudEvent<byte[]>> processBinaryEvent() {
return event -> {
logger.info("📦 Received Binary CloudEvent");
String data = new String(event.getData());
logger.info("🔧 Event Data: " + data);
};
}
private void processEventData(String data) {
// Business logic for processing events
logger.info("🔄 Processing event data: " + data);
// Simulate processing time
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
logger.info("✅ Event processing completed");
}
}
// CloudEvent POJO
class CloudEvent<T> {
private String id;
private String source;
private String type;
private String specversion;
private T data;
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getSource() { return source; }
public void setSource(String source) { this.source = source; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
public String getSpecversion() { return specversion; }
public void setSpecversion(String specversion) { this.specversion = specversion; }
public T getData() { return data; }
public void setData(T data) { this.data = data; }
@Override
public String toString() {
return String.format(
"CloudEvent{id='%s', source='%s', type='%s', specversion='%s'}",
id, source, type, specversion
);
}
}

Eventing Configuration

eventing-setup.yaml

apiVersion: eventing.knative.dev/v1
kind: Broker
metadata:
name: default
namespace: default
---
apiVersion: eventing.knative.dev/v1
kind: Trigger
metadata:
name: java-app-trigger
namespace: default
spec:
broker: default
filter:
attributes:
type: "order.created"
subscriber:
ref:
apiVersion: serving.knative.dev/v1
kind: Service
name: java-event-processor

7. Java Application with Knative Features

Knative-Aware Application

KnativeAwareController.java

package com.example.knative.controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
@RestController
@RequestMapping("/knative")
public class KnativeAwareController {
private final AtomicInteger coldStartCount = new AtomicInteger(0);
private long startupTime = System.currentTimeMillis();
private boolean isColdStart = true;
@GetMapping("/info")
public Map<String, Object> getKnativeInfo(HttpServletRequest request) {
// Check if this is a cold start
if (isColdStart) {
coldStartCount.incrementAndGet();
isColdStart = false;
}
return Map.of(
"knativeRevision", System.getenv().getOrDefault("K_REVISION", "unknown"),
"knativeConfiguration", System.getenv().getOrDefault("K_CONFIGURATION", "unknown"),
"knativeService", System.getenv().getOrDefault("K_SERVICE", "unknown"),
"coldStarts", coldStartCount.get(),
"uptime", System.currentTimeMillis() - startupTime,
"instanceId", System.getenv().getOrDefault("HOSTNAME", "unknown"),
"requestHeaders", extractHeaders(request)
);
}
@PostMapping("/scale-test")
public ResponseEntity<Map<String, Object>> scaleTest(@RequestBody ScaleTestRequest request) {
int duration = request.duration();
int workload = request.workload();
// Simulate CPU workload
long startTime = System.currentTimeMillis();
performWorkload(workload, duration);
long endTime = System.currentTimeMillis();
return ResponseEntity.ok(Map.of(
"status", "completed",
"workload", workload,
"requestedDuration", duration,
"actualDuration", endTime - startTime,
"threadsUsed", Runtime.getRuntime().availableProcessors()
));
}
@GetMapping("/health/detailed")
public Map<String, Object> detailedHealth() {
Runtime runtime = Runtime.getRuntime();
return Map.of(
"status", "healthy",
"memory", Map.of(
"used", runtime.totalMemory() - runtime.freeMemory(),
"free", runtime.freeMemory(),
"total", runtime.totalMemory(),
"max", runtime.maxMemory()
),
"threads", Thread.activeCount(),
"coldStarts", coldStartCount.get(),
"environment", System.getenv().getOrDefault("KNATIVE_ENV", "default")
);
}
private Map<String, String> extractHeaders(HttpServletRequest request) {
return Map.of(
"user-agent", request.getHeader("User-Agent"),
"forwarded", request.getHeader("Forwarded"),
"knative-serving-namespace", request.getHeader("Knative-Serving-Namespace"),
"knative-serving-revision", request.getHeader("Knative-Serving-Revision")
);
}
private void performWorkload(int intensity, int duration) {
long endTime = System.currentTimeMillis() + duration;
while (System.currentTimeMillis() < endTime) {
// Simulate CPU-intensive work
double result = 0;
for (int i = 0; i < intensity * 1000; i++) {
result += Math.sqrt(i) * Math.sin(i);
}
}
}
record ScaleTestRequest(int duration, int workload) {}
}

8. Configuration and Secrets

ConfigMap and Secrets

config.yaml

apiVersion: v1
kind: ConfigMap
metadata:
name: java-app-config
data:
application.properties: |
server.port=8080
management.endpoints.web.exposure.include=health,info,metrics,prometheus
logging.level.com.example=INFO
spring.sleuth.sampler.probability=1.0
logback-spring.xml: |
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
<providers>
<timestamp/>
<logLevel/>
<loggerName/>
<message/>
<mdc/>
<stackTrace/>
</providers>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>
---
apiVersion: v1
kind: Secret
metadata:
name: java-app-secrets
type: Opaque
data:
database-url: base64EncodedValue
api-key: base64EncodedValue
---
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: java-app-with-config
spec:
template:
spec:
containers:
- image: myregistry/knative-java-app:1.0.0
env:
- name: CONFIG_MAP_VALUE
valueFrom:
configMapKeyRef:
name: java-app-config
key: application.properties
- name: SECRET_VALUE
valueFrom:
secretKeyRef:
name: java-app-secrets
key: api-key
volumeMounts:
- name: config-volume
mountPath: /app/config
volumes:
- name: config-volume
configMap:
name: java-app-config

9. Monitoring and Observability

Custom Metrics with Micrometer

MetricsConfiguration.java

package com.example.knative.metrics;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.Timer;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
@Component
public class KnativeMetrics {
private final MeterRegistry registry;
private final AtomicLong coldStartCounter;
private final Timer requestTimer;
private final ConcurrentHashMap<String, AtomicLong> endpointCounters;
public KnativeMetrics(MeterRegistry registry) {
this.registry = registry;
this.coldStartCounter = registry.gauge("knative.cold_starts", new AtomicLong(0));
this.requestTimer = Timer.builder("knative.request.duration")
.description("HTTP request duration")
.register(registry);
this.endpointCounters = new ConcurrentHashMap<>();
}
public void recordColdStart() {
coldStartCounter.incrementAndGet();
registry.counter("knative.cold_start_events").increment();
}
public void recordRequest(String endpoint, long duration) {
requestTimer.record(duration, TimeUnit.MILLISECONDS);
// Track endpoint-specific metrics
endpointCounters
.computeIfAbsent(endpoint, k -> registry.counter("knative.requests", "endpoint", k))
.increment();
}
public void recordScaleEvent(int currentPods, int desiredPods) {
registry.gauge("knative.pods.current", currentPods);
registry.gauge("knative.pods.desired", desiredPods);
}
}

10. CI/CD Pipeline with Knative

GitHub Actions for Knative

.github/workflows/knative-deploy.yaml

name: Deploy to Knative
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Build with Maven
run: mvn -B package -DskipTests
- name: Run tests
run: mvn test
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
- name: Deploy to Knative
if: github.ref == 'refs/heads/main'
run: |
# Update Knative service with new image
kn service update java-knative-service \
--image ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \
--env APP_VERSION=${{ github.sha }}
- name: Run smoke tests
if: github.ref == 'refs/heads/main'
run: |
# Wait for service to be ready
kn service describe java-knative-service --verbose
# Run smoke test
SERVICE_URL=$(kn service describe java-knative-service -o url)
curl -f $SERVICE_URL/api/health

Best Practices for Java on Knative

  1. Fast Startup - Optimize JVM startup with Class Data Sharing
  2. Graceful Shutdown - Handle SIGTERM properly
  3. Resource Efficiency - Set appropriate memory limits
  4. Health Checks - Implement comprehensive readiness/liveness probes
  5. Stateless Design - Don't rely on local storage
  6. Observability - Implement distributed tracing and metrics

Benefits for Java Applications

  • Cost Savings - Scale to zero when not in use
  • Automatic Scaling - Handle traffic spikes seamlessly
  • Simplified Operations - Less infrastructure management
  • Rapid Deployment - Quick iteration and deployment
  • Event-Driven - Natural fit for event-driven architectures

Conclusion

Knative provides a powerful serverless platform for Java applications on Kubernetes, offering:

  • Auto-scaling - From zero to many based on demand
  • Simplified Deployment - Abstract away Kubernetes complexity
  • Event-Driven Architecture - Built-in eventing capabilities
  • Traffic Management - Advanced routing and canary deployments
  • Cost Efficiency - Pay only for what you use

Java applications can leverage Knative to achieve true serverless benefits while running on any Kubernetes cluster, making it ideal for microservices, event processors, and API endpoints.


Next Steps: Explore Knative Eventing sources, implement custom metrics for auto-scaling, and integrate with service meshes for advanced traffic management.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper