JusDB
Free DB Audit
NoSQL Databases

OpenSearch on Kubernetes with the OpenSearch Operator

Deploy OpenSearch on Kubernetes with operator 3.0.0: design node pools, size heap against memory limits, set up TLS, run rolling upgrades, and back up to S3.

JusDB Team
Published October 22, 2026
Updated November 9, 2026
20 min read

Most attempts to run OpenSearch on Kubernetes fail in the same few places. Pods crash-loop with max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]. Data nodes get OOMKilled because the JVM heap was sized against the node instead of the container. Or a plain StatefulSet restarts the next pod before the cluster has recovered its replicas. The OpenSearch Kubernetes Operator handles most of this for you. Version 3.0.0 shipped on 25 September 2026, moved its CRDs to the opensearch.org/v1 API group, and supports OpenSearch 2.19.2 through the latest 3.x release (3.9.0 as of November 2026). This guide covers the OpenSearchCluster manifest, storage, heap and memory limits, TLS, rolling upgrades, scaling, S3 snapshots, monitoring, and when the plain Helm chart is the better choice.

TL;DR
  • Install cert-manager first, then operator chart 3.0.14 (operator 3.0.0). Write new manifests against opensearch.org/v1; opensearch.opster.io is deprecated.
  • Give each role its own node pool. The operator creates one StatefulSet and one Service per pool, so three dedicated cluster_manager nodes stay separate from data and ingest nodes.
  • Set the heap to half of the container's memory request and set requests equal to limits. The rest of the memory is page cache for Lucene, not waste.
  • Version upgrades are a one-field change to spec.general.version. The operator restarts one node at a time, refuses downgrades, and rejects jumps of more than one major version.
  • Upgrading the operator itself from 2.x to 3.0 restarts every node once. Schedule it like any rolling restart.

Scope and review date: November 2026. This post covers the OpenSearch Kubernetes Operator 3.0.0 (Helm chart 3.0.14) running OpenSearch 2.19.x and 3.x, plus the standalone opensearch Helm chart 3.9.0. Field names, defaults, and versions were checked on 9 November 2026 against the operator's v3.0.0 user guide and CRD reference and the OpenSearch documentation. The manifests and commands were not run in a lab, so apply them to a staging cluster first.


Which OpenSearch operator and OpenSearch versions should you run?

As of November 2026, the current operator release is 3.0.0, published on 25 September 2026 together with operator Helm chart 3.0.14. The operator's compatibility table lists OpenSearch 2.19.2 as the minimum and "latest 3.x" as the maximum. On the OpenSearch side, 3.9.0 came out on 29 September 2026, and the 2.19 line is still patched (2.19.7 shipped on 20 October 2026).

The biggest change in 3.0 is the API group. Resources move from opensearch.opster.io/v1 to opensearch.org/v1. The legacy group still works, but its webhooks deny new resources and spec changes in it. A migration controller copies each running legacy cluster to a new opensearch.org twin.

bash
# cert-manager issues the operator's webhook certificate, so install it first
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager --create-namespace --set crds.enabled=true

helm repo add opensearch-operator https://opensearch-project.github.io/opensearch-k8s-operator/
helm install opensearch-operator opensearch-operator/opensearch-operator \
  --namespace opensearch-operator --create-namespace --version 3.0.14

Interpretation: without cert-manager, helm install fails with no matches for kind "Certificate" in version "cert-manager.io/v1". If you cannot run cert-manager, the operator's webhook guide describes supplying the webhook certificate yourself or disabling the webhook.

Upgrading from operator 2.x restarts every node

The StatefulSet selector is immutable, and 3.0 selects pods by new opensearch.org/... labels. The operator recreates each StatefulSet with orphaned pods and then rolls every pod once. The migration guide also lists stored values to recheck afterwards: rotateDaysBeforeExpiry: -1, smartScaler: false, and a lost setVMMaxMapCount: false.

From one OpenSearchCluster resource to StatefulSets

Follow the numbers: one custom resource, one operator, one StatefulSet per node pool

