Kvindo™ Cloud Docs
An OpenSearch Cluster is a managed OpenSearch 3.9.0 deployment split into two dedicated roles: a cluster-manager quorum of 1 or 3 virtual machines that holds the metadata and runs the elections, and 1..N data nodes that hold the shards and answer queries. Every member runs on its own virtual machine, and the cluster is described as one resource — spec.dataNodes and spec.clusterManagers each hold one entry per member, listing only its name and subnet. There is no combined cluster-manager+data node and no mode flag: a data node under merge, GC or disk pressure would stall the metadata quorum while every shard kept answering, which is a silent outage, so the platform ships one topology and one playbook path.
• Two roles, sized independently — data nodes hold your data, cluster managers hold only the cluster metadata. Sizing is one offer triplet per role (dataVmOfferId/dataVolumeOfferId/dataVolumeSizeGiB and the manager* equivalents) rather than per member, so a role's members are interchangeable by design. An omitted offer is resolved to that role's default server-side and written back, never left null
• Cluster managers: exactly 1 or 3, never 2 or 4 — a majority is more than half, so an even count tolerates no more failures than the odd count below it while costing more. 1 is a documented non-HA tier (no quorum HA) that can be grown to 3; both joiners arrive in a single request, because 2 is not a valid quorum
• Data nodes: 1..N, scale-out only — a modify may add data nodes. OpenSearch has no scale-in path here, so removing one is rejected outright rather than silently ignored. The minimum cluster is therefore 2 VMs: 1 manager + 1 data node
• Node name is the real identity — the server resolves each entry against this cluster's existing members by name and reuses the same member (and its VM) when it matches. Renaming is indistinguishable from remove+add, and since scale-in is refused the rename is rejected rather than rebuilt, so do not rename a member you want to keep
• Indices are yours — there is no shard, replica, ISM or index-template field on this resource. The platform sets only cluster.default_number_of_replicas cluster-wide: 0 on a single-data-node cluster (so its indices are not permanently yellow) and 1 otherwise. Fields you cannot set are not offered rather than accepted and ignored
• Public access is TLS-only — private by default. Setting spec.createPublicIpv4 attaches a floating IP to every data node so the REST port 9200 is reachable from outside the VPC; a cluster manager never receives one, because client traffic must not be sent to the voting configuration. There is no plaintext listener at all: even a private client pins the cluster CA
• The CA certificate is in status — status.caCertificate is the credential-free thing a client pins (--cacert, or verify_certs plus ca_certs). It is minted once at create and never regenerated on reconcile, so a client that pinned it keeps working
• No cluster-wide credential on this resource — the break-glass admin credential is generated and stored server-side only. Create an OpenSearch User to get a real, tenant-facing credential
• Disk watermarks are platform-managed — past flood_stage (95%) every index with a shard on a full node gets a read-only block and writes start failing with index read-only. That threshold is deliberately not settable: it is the last thing between a tenant and a full volume. The low/high watermarks are tunable through a Parameter Set — see the error's own explanation if you try to set flood_stage there
• Backups and Dashboards are not in v1 — there is no snapshot repository, no restore and no OpenSearch Dashboards deployment. A cluster is a derived copy of data you can re-index, not a system of record
| UI Name | API / kc name | Terraform name | Optional | Format | Default | Is read only | Description |
|---|---|---|---|---|---|---|---|
| Data nodes | spec.dataNodes |
spec.data_nodes |
No | list of objects | — | No | One entry per data node — 1..N, scale-out only |
| Data node name | dataNodes[].name |
data_nodes[].name |
No | string — [a-z0-9][a-z0-9-], ≤64 chars |
— | No | The real identity — reuse it across modifies to keep the same node; a rename is rejected, not applied |
| Data node subnet | dataNodes[].vpcSubnetId |
data_nodes[].vpc_subnet_id |
No | ULID — references a VPC Subnet | — | No | Subnet for this data node's VM |
| Cluster managers | spec.clusterManagers |
spec.cluster_managers |
No | list of objects | — | No | Exactly 1 or 3 — never 2 or 4. Append-only, grows 1 -> 3 |
| Cluster manager name | clusterManagers[].name |
cluster_managers[].name |
No | string — [a-z0-9][a-z0-9-], ≤64 chars |
— | No | The real identity — a rename is rejected, not applied |
| Cluster manager subnet | clusterManagers[].vpcSubnetId |
cluster_managers[].vpc_subnet_id |
No | ULID — references a VPC Subnet | — | No | Subnet for this cluster manager's VM |
| Parameters set | spec.parametersSetId |
spec.parameters_set_id |
Yes | ULID — references an OpenSearchParametersSet | — | No | Cluster-wide settings applied by the platform: allow-listed dynamic keys go through PUT _cluster/settings, the rest are rendered into opensearch.yml with a rolling restart |
| OpenSearch version | spec.openSearchVersion |
spec.opensearch_version |
No | string — full version, e.g. 3.9.0 |
— | No | Required. Must be a version the deployed image prefetched, because the platform installs only the package it ships. Immutable in v1: there is no rolling upgrade |
| Data node VM offer | spec.dataVmOfferId |
spec.data_vm_offer_id |
No | string (≤32) — compute offer | — | No | Applies to every data node. Resolved and written back server-side when omitted; see the Console catalog for real offer ids |
| Data node volume offer | spec.dataVolumeOfferId |
spec.data_volume_offer_id |
No | string (≤32) — disk offer | — | No | Applies to every data node. Changeable after creation; the volume is re-described |
| Data node volume size | spec.dataVolumeSizeGiB |
spec.data_volume_size_gib |
No | integer — GiB | — | No | Applies to every data node. Increase only — a shrink is refused, because the platform cannot shrink a volume |
| Cluster manager VM offer | spec.managerVmOfferId |
spec.manager_vm_offer_id |
No | string (≤32) — compute offer | — | No | Applies to every cluster manager — it serves metadata only, so it is sized far smaller than a data node |
| Cluster manager volume offer | spec.managerVolumeOfferId |
spec.manager_volume_offer_id |
No | string (≤32) — disk offer | — | No | Applies to every cluster manager. Changeable after creation |
| Cluster manager volume size | spec.managerVolumeSizeGiB |
spec.manager_volume_size_gib |
No | integer — GiB | — | No | Applies to every cluster manager. Increase only |
| Dashboards nodes | spec.dashboardsNodes |
spec.dashboards_nodes |
No | array — at most 1 | — | No | The UI tier. Each entry is a name plus a subnet, exactly like a cluster manager. A dashboards node is NOT an OpenSearch node: no shards, no vote, never in the quorum. Reachable on port 5601 over the platform's managed certificate at the PRIVATE address only — there is no floating IP |
| Dashboards VM offer | spec.dashboardsVmOfferId |
spec.dashboards_vm_offer_id |
No | string (≤32) — compute offer | — | No | Applies to the dashboards node. At least 4 GiB, like every OpenSearch VM |
| Dashboards volume offer | spec.dashboardsVolumeOfferId |
spec.dashboards_volume_offer_id |
No | string (≤32) — disk offer | — | No | Applies to the dashboards node. Changeable after creation |
| Dashboards volume size | spec.dashboardsVolumeSizeGiB |
spec.dashboards_volume_size_gib |
No | integer — GiB | — | No | Applies to the dashboards node. Increase only |
| Use FQDN | spec.useFqdn |
spec.use_fqdn |
Yes | boolean | false |
No | Announce DNS names instead of raw IPs in every node certificate and in status.dnsSeedFqdn, so an address can change without re-seeding every client. Immutable after creation — it re-signs every certificate |
| Public IPv4 | spec.createPublicIpv4 |
spec.create_public_ipv4 |
Yes | boolean | false |
No | Attaches a floating IP to every data node so the TLS REST port 9200 is reachable from outside the VPC; cluster managers never get one. Immutable after creation |
| Cluster name | status.clusterName |
status.cluster_name |
— | string | — | Yes | The cluster id, lowercased — what an OpenSearch client reports and pins as cluster.name |
| Cluster state | status.clusterState |
status.cluster_state |
— | string | — | Yes | ok | degraded | no-quorum | unknown. Losing quorum does not take the cluster down: reads keep working, but index creation, mapping changes and shard allocation fail |
| Health | status.health |
status.health |
— | string | — | Yes | green | yellow | red | unknown — OpenSearch's own cluster health |
| REST endpoints | status.restEndpoints |
status.rest_endpoints |
— | list of strings | — | Yes | host:9200 over TLS on the private network. Always over the DATA nodes — a cluster manager is never published |
| REST public endpoints | status.restPublicEndpoints |
status.rest_public_endpoints |
— | list of strings | — | Yes | floating-ip:9200 on the data nodes that received one; empty on a private cluster |
| DNS seed FQDN | status.dnsSeedFqdn |
status.dns_seed_fqdn |
— | string | — | Yes | A DNS name that round-robins over the data nodes — the stable way to seed a client's host list |
| CA certificate | status.caCertificate |
status.ca_certificate |
— | string (PEM) | — | Yes | The cluster root CA — what a client pins to authenticate the nodes. Credential-free; minted once and never regenerated |
| Active cluster managers | status.activeClusterManagerCount |
status.active_cluster_manager_count |
— | integer | — | Yes | How many cluster managers currently answer |
| Quorum size | status.clusterManagerQuorumSize |
status.cluster_manager_quorum_size |
— | integer | — | Yes | How many cluster managers must agree: a MAJORITY of the voting configuration, so 1 of 1 or 2 of 3 — not the member count |
| Default replicas | status.defaultNumberOfReplicas |
status.default_number_of_replicas |
— | integer | — | Yes | What the platform set cluster-wide: 0 on a single-data-node cluster, else 1. It applies to indices created from then on, not to existing ones |
| Index count | status.indexCount |
status.index_count |
— | integer | — | Yes | How many indices the cluster currently has |
| Documents | status.totalDocuments |
status.total_documents |
— | integer | — | Yes | Total document count across all indices |
| Data size | status.totalDataBytes |
status.total_data_bytes |
— | integer — bytes | — | Yes | Total size of the index data |
| Unassigned shards | status.unassignedShards |
status.unassigned_shards |
— | integer | — | Yes | Shards with no node at all — a non-zero value is the usual reason health is red or yellow |
| Relocating shards | status.relocatingShards |
status.relocating_shards |
— | integer | — | Yes | Shards currently moving between nodes |
| Active shards | status.activeShardsPercent |
status.active_shards_percent |
— | integer — percent | — | Yes | Share of shards that are active. It rarely reaches 100, because relocating shards are not counted |
| Yellow indices | status.yellowIndexCount |
status.yellow_index_count |
— | integer | — | Yes | Indices with unassigned replicas — normal on a cluster with fewer data nodes than the replica count asks for |
| Red indices | status.redIndexCount |
status.red_index_count |
— | integer | — | Yes | Indices with at least one unassigned primary — that index is unsearchable and losing writes |
| Any index read-only | status.anyIndexReadonly |
status.any_index_readonly |
— | boolean | — | Yes | True when a node crossed flood_stage and a read-only block was placed on every index with a shard there. This is the number-one "my writes stopped" cause; the block clears by itself once usage falls back below high |
| Running version | status.runningVersion |
status.running_version |
— | string | — | Yes | The OpenSearch version the nodes actually report |
| Last probe | status.lastProbeUtc |
status.last_probe_utc |
— | integer | — | Yes | Unix time of the last health probe |
| Anti-affinity OK | status.antiAffinityOk |
status.anti_affinity_ok |
— | boolean | — | Yes | Always null: the platform does not report hypervisor placement, so anti-affinity cannot be verified from here |
| Anti-affinity message | status.antiAffinityMessage |
status.anti_affinity_message |
— | string | — | Yes | Human-readable detail behind antiAffinityOk |
| Data nodes | status.dataNodes |
status.data_nodes |
— | list of objects | — | Yes | Observed state per data node |
| Node name | dataNodes[].name |
data_nodes[].name |
— | string | — | Yes | Equal to the node's own node.name |
| Private IP | dataNodes[].privateIpV4 |
data_nodes[].private_ip_v4 |
— | string | — | Yes | The node's address inside the VPC |
| Public IP | dataNodes[].publicIpV4 |
data_nodes[].public_ip_v4 |
— | string | — | Yes | The floating IP, when spec.createPublicIpv4 is set |
| Announced FQDN | dataNodes[].announceFqdn |
data_nodes[].announce_fqdn |
— | string | — | Yes | The DNS name announced in the node's certificate when spec.useFqdn is set |
| Observed state | dataNodes[].observedState |
data_nodes[].observed_state |
— | string | — | Yes | ok | unreachable | unknown |
| Disk used | dataNodes[].diskUsedBytes |
data_nodes[].disk_used_bytes |
— | integer — bytes | — | Yes | How much of the node's volume is in use |
| Disk free | dataNodes[].diskFreeBytes |
data_nodes[].disk_free_bytes |
— | integer — bytes | — | Yes | Free space left on the node's volume — the number to watch against the watermarks |
| JVM heap used | dataNodes[].jvmHeapUsedPercent |
data_nodes[].jvm_heap_used_percent |
— | number — percent | — | Yes | Share of the node's heap in use |
| Running version | dataNodes[].runningVersion |
data_nodes[].running_version |
— | string | — | Yes | The version this node reports |
| Cluster managers | status.clusterManagers |
status.cluster_managers |
— | list of objects | — | Yes | Observed state per cluster manager |
| Manager name | clusterManagers[].name |
cluster_managers[].name |
— | string | — | Yes | The member's identity |
| Private IP | clusterManagers[].privateIpV4 |
cluster_managers[].private_ip_v4 |
— | string | — | Yes | The manager's address inside the VPC — never published |
| Elected | clusterManagers[].isElectedClusterManager |
cluster_managers[].is_elected_cluster_manager |
— | boolean | — | Yes | True for whichever member currently holds the election |
| Observed state | clusterManagers[].observedState |
cluster_managers[].observed_state |
— | string | — | Yes | elected | candidate | unreachable | unknown |
• Adding a data node does not raise number_of_replicas on indices that already exist. The platform changes the cluster default, which applies to indices created from then on — raising an existing index is your own PUT <pattern>/_settings
• On a single-data-node cluster there is no redundancy at all, and with backups deferred there is no restore path. Treat three data nodes as the production floor and raise the data-node offer before the node count
• index.number_of_shards is final for an index: it can only be set at creation, so plan the shard count up front — over-sharding a small cluster is the other classic mistake
• action.destructive_requires_name is true, so DELETE /logs-* and DELETE /_all are refused. Delete by explicit name, or use _delete_by_query
One cluster manager and two data nodes — the smallest topology that is both real and shard-safe, plus a parameter set and an index-granted user.
Terraform
Python SDK
kc CLI
Resource docs
Examples
Guides