This article explains how to use my Spring Boot Istio library to generate and create Istio resources on a Kubernetes cluster during application startup. The library is primarily intended for development purposes. It aims to make it easier for developers to quickly and easily launch their applications within the Istio mesh. Of course, you can also use this library in production. However, its purpose is to generate resources from annotations in Java application code automatically.
You can read more about Istio, Spring Boot, and other Java and Kubernetes-related technologies in my latest book, Hands-On Java with Kubernetes.
You can also find many other articles on my blog about Istio. For example, this article is about Quarkus and tracing with Istio.
Source Code
Feel free to use my source code if you’d like to try it out yourself. To do that, you must clone my sample GitHub repository. Two sample applications for this exercise are available in the spring-boot-istio directory.
Prerequisites
We will start with the most straightforward Istio installation on a local Kubernetes cluster. This could be Minikube, which you can run with the following command. You can set slightly lower resource limits than I did.
minikube start --memory='8gb' --cpus='6'
ShellSession
To complete the exercise below, you need to install istioctl in addition to kubectl. Here you will find the available distributions for the latest versions of kubectl and istioctl. I install them on my laptop using Homebrew.
$ brew install kubectl
$ brew install istioctl
ShellSession
To install Istio with default parameters, run the following command:
istioctl install
ShellSession
After a moment, Istio should be running in the istio-system namespace.

It is also worth installing Kiali to verify the Istio resources we have created. Kiali is an observability and management tool for Istio that provides a web-based dashboard for service mesh monitoring. It visualizes service-to-service traffic, validates Istio configuration, and integrates with tools like Prometheus, Grafana, and Jaeger.
kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.28/samples/addons/kiali.yaml
ShellSession
Once Kiali is successfully installed on Kubernetes, we can expose its web dashboard locally with the following istioctl command:
istioctl dashboard kiali
ShellSession
To test access to the Istio mesh from outside the cluster, you need to expose the ingress gateway. To do this, run the minikube tunnel command.

Use Spring Boot Istio Library
To test our library’s functionality, we will create a simple Spring Boot application that exposes a single REST endpoint.
@RestController
@RequestMapping("/callme")
public class CallmeController {
private static final Logger LOGGER = LoggerFactory
.getLogger(CallmeController.class);
@Autowired
Optional<BuildProperties> buildProperties;
@Value("${VERSION}")
private String version;
@GetMapping("/ping")
public String ping() {
LOGGER.info("Ping: name={}, version={}", buildProperties.isPresent() ?
buildProperties.get().getName() : "callme-service", version);
return "I'm callme-service " + version;
}
}
Java
Then, in addition to the standard Spring Web starter, add the istio-spring-boot-starter dependency.
org.springframework.boot
spring-boot-starter-web
com.github.piomin
istio-spring-boot-starter
1.2.1
XML
Finally, we must add the @EnableIstio annotation to our application’s main class. We can also enable Istio Gateway to expose the REST endpoint outside the cluster. An Istio Gateway is a component that controls how external traffic enters or leaves a service mesh.
@SpringBootApplication
@EnableIstio(enableGateway = true)
public class CallmeApplication {
public static void main(String[] args) {
SpringApplication.run(CallmeApplication.class, args);
}
}
Java
Let’s deploy our application on the Kubernetes cluster. To do this, we must first create a role with the necessary permissions to manage Istio resources in the cluster. The role must be assigned to the ServiceAccount used by the application.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: callme-service-with-starter
rules:
- apiGroups: ["networking.istio.io"]
resources: ["virtualservices", "destinationrules", "gateways"]
verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: callme-service-with-starter
subjects:
- kind: ServiceAccount
name: callme-service-with-starter
namespace: spring
roleRef:
kind: Role
name: callme-service-with-starter
apiGroup: rbac.authorization.k8s.io
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: callme-service-with-starter
YAML
Here are the Deployment and Service resources.
apiVersion: apps/v1
kind: Deployment
metadata:
name: callme-service-with-starter
spec:
replicas: 1
selector:
matchLabels:
app: callme-service-with-starter
template:
metadata:
labels:
app: callme-service-with-starter
spec:
containers:
- name: callme-service-with-starter
image: piomin/callme-service-with-starter
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: VERSION
value: "v1"
serviceAccountName: callme-service-with-starter
---
apiVersion: v1
kind: Service
metadata:
name: callme-service-with-starter
labels:
app: callme-service-with-starter
spec:
type: ClusterIP
ports:
- port: 8080
name: http
selector:
app: callme-service-with-starter
YAML
The application repository is configured to run it with Skaffold. Of course, you can apply YAML manifests to the cluster with kubectl apply. To do this, simply navigate to the callme-service-with-starter/k8s directory and apply the deployment.yaml file. As part of the exercise, we will run our applications in the spring namespace.
skaffold dev -n spring
ShellSession
The sample Spring Boot application creates two Istio objects at startup: a VirtualService and a Gateway. We can verify them in the Kiali dashboard.