How the OpenSearch operator turns node pools into StatefulSets Step 1: you apply an OpenSearchCluster custom resource in the opensearch.org/v1 API group. Step 2: the operator reconciles it, checks cluster health through the REST API, and creates the Kubernetes objects. Step 3: each node pool in the spec becomes its own StatefulSet with its own Service and one persistent volume claim per pod. In this example the masters pool has three cluster manager pods, the data pool has three data pods, and the ingest pool has two ingest pods. OpenSearchCluster opensearch.org/v1 Operator 3.0.0 reconcile loop masters StatefulSet, 3 pods data StatefulSet, 3 pods ingest StatefulSet, 2 pods 1 2 3 each pool: one StatefulSet, one Service, one PVC per pod

Per-pool objects: because each pool is its own StatefulSet, you can size, schedule, and protect cluster managers, data nodes, and ingest nodes separately.

What does a production OpenSearchCluster manifest look like?

The operator's quickstart runs three nodes that hold both cluster_manager and data. That is fine for a demo. In production, give each role its own pool. Valid roles include cluster_manager, data, ingest, ml, remote_cluster_client, search and warm. In 3.0.0, the admission webhook rejects unknown roles and any cluster without a cluster-manager pool of at least one replica.

yaml
apiVersion: opensearch.org/v1
kind: OpenSearchCluster
metadata:
  name: logs
  namespace: search
spec:
  general:
    serviceName: logs
    version: "3.9.0"
    httpPort: 9200
    pluginsList: ["repository-s3"]
  dashboards:
    enable: true
    version: "3.9.0"
    replicas: 2
    tls:
      enable: true
      generate: true
  security:
    tls:
      transport:
        generate: true
        perNode: true
      http:
        generate: true
  nodePools:
    - component: masters
      replicas: 3
      diskSize: "20Gi"
      roles: ["cluster_manager"]
      jvm: "-Xms2g -Xmx2g"
      resources:
        requests: { memory: "4Gi", cpu: "1" }
        limits:   { memory: "4Gi", cpu: "1" }
      pdb:
        enable: true
        maxUnavailable: 1
    - component: data
      replicas: 3
      diskSize: "500Gi"
      roles: ["data"]
      jvm: "-Xms8g -Xmx8g"
      resources:
        requests: { memory: "16Gi", cpu: "4" }
        limits:   { memory: "16Gi", cpu: "4" }
      persistence:
        pvc:
          storageClass: ssd-expandable
          accessModes: ["ReadWriteOnce"]
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: DoNotSchedule
          labelSelector:
            matchLabels:
              opensearch.org/opensearch-nodepool: data
    - component: ingest
      replicas: 2
      diskSize: "10Gi"
      roles: ["ingest"]
      resources:
        requests: { memory: "4Gi", cpu: "2" }
        limits:   { memory: "4Gi", cpu: "2" }

Interpretation: this produces StatefulSets named after the cluster and component, for example pods logs-masters-0 to logs-masters-2. A temporary logs-bootstrap-0 pod helps the first cluster-manager election and is removed after initialization. Three cluster-manager pods keep a quorum when one is down. The ingest pool has no jvm field, so the operator sets the heap to half of its memory request.

bash
kubectl apply -f logs-cluster.yaml
kubectl -n search get opensearchclusters.opensearch.org logs
kubectl -n search get statefulsets
kubectl -n search get pods -l opensearch.org/opensearch-cluster=logs

Interpretation: the custom resource prints HEALTH, NODES, VERSION, PHASE, and AGE columns. PHASE is PENDING, RUNNING, or UPGRADING, and RUNNING only means the operator reconciled the cluster. Health comes from OpenSearch. Use the fully qualified opensearchclusters.opensearch.org while both API groups are installed, so kubectl does not resolve the legacy kind.

How do you size storage, JVM heap, and memory limits?

Storage

