New to KubeDB? Please start here.
KubeDB - Milvus Distributed Cluster
This tutorial shows how to use KubeDB to provision a distributed Milvus cluster, where the Milvus roles run as separate workloads.
Before You Begin
- You need a Kubernetes cluster and
kubectlconfigured to talk to it. - Install KubeDB with
--set global.featureGates.Milvus=true --set global.featureGates.Etcd=true(the internal metadata store is a KubeDBEtcd). - Complete the dependency setup from Prepare Dependencies. That guide installs MinIO and creates the object-storage secrets (
milvus-storage-configformilvus-standalone,milvus-cluster-storage-configformilvus-cluster). - This quickstart intentionally uses the smallest working distributed manifest. It does not require Prometheus Operator or cert-manager.
Note: The yaml files used in this tutorial are stored in docs/guides/milvus/quickstart/yamls folder in GitHub repository kubedb/docs.
Distributed Topology
A distributed Milvus is composed of five roles:
| Role | Purpose | Persistent storage |
|---|---|---|
mixcoord | Coordinator | No |
proxy | Client-facing gateway | No |
datanode | Data persistence | No |
querynode | Query execution | No |
streamingnode | Streaming / WAL | Yes |
Only streamingnode carries a persistent volume. This is why distributed storage operations target streamingnode.
Create a Distributed Milvus
In the manifest below, only streamingnode is specified under spec.topology.distributed. The operator defaults the other four roles automatically.
distributed.yaml
apiVersion: kubedb.com/v1alpha2
kind: Milvus
metadata:
name: milvus-cluster
namespace: demo
spec:
version: "2.6.11"
objectStorage:
configSecret:
name: "milvus-cluster-storage-config"
topology:
mode: Distributed
distributed:
streamingnode:
storageType: Durable
storage:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 1Gi
This manifest also omits spec.metaStorage, so KubeDB creates and manages the etcd metadata cluster as a KubeDB Etcd object named milvus-cluster-etcd. It uses the milvus-cluster-storage-config secret, so its object-storage rootPath is separate from the standalone quickstart’s (see Prepare Dependencies).
$ kubectl apply -f https://github.com/kubedb/docs/raw/v2026.10.12/docs/guides/milvus/quickstart/yamls/distributed.yaml
milvus.kubedb.com/milvus-cluster created
Wait for the Cluster to be Ready
KubeDB first brings up the internal etcd (milvus-cluster-etcd, three pods), then the Milvus roles. Expect a few minutes, longer on the first run while images are pulled:
$ kubectl get milvuses.kubedb.com -n demo milvus-cluster -w
NAME VERSION STATUS AGE
milvus-cluster 2.6.11 Provisioning 20s
milvus-cluster 2.6.11 Ready 3m
Verify the Created Resources
PetSets
$ kubectl get petset -n demo -l app.kubernetes.io/instance=milvus-cluster
NAME AGE
milvus-cluster-datanode 2m57s
milvus-cluster-mixcoord 2m58s
milvus-cluster-proxy 2m54s
milvus-cluster-querynode 2m56s
milvus-cluster-streamingnode 2m55s
The four roles other than streamingnode were created even though only streamingnode was specified.
$ kubectl get pods -n demo -l app.kubernetes.io/instance=milvus-cluster
NAME READY STATUS RESTARTS AGE
milvus-cluster-datanode-0 1/1 Running 0 2m58s
milvus-cluster-mixcoord-0 1/1 Running 0 2m59s
milvus-cluster-proxy-0 1/1 Running 0 2m55s
milvus-cluster-querynode-0 1/1 Running 0 2m57s
milvus-cluster-streamingnode-0 1/1 Running 0 2m55s
Services
A primary client service (milvus-cluster, backed by the proxy) and a headless governing service per role are created. The primary service exposes:
- gRPC on
19530 - metrics on
9091 - REST on
8080
$ kubectl get svc -n demo -l app.kubernetes.io/instance=milvus-cluster
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
milvus-cluster ClusterIP 10.43.221.1 <none> 19530/TCP,9091/TCP,8080/TCP 3m
milvus-cluster-datanode ClusterIP None <none> 9091/TCP 3m
milvus-cluster-mixcoord ClusterIP None <none> 9091/TCP 3m
milvus-cluster-querynode ClusterIP None <none> 9091/TCP 3m
milvus-cluster-streamingnode ClusterIP None <none> 9091/TCP 3m
If you later enable Prometheus Operator monitoring, KubeDB also creates a dedicated milvus-cluster-stats service and a ServiceMonitor.
Storage
There is exactly one Milvus PVC, for the streamingnode:
$ kubectl get pvc -n demo -l app.kubernetes.io/instance=milvus-cluster
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
data-milvus-cluster-streamingnode-0 Bound pvc-... 1Gi RWO local-path 2m55s
Auth Secret and AppBinding
As with standalone, KubeDB auto-generates the auth secret (milvus-cluster-auth, user root), the rendered configuration secret, the internal etcd’s auth secret (milvus-cluster-etcd-auth), and an AppBinding. Because this quickstart does not enable TLS, the AppBinding scheme is http:
$ kubectl get secret -n demo | grep milvus-cluster
milvus-cluster-auth kubernetes.io/basic-auth 2 3m
milvus-cluster-d7497a Opaque 2 3m
milvus-cluster-etcd-auth kubernetes.io/basic-auth 2 3m
$ kubectl get appbinding milvus-cluster -n demo -o jsonpath='{.spec.clientConfig.service}'
{"name":"milvus-cluster","path":"/","port":19530,"scheme":"http"}
Connect and Run Basic Operations
For a distributed Milvus deployment, use the primary milvus-cluster service, which is backed by the proxy role, to reach the REST API.
Port-forward the proxy REST port
Run this in a separate terminal:
$ kubectl port-forward svc/milvus-cluster -n demo 8080:8080
Forwarding from 127.0.0.1:8080 -> 8080
Get the root password
$ PASSWORD=$(kubectl get secret milvus-cluster-auth -n demo -o jsonpath='{.data.password}' | base64 -d)
Create a collection
$ curl -s -X POST "http://localhost:8080/v2/vectordb/collections/create" \
-H "Authorization: Bearer root:${PASSWORD}" \
-H "Content-Type: application/json" \
-d '{
"collectionName": "health_check_collection",
"dbName": "default",
"dimension": 4,
"metricType": "L2"
}' | jq .
{
"code": 0,
"data": {}
}
Insert and search sample vectors
$ curl -s -X POST "http://localhost:8080/v2/vectordb/entities/insert" \
-H "Authorization: Bearer root:${PASSWORD}" \
-H "Content-Type: application/json" \
-d '{
"collectionName": "health_check_collection",
"dbName": "default",
"data": [
{"id": 1, "vector": [0.9, 0.8, 0.7, 0.6]},
{"id": 2, "vector": [0.5, 0.4, 0.3, 0.2]}
]
}' | jq .
{
"code": 0,
"cost": 0,
"data": {
"insertCount": 2,
"insertIds": [1, 2]
}
}
$ curl -s -X POST "http://localhost:8080/v2/vectordb/entities/search" \
-H "Authorization: Bearer root:${PASSWORD}" \
-H "Content-Type: application/json" \
-d '{
"collectionName": "health_check_collection",
"dbName": "default",
"data": [[0.5, 0.4, 0.3, 0.2]],
"topK": 2,
"metricType": "L2",
"params": {"nprobe": 10}
}' | jq .
{
"code": 0,
"cost": 0,
"data": [
{
"distance": 0,
"id": 2
},
{
"distance": 0.64,
"id": 1
}
],
"topks": [2]
}
Use an External etcd Cluster Instead
The default quickstart omits spec.metaStorage, so KubeDB manages etcd for you. If you want Milvus to use an external etcd cluster instead:
- Manage your etcd cluster yourself.
- Add
spec.metaStorage.externallyManaged: trueand the external endpoints to yourMilvusmanifest as shown in Prepare Dependencies.
Cleaning up
$ kubectl patch -n demo milvus.kubedb.com milvus-cluster -p '{"spec":{"deletionPolicy":"WipeOut"}}' --type="merge"
$ kubectl delete milvus.kubedb.com milvus-cluster -n demo
$ kubectl delete secret -n demo milvus-cluster-etcd-auth
The internal etcd’s auth secret is not removed by WipeOut, so the last command deletes it.
KubeDB does not delete the database’s data in object storage (mlv-release/milvus-cluster/ in this guide). If you will create a new Milvus on the same rootPath later, for example by following another guide, delete that prefix first, as shown in Delete a Milvus Instance’s Object-Storage Data. Otherwise the new database can hang in Provisioning.
If you want to remove the dependencies too, either delete the whole demo namespace or follow the cleanup steps in Prepare Dependencies.
Next Steps
- Prepare Dependencies for another cluster.
- Monitor your Milvus cluster.
- Horizontally scale the distributed roles.
- Detail concepts of Milvus object.
- Want to hack on KubeDB? Check our contribution guidelines.
