The default host name generated for the gateway includes the deployment name and the .ext suffix. We can change the suffix name using the domain field in the @EnableIstio annotation. Assuming you run the minikube tunnel command, you can call the service using the Host header with the hostname in the following way:
$ curl http://localhost/callme/ping -H "Host:callme-service-with-starter.ext"
I'm callme-service v1
ShellSession
Additional Capabilities with Spring Boot Istio
The library’s behavior can be customized by modifying the @EnableIstio annotation. For example, you can enable the fault injection mechanism using the fault field. Both abort and delay are possible. You can make this change without redeploying the app. The library updates the existing VirtualService.
@SpringBootApplication
@EnableIstio(enableGateway = true, fault = @Fault(percentage = 50))
public class CallmeApplication {
public static void main(String[] args) {
SpringApplication.run(CallmeApplication.class, args);
}
}
Java
Now, you can call the same endpoint through the gateway. There is a 50% chance that you will receive this response.

Now let’s analyze a slightly more complex scenario. Let’s assume that we are running two different versions of the same application on Kubernetes. We use Istio to manage its versioning. Traffic is forwarded to the particular version based on the X-Version header in the incoming request. If the header value is v1 the request is sent to the application Pod with the label version=v1, and similarly, for the version v2. Here’s the annotation for the v1 application main class.
@SpringBootApplication
@EnableIstio(enableGateway = true, version = "v1",
matches = {
@Match(type = MatchType.HEADERS, key = "X-Version", value = "v1") })
public class CallmeApplication {
public static void main(String[] args) {
SpringApplication.run(CallmeApplication.class, args);
}
}
Java
The Deployment manifest for the callme-service-with-starter-v1 should define two labels: app and version.
apiVersion: apps/v1
kind: Deployment
metadata:
name: callme-service-with-starter-v1
spec:
replicas: 1
selector:
matchLabels:
app: callme-service-with-starter
version: v1
template:
metadata:
labels:
app: callme-service-with-starter
version: v1
spec:
containers:
- name: callme-service-with-starter
image: piomin/callme-service-with-starter
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: VERSION
value: "v1"
serviceAccountName: callme-service-with-starter
YAML
Unlike the skaffold dev command, the skaffold run command simply launches the application on the cluster and terminates. Let’s first release version v1, and then move on to version v2.
skaffold run -n spring
ShellSession
Then, we can deploy the v2 version of our sample Spring Boot application. In this exercise, it is just an “artificial” version, since we deploy the same source code, but with different labels and environment variables injected in the Deployment manifest.
@SpringBootApplication
@EnableIstio(enableGateway = true, version = "v2",
matches = {
@Match(type = MatchType.HEADERS, key = "X-Version", value = "v2") })
public class CallmeApplication {
public static void main(String[] args) {
SpringApplication.run(CallmeApplication.class, args);
}
}
Java
Here’s the callme-service-with-starter-v2 Deployment manifest.
apiVersion: apps/v1
kind: Deployment
metadata:
name: callme-service-with-starter-v2
spec:
replicas: 1
selector:
matchLabels:
app: callme-service-with-starter
version: v2
template:
metadata:
labels:
app: callme-service-with-starter
version: v2
spec:
containers:
- name: callme-service-with-starter
image: piomin/callme-service-with-starter
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
env:
- name: VERSION
value: "v2"
serviceAccountName: callme-service-with-starter
YAML
Service, on the other hand, remains unchanged. It still refers to Pods labeled with app=callme-service-with-starter. However, this time it includes both application instances marked as v1 or v2.
apiVersion: v1
kind: Service
metadata:
name: callme-service-with-starter
labels:
app: callme-service-with-starter
spec:
type: ClusterIP
ports:
- port: 8080
name: http
selector:
app: callme-service-with-starter
YAML
Version v2 should be run in the same way as before, using the skaffold run command. There are three Istio objects generated during apps startup: Gateway, VirtualService and DestinationRule.

A generated DestinationRule contains two subsets for both v1 and v2 versions.

The automatically generated VirtualService looks as follows.
kind: VirtualService
apiVersion: networking.istio.io/v1
metadata:
name: callme-service-with-starter-route
spec:
hosts:
- callme-service-with-starter
- callme-service-with-starter.ext
gateways:
- callme-service-with-starter
http:
- match:
- headers:
X-Version:
prefix: v1
route:
- destination:
host: callme-service-with-starter
subset: v1
timeout: 6s
retries:
attempts: 3
perTryTimeout: 2s
retryOn: 5xx
- match:
- headers:
X-Version:
prefix: v2
route:
- destination:
host: callme-service-with-starter
subset: v2
weight: 100
timeout: 6s
retries:
attempts: 3
perTryTimeout: 2s
retryOn: 5xx
YAML
To test the versioning mechanism generated using the Spring Boot Istio library, set the X-Version header for each call.
$ curl http://localhost/callme/ping -H "Host:callme-service-with-starter.ext" -H "X-Version:v1"
I'm callme-service v1
$ curl http://localhost/callme/ping -H "Host:callme-service-with-starter.ext" -H "X-Version:v2"
I'm callme-service v2
ShellSession
Conclusion
I am still working on this library, and new features will be added in the near future. I hope it will be helpful for those of you who want to get started with Istio without getting into the details of its configuration.