By default each pool gets PVCs from the default StorageClass. Name a StorageClass explicitly; when you set persistence.pvc, accessModes is required. StatefulSet volume claim templates are immutable, so pick the class before the first apply. To grow disks later, raise diskSize. The operator expands the PVCs online if the StorageClass has allowVolumeExpansion: true, and keeps the same unit (Gi or G) you started with.

yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
  name: ssd-expandable
provisioner: ebs.csi.aws.com
parameters:
  type: gp3
allowVolumeExpansion: true
volumeBindingMode: WaitForFirstConsumer

Interpretation: WaitForFirstConsumer creates each volume in the zone where its pod is scheduled, which matters with zone spread constraints. Leave headroom for OpenSearch's disk watermarks. The cluster settings reference lists defaults of 85% (no new replicas), 90% (shards move away), and 95% (indexes become read-only).

PVCs survive a scale-down or a deleted cluster unless general.persistentVolumeClaimRetentionPolicy says otherwise; keep Retain in production.

Heap versus memory limits

The OpenSearch install guide recommends a heap of half the system RAM. In a pod, "system RAM" is the container's memory limit, not the Kubernetes node. The operator follows the same rule: if jvm omits -Xms and -Xmx, it uses half of resources.requests.memory, or 512 MB when no request is set. The other half is page cache for Lucene's memory-mapped files and off-heap memory. The k-NN plugin's native-memory circuit breaker, for example, defaults to 50% of the RAM left over after the heap.

bash
# Heap in use per node, against its maximum
curl -sk -u "admin:$OS_PASS" \
  "https://localhost:9200/_cat/nodes?v&h=name,node.role,heap.percent,heap.max,ram.max"

Interpretation: heap.max should be about half of each pod's memory limit. If it is close to the full limit, the JVM and the page cache compete, and the kernel OOM-kills the container instead of the JVM failing cleanly.

Set memory requests equal to limits

The operator derives the default heap from the request. If the limit is higher than the request, the pod can be scheduled on a node that cannot give it the limit, and page-cache pressure shows up as slow searches long before any error.

How do you fix vm.max_map_count on Kubernetes?

OpenSearch refuses to start on Linux when the host's vm.max_map_count is below 262144, and logs max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]. The setting belongs to the host kernel, not to the container. By default, the operator sets it to 262144 with a privileged init container in every OpenSearch pod (setVMMaxMapCount, default true).

bash
# Read the value the pod actually sees (it is the host's value)
kubectl -n search exec logs-data-0 -- cat /proc/sys/vm/max_map_count

Interpretation: anything below 262144 means the init container did not run or the node was reset. On clusters whose Pod Security admission blocks privileged containers, set the value on the nodes instead, for example in the EKS launch template or node bootstrap, and turn the init container off:

yaml
spec:
  general:
    setVMMaxMapCount: false   # nodes already run sysctl -w vm.max_map_count=262144

Interpretation: with this set to false, the operator stops adding the privileged sysctl init container. A separate init container still fixes volume ownership as root, and the user guide explains how to disable that too.

How does the operator set up the security plugin, TLS, and Dashboards?

The security plugin needs TLS on the transport layer (port 9300) and, for the operator's admin tasks, on HTTP (port 9200). With generate: true, the operator creates its own CA and signs the certificates. With perNode: true, each node gets its own transport certificate. Generated certificates last one year by default (duration: "8760h"), and the operator reissues them 30 days before expiry (rotateDaysBeforeExpiry: 30). On OpenSearch 3.x, TLS hot reload is on by default, so nodes pick up renewed certificates without a restart.

bash
# The operator generates a random admin password and stores it in a Secret
kubectl -n search get secret logs-admin-password \
  -o jsonpath='{.data.password}' | base64 -d

Interpretation: the operator uses this admin account for health checks and node draining, and Dashboards gets its own kibanaserver password in logs-dashboards-password. Give people named users instead.

For anything you do not supply in security.config.securityConfigSecret, operator 3.0.0 applies the security plugin's demo configuration files. Replace roles and role mappings before production. The operator applies your files with a logs-securityconfig-update Job and reruns it when the secret changes. If you supply your own HTTP certificates, you must also provide an admin client certificate whose DN is listed under security.tls.http.adminDn.

yaml
spec:
  security:
    config:
      securityConfigSecret:
        name: logs-securityconfig     # config.yml, roles.yml, roles_mapping.yml, ...
      adminCredentialsSecret:
        name: logs-operator-admin      # keys: username, password

Interpretation: if your secret includes roles_mapping.yml, it replaces the default file entirely. Keep the Dashboards user mapped to a role with cluster monitoring permissions (kibana_server by default), or Dashboards cannot log in.

Do not regenerate the CA in place

Deleting and regenerating the CA Secret is not a supported rotation procedure. The operator's user guide warns that it can leave nodes trusting different CAs while they reload.

How does the operator run a rolling OpenSearch upgrade?

Change spec.general.version and apply the manifest. The operator restarts nodes one at a time. After each node, it waits for green health. If some replicas can never be assigned, a stable yellow is enough: every data node has joined and no shards are initializing or relocating. It never restarts a node that holds the only active copy of a shard that should have replicas. The user guide marks downgrades and jumps across more than one major version as unsupported.

OpenSearch adds its own rules. The rolling upgrade guide sets 2.19.0 as the minimum cluster version for any upgrade to 3.x. The 3.x line reads only indexes created by 2.x, so reindex anything created on 1.x first.

Operator-driven upgrade runbook, for example 3.8.0 to 3.9.0

Seven steps; no step takes the cluster down, but health goes yellow while each node restarts

  1. Check the upgrade path

    Same major, or the next major from 2.19.0 or later. Indexes created on 1.x must be reindexed before 3.x.

    kubectl -n search get \ opensearchclusters.opensearch.org logs
  2. Confirm every plugin exists for the target

    Plugin zips must match the OpenSearch version exactly. The Prometheus exporter for 3.9.0 shipped six days after the server.

    curl -sIL -o /dev/null -w "%{http_code}\n" \ $EXPORTER_ZIP_URL_FOR_3_9_0
  3. Take a snapshot

    This snapshot is the rollback path, because the operator will not downgrade. Wait for state SUCCESS.

    PUT _snapshot/s3-snapshots/pre-upgrade
  4. Start from green

    Fix unassigned shards first; a red cluster blocks the upgrade.

    GET _cluster/health?wait_for_status=green&timeout=60s
  5. Change the version

    The patch file sets spec.general.version and spec.dashboards.version to 3.9.0. In GitOps, edit the manifest instead.

    kubectl -n search patch \ opensearchclusters.opensearch.org logs \ --type merge --patch-file v3.9.0.yaml
  6. Watch the phase

    PHASE shows UPGRADING until the last pod restarts, then RUNNING. Expect yellow between nodes.

    kubectl -n search get \ opensearchclusters.opensearch.org logs -w
  7. Verify every node's version

    All nodes should report 3.9.0 before you change anything else.

    GET _cat/nodes?v&h=name,version,node.role

Rollback point: once the first node runs the new version, there is no in-place way back. Rolling back means a new cluster at the old version, restored from the step 3 snapshot.

Leave drainDataNodes off for PVC-backed pools. It moves every shard off a node before each restart, which the user guide recommends only for emptyDir data.

How do scaling and shard rebalancing work?

To add data nodes, raise replicas on the pool. New pods join, and OpenSearch rebalances shards onto them on its own. To remove data nodes, lower replicas. With SmartScaler (spec.confMgmt.smartScaler, default true for new clusters), the operator first adds the node to cluster.routing.allocation.exclude._name, waits until it holds no shards, and only then deletes the pod. Cluster-manager nodes leave one at a time through voting configuration exclusions, whatever the SmartScaler setting.

bash
# Where are shards moving, and why is one unassigned?
curl -sk -u "admin:$OS_PASS" "https://localhost:9200/_cat/recovery?v&active_only=true"
curl -sk -u "admin:$OS_PASS" "https://localhost:9200/_cluster/allocation/explain?pretty"

Interpretation: active recoveries show each relocating shard with its source and target node. The allocation explain output names the decider that blocks an unassigned shard, such as a disk watermark or allocation awareness.

Rebalancing speed is limited on purpose. cluster.routing.allocation.cluster_concurrent_rebalance defaults to 2, and indices.recovery.max_bytes_per_sec defaults to 40 MB per node. Raise them temporarily if a scale-out has to finish inside a window, and put them back afterwards. Replicas of a restarted node wait out index.unassigned.node_left.delayed_timeout (default 1m) before OpenSearch rebuilds them elsewhere.

yaml
spec:
  general:
    nodeAttributes:
      - name: zone
        nodeLabel: topology.kubernetes.io/zone
    additionalConfig:
      cluster.routing.allocation.awareness.attributes: zone

Interpretation: nodeAttributes copies each Kubernetes node's zone label into node.attr.zone, so OpenSearch keeps a primary and its replica in different zones. Pair it with the pool's topologySpreadConstraints. Changing additionalConfig triggers a rolling restart, so use the cluster settings API for runtime changes.

Cluster health during a data pool scale-out

Illustrative output: the shape of the operator status and the _cluster/health response, not a measured cluster

ops@bastion:~
$ kubectl -n search get opensearchclusters.opensearch.org logsNAME HEALTH NODES VERSION PHASE AGElogs green 9 3.9.0 RUNNING 41d$ curl -sk -u "admin:$OS_PASS" \ "https://localhost:9200/_cluster/health?pretty"{ "cluster_name" : "logs", "status" : "green", "number_of_nodes" : 9, "number_of_data_nodes" : 4, "discovered_cluster_manager" : true, "active_primary_shards" : 120, "active_shards" : 240, "relocating_shards" : 2, "initializing_shards" : 0, "unassigned_shards" : 0, "active_shards_percent_as_number" : 100.0}

Read relocating_shards: the cluster is green while shards move to the new fourth data node, and two at a time is the default cluster_concurrent_rebalance limit. Field names follow the cluster health API reference.

How do you back up OpenSearch on Kubernetes to S3?

PVCs are not backups. Use OpenSearch snapshots to an object store. The operator installs the repository-s3 plugin from pluginsList, loads credentials into the OpenSearch keystore from a Kubernetes Secret, and registers the repositories in snapshotRepositories once the cluster is running.

yaml
spec:
  general:
    pluginsList: ["repository-s3"]
    keystore:
      - secret:
          name: s3-snapshot-credentials
        keyMappings:
          accessKey: s3.client.default.access_key
          secretKey: s3.client.default.secret_key
    snapshotRepositories:
      - name: s3-snapshots
        type: s3
        settings:
          bucket: acme-opensearch-snapshots
          region: us-east-1
          base_path: logs

Interpretation: only the keys listed under keyMappings are loaded, renamed to the keystore names the S3 plugin expects. The IAM principal needs s3:ListBucket on the bucket plus s3:GetObject, s3:PutObject, and s3:DeleteObject on its objects. Changing pluginsList on a running cluster triggers a rolling restart.

Schedule snapshots with Snapshot Management. The operator has an OpensearchSnapshotPolicy resource, but its development docs mark it and the other auxiliary CRDs deprecated for removal in operator v4. The SM API or the Terraform provider will outlast it.

json
POST _plugins/_sm/policies/daily-logs
{
  "description": "Daily snapshot of all indexes, keep 14",
  "creation": {
    "schedule": { "cron": { "expression": "0 2 * * *", "timezone": "UTC" } },
    "time_limit": "1h"
  },
  "deletion": {
    "schedule": { "cron": { "expression": "0 4 * * *", "timezone": "UTC" } },
    "condition": { "max_age": "14d", "max_count": 14, "min_count": 7 },
    "time_limit": "1h"
  },
  "snapshot_config": {
    "repository": "s3-snapshots",
    "indices": "*",
    "include_global_state": "false"
  }
}

Interpretation: GET _plugins/_sm/policies/daily-logs/_explain shows the last creation and deletion results. A restore drill matters more than the policy: restore one index under a new name with rename_pattern every month, and time it.

How do you monitor OpenSearch on Kubernetes?

With monitoring.enable: true, the operator installs the OpenSearch Prometheus exporter plugin and creates a ServiceMonitor for the Prometheus Operator. The user guide's example scrapes every 30 seconds. In 3.0.0, a missing ServiceMonitor CRD no longer blocks reconciliation, but you get no scrape target either.

yaml
spec:
  general:
    monitoring:
      enable: true
      scrapeInterval: 30s
      monitoringUserSecret: logs-monitoring-user   # read-only user, not admin
      tlsConfig:
        insecureSkipVerify: true   # the ServiceMonitor cannot take a CA from the operator yet

Interpretation: by default the exporter is scraped as the admin user; a dedicated read-only user limits the damage if Prometheus credentials leak. The exporter zip is downloaded from GitHub at pod start, so air-gapped clusters set pluginUrl to an internal mirror.

Alert on cluster status, unassigned shards, heap use, and disk use against the watermarks. On the Kubernetes side, watch the operator's events and pod restarts.

bash
kubectl -n search describe opensearchclusters.opensearch.org logs | sed -n '/Events/,$p'
kubectl -n search get pods -l opensearch.org/opensearch-cluster=logs \
  -o custom-columns=POD:.metadata.name,RESTARTS:.status.containerStatuses[0].restartCount,LAST:.status.containerStatuses[0].lastState.terminated.reason

Interpretation: OOMKilled in the last column means the container hit its memory limit; lower the heap or raise the limit. Events such as RollingRestart or Upgrade explain restarts the operator started itself.

Should you use the operator or the OpenSearch Helm chart?

There are two Helm paths, and they are easy to confuse. The operator project ships an opensearch-cluster chart that only renders an OpenSearchCluster resource, so the operator still does the work. The opensearch-project/helm-charts repository ships a standalone opensearch chart (3.9.0, matching OpenSearch 3.9.0) that creates StatefulSets directly, with no operator.

bash
helm repo add opensearch https://opensearch-project.github.io/helm-charts/
helm install logs-masters opensearch/opensearch --version 3.9.0 -f masters-values.yaml
helm install logs-data    opensearch/opensearch --version 3.9.0 -f data-values.yaml

Interpretation: the standalone chart installs one node group per release, set by nodeGroup and roles. Data releases join the managers through masterService. From OpenSearch 2.12, the chart's demo security setup also needs OPENSEARCH_INITIAL_ADMIN_PASSWORD.

Operator 3.0.0 or the standalone opensearch Helm chart

Switch tabs to see who does each day-2 job

OpenSearch operator

JobOpenSearch operator
What you installOperator chart once, then one OpenSearchCluster per cluster
Version upgradeChange general.version; one node at a time, gated on cluster health
Removing data nodesSmartScaler drains shards before deleting the pod
vm.max_map_countPrivileged init container, on by default
TLS certificatesGenerated per node, reissued 30 days before expiry
Snapshot repositoriesDeclared in general.snapshotRepositories

Standalone Helm chart

JobStandalone Helm chart
What you installOne opensearch release per node group, plus Dashboards
Version upgradehelm upgrade; StatefulSet rolling update gated by the readiness probe (TCP on 9200 by default)
Removing data nodesStatefulSet removes the highest ordinal; you drain it first by hand
vm.max_map_countOff by default (sysctlInit.enabled: false)
TLS certificatesDemo configuration by default; you mount and rotate your own
Snapshot repositoriesRegistered through the REST API

The readiness gap: the chart's README says Kubernetes waits for green after each pod, but the default readiness probe in its values.yaml only opens a TCP socket on port 9200. Add a health-based probe or use updateStrategy: OnDelete before you trust helm upgrade in production.

Choose the standalone chart if you already have tooling for upgrades, certificates, and draining. Choose the operator if you want those jobs automated, at the cost of one more component to upgrade.

Frequently asked questions

Which OpenSearch versions does the OpenSearch Kubernetes Operator support?

Operator 3.0.0, released on 25 September 2026, is tested with OpenSearch 2.19.2 up to the latest 3.x release, which is 3.9.0 as of November 2026. Operators 2.5.0 to 2.7.0 cover OpenSearch 1.3.x to 2.19.2. The operator does not block untested versions, but the project recommends testing them outside production first.

Do I need cert-manager to install the OpenSearch operator?

With the default Helm values, yes. Operator chart 3.0.14 installs a validating webhook and asks cert-manager 1.0 or later to issue its serving certificate. Without cert-manager, the install fails on the Certificate kind. You can instead supply the webhook certificate yourself through webhook.secretName, or set webhook.enabled to false.

How do I fix "vm.max_map_count [65530] is too low" on Kubernetes?

Raise vm.max_map_count to at least 262144 on every Kubernetes node that runs OpenSearch. The OpenSearch operator does this by default with a privileged init container. If your cluster blocks privileged containers, set it in the node bootstrap instead and set spec.general.setVMMaxMapCount to false. The standalone Helm chart has sysctlInit, which is off by default.

How much JVM heap should an OpenSearch pod get?

Half of the container's memory, with memory requests equal to limits. OpenSearch recommends a heap of half the system RAM, and in a pod that means the memory limit. If you omit -Xms and -Xmx, the operator uses half of the memory request. The other half serves Lucene's page cache and off-heap memory such as k-NN.

Can the OpenSearch operator upgrade from OpenSearch 2.x to 3.x?

Yes, as a rolling upgrade, if the cluster runs 2.19.0 or later, as OpenSearch requires for any 3.x upgrade. Change spec.general.version and the operator restarts one node at a time. Reindex any index created on OpenSearch 1.x first, because 3.x reads only indexes created by 2.x. Take a snapshot before you start.

Key takeaways

  • Run operator 3.0.0 with cert-manager, and write new resources against opensearch.org/v1. Moving from operator 2.x restarts every node once.
  • Use separate node pools for cluster managers, data, and ingest, with a PodDisruptionBudget and zone spread on each.
  • Size the heap at half the container memory, with requests equal to limits, and leave the rest to the page cache.
  • Treat every version upgrade as one-way: snapshot first, start from green, and check that plugin zips exist for the target version.
  • Schedule S3 snapshots with Snapshot Management rather than the deprecated policy CRD, and test restores on a schedule.

Run OpenSearch on Kubernetes with JusDB

JusDB's OpenSearch on Kubernetes team can review your OpenSearchCluster manifests or Helm values, plan the move from operator 2.x to 3.0 or from OpenSearch 2.19 to 3.x, and write the upgrade and restore runbooks for your clusters.

Bring your current manifests, operator and OpenSearch versions, node pool sizes, and snapshot setup. Your team approves every production change.

Share this article

Database engineering notes

Articles like this one, in your inbox. No spam, unsubscribe anytime.

JusDB Team

Official JusDB content team

Keep reading

Moving a Multi-Database Valkey Instance into a 9.x Cluster

Move a Valkey instance that uses SELECT 1 to 15 into a 9.x cluster: keep or remap databases, copy each one, verify counts per database, and keep a way back.

Redis18 minNov 10, 2026
Read

Valkey Keyspace Notifications in Production: Expired Events, hexpired and Cluster Mode

Run Valkey keyspace notifications in production: why expired and hexpired events lag the TTL, how to subscribe on every cluster node, and when a stream is safer.

Redis20 minNov 10, 2026
Read

Valkey GLIDE in Production: AZ-Affinity Reads and Migrating from Legacy Clients

Configure Valkey GLIDE AZ-affinity reads on servers and clients, learn what GLIDE 2.5.3 does not support, and migrate from redis-py, Jedis, ioredis, or go-redis one service at a time.

Redis20 minNov 10, 2026
Read