Jump to content

Data Platform/Systems/OpenSearch-on-K8s/Administration

From Wikitech

OpenSearch on K8s

Intended audience: SREs who operate the OpenSearch on K8s platform.

If you are a service owner, or are interested in deploying on the platform, you may be more interested in this page.

Dashboards and Alerts

Dashboards

See our OpenSearch on K8s dashboard. The dashboard contains a number of metrics we use to gauge health.

Look out for the 'interval' dropdown

Unlike other WMF dashboard, OpenSearch on K8s contains an "interval" dropdown, used to get a rolling average. If the panels are showing up blank, try to lengthen the interval. But you should also realize if you make the interval too long, it might confuse things as well.

For example, we had an incident where latency spiked to about 10x its normal value. After the incident was resolved, we still had the interval set to 24h. Thus, it was still including periods of extremely high latency in its graphs, making it seem like the incident was still ongoing. We are working to address this confusion in in T417230 .

Important Dashboard Panels
  • The "network probes" panel, showing the latency of all OpenSearch clusters per DC.
  • Disk Usage - Nothing good happens when you run out of disk space!
  • Memory usage - amount of memory used per pod
  • Search latency

You can find alerts in the Wikimedia Foundation's alert repo .

Alerting

These alerts will notify DPE SRE once your cluster is in "production" state in service.yaml in Puppet.

As stated in the "Service Availability/Expectations" section, DPE SRE will respond to the alerts within business hours.

The table below describes whether or not the application owner has permissions to fix the problem. This is not meant to imply the application owner must fix the problem. Generally speaking, the platform owner (DPE SRE) is the right team to deal with alerts in non-emergency situations.

Alert Probable cause Probable solution Can application owner fix this with their current perms? Should they do this in a non-emergency situation?
OpenSearchNode(Low,High)DiskWatermarkReached The OpenSearch pods are out of disk space Expand disk space Yes No
OpenSearchClusterAtLeastOneRedIndex At least one of the indices is missing data. Delete the broken index/regenerate the data Yes Only if the cause is well-understood (I lost data on an index I don't care about, let me completely delete the index to make the alerts go away).
OpenSearchBulkRequestsRejectionJumps The OpenSearch pods are overwhelmed 1. Determine cause (increased traffic? dying K8s worker?)

2. Take action based on cause (increase resources, block attacker, etc)

Unknown Only if the cause is well-understood (I just tried to ingest a 1 trillion line bulk JSON file into OpenSearch).
OpenSearchJVMHeapUseHigh The OpenSearch pods are using too much memory

- Check pod lifetimes to see if the pods are getting frequently OOMKilled

- Typical Java memory troubleshooting.

- Consider raising memory requests/limits in Kubernetes up to a maximum of 16 GB

Yes No

Operations

Deploying a New OpenSearch on K8s Cluster

  • Begin by following the general instructions for deploying a new Kubernetes service.
  • Create/merge a patch that adds your new namespace to the list of tenantNamespaces in deployment-charts repo (example CR). OpenSearch uses RBD, so you want to change the helmfile.d/admin_ng/values/dse-k8s-eqiad/ceph-csi-rbd-values.yaml file.
  • After merging, you will need to deploy admin_ng for both dse-k8s clusters.
  • Create/merge a patch that increases default resources for your new namespace (example patch) . This is needed because the version of the operator chart we use (2.7.0) does not allow changing the resources allotted to the bootstrap pod, and it needs at least 2 GB RAM to stand up the cluster.
  • Create and add secrets. By default, you will need the following secrets:
Secret Name Details
username opensearch
password Used to access the OpenSearch REST API via basic auth. Read/write access to OpenSearch indices only; cannot do cluster operations.
hashed_password Used to populate the opensearch security YAML config, which in turn is pushed to the OpenSearch Security API via securityadmin.sh . This must be a bcrypted hash. You can generate this with htpasswd , Python's bcrypt library, etc.
Secret Name Details
username opensearch-operator
password Used to access the OpenSearch REST API via basic auth. Used by OpenSearch operator and power users for cluster-level operations
hashed_password Used to populate the opensearch security YAML config, which in turn is pushed to the OpenSearch Security API via securityadmin.sh . This must be a bcrypted hash. You can generate this with htpasswd , Python's bcrypt library, etc.

Better User Management is Coming

We should have better user management once this Phab ticket gets some attention.


  • Create and merge a patch that tells the OpenSearch operator to watch your new namespace. Example patch

Phases of Deployment

1. The bootstrap, first master, and security pods are created:

NAME                                                              READY   STATUS    RESTARTS   AGE
cluster-bootstrap-0                                               1/1     Running   0          61s
cluster-masters-0                                                 0/1     Running   0          61s
cluster-securityconfig-update-f7wmp                               1/1     Running   0          61s

During this phase, the securityconfig pod (which creates the .opendistro_security index, somewhat akin to the MySQL users table) continually polls the cluster, waiting for the cluster to reach yellow status (in this context, when the bootstrap node and initial master node form a cluster). You will see a lot of Waiting to connect to the cluster in the security config pod, this is normal for the first few minutes of a deploy.

If you look at the bootstrap pod's logs, you might see logs complaining that the .opendistro_security index doesn't exist. This is also normal for the first few minutes of a deploy.

2. The cluster has formed, but not all the masters have deployed:

$ kubectl get pod
NAME                                                              READY   STATUS      RESTARTS   AGE
cluster-bootstrap-0                                               1/1     Running     0          10m
cluster-masters-0                                                 1/1     Running     0          10m
cluster-masters-1                                                 0/1     Running     0          8m
cluster-securityconfig-update-f7wmp                               0/1     Completed   0          10m

This phase begins once the securityconfig pod creates the .opendistro_security index. As shown above, this pod's status will change to Completed and the operator will create any additional masters.

3. The cluster is fully deployed:

$ kubectl get pod
NAME                                                              READY   STATUS      RESTARTS   AGE
cluster-masters-0                                                 1/1     Running     0          63m
cluster-masters-1                                                 1/1     Running     0          62m
cluster-masters-2                                                 1/1     Running     0          60m
cluster-securityconfig-update-f7wmp                               0/1     Completed   0          63m

This is what the cluster looks like after a successful deploy. Note that the operator has removed the bootstrap pod.


Verifying Cluster Access

Start a port-forward in one terminal:

$ kubectl port-forward --address 0.0.0.0 cluster-masters-0 :9200
Forwarding from 0.0.0.0:45679 -> 9200

In a second terminal, set your credentials and port. You can find the username and password in /srv/git/private/hieradata/role/common/deployment_server/kubernetes.yaml on the Puppet server.

$ PW=user:password  # used for basic auth with cURL
$ PT=45679          # must match the port from the port-forward above

Test with authentication:

$ curl -ks -u ${PW} https://0:${PT}/_prometheus/metrics

Or without authentication (read-only access is enabled by default):

$ curl -ks https://0:${PT}/_cat/nodes
$ curl -ks https://0:${PT}/_cat/health
$ curl -ks https://0:${PT}/_prometheus/metrics

Deploying on an existing cluster

From the deployment server, run kube_env ${namespace}-deploy dse-k8s-${site} . This sources all the environment variables you need to run helmfile apply, delete pods, etc...

Changing Resources on a Live Cluster

The chart has several places to change requests/limits.

The resource configuration that applies to the cluster nodes is under

opensearchCluster:
  nodePools:
    - component: masters
      roles:
        - "master"
        - "data"
      resources:
        requests:
          memory: "4Gi"
          cpu: "2000m"
        limits:
          memory: "4Gi"
          cpu: "2000m"


As of this writing, that configuration is applied via this file in our deployment-charts repo. Which means if you want to override it, you need to add it to the values.yaml file specific to your deployment and/or helmfile release.

Again, as of this writing, the operator does not detect changes to the resources or source image.

As such, you'll have to delete the pods one-by-one to apply the resource changes. Note that since the PersistentVolumes and their PersistentVolumeClaims are not deleted, the data is not at risk. In practice it feels a lot more like a very quick reboot (delete the pod, the operator replaces it immediately, the new pod is connected to the existing OpenSearch data). It typically only takes a minute or two after deletion for the cluster to get back into its healthy "green" state.

But you should still take your time and ensure the cluster is healthy before moving on to the next pod. You can use the following API calls to check cluster health:

$ curl -H 'Accept:Application/yaml' -XGET https://opensearch-test.discovery.wmnet:30443/_cluster/health
---
cluster_name: "opensearch-test"
status: "yellow"
timed_out: false
number_of_nodes: 3
number_of_data_nodes: 3
discovered_master: true
discovered_cluster_manager: true
active_primary_shards: 8
active_shards: 18
relocating_shards: 0
initializing_shards: 0
unassigned_shards: 2
delayed_unassigned_shards: 0
number_of_pending_tasks: 0
number_of_in_flight_fetch: 0
task_max_waiting_in_queue_millis: 0
active_shards_percent_as_number: 90.0
---

This cluster is yellow. If it stays in yellow for more than a few minutes after launching a new pod, further investigation is required. Did the new pod join the cluster? Are shards moving to it? If there are under-replicated shards, are they on the pod you want to delete?

$ curl -XGET https://opensearch-test.discovery.wmnet:30443/_cat/nodes
10.67.29.29   8 56 1 0.80 1.39 1.72 dm cluster_manager,data - opensearch-test-masters-2
10.67.28.200 36 83 4 0.83 1.13 1.45 dm cluster_manager,data * opensearch-test-masters-0

This cluster is missing a pod, wait for the third pod to finish deploying before deleting another pod.

Example: Rolling Resource Update

1.

$ kubectl get pod -o yaml opensearch-test-masters-0 | grep -i memory
        memory: 2Gi
        memory: 2Gi

We have applied the change via helmfile, but the operator hasn't detected the changes.

2.

$ kubectl delete pod opensearch-test-masters-0

We've deleted one of the pods to force its replacement.

3.

$ kubectl get pod -o yaml opensearch-test-masters-0 | grep -i memory
        memory: 4Gi
        memory: 4Gi

After a few minutes, the pod has been replaced and it has the new specs.

The operator should be handling this automatically. We'll continue investigating, but it's likely this will be fixed when we move to a newer version of the operator and/or helm chart.

Renewing TLS Certificates

The current version of the OpenSearch operator does not currently support hot reloading of certificates (See phab:T414217 for tracking automatic certificate reload in a future operator version).

Note that kubectl get cert is NOT sufficient to see which certificate is presented by the cluster, you have to hit the endpoint directly:

# certificate for endpoint
$ kubectl port-forward service/${K8S_NAMESPACE%-deploy}-bulk 9200 &
$ gnutls-cli --print-cert localhost:9200 | grep --line-buffered CN=opensearch-wmf | grep --line-buffered -o 'expires[^,]*'
expires 2026-06-16 02:51:00 UTC

# certificate for opensearch itself
$ kubectl exec -it ${K8S_NAMESPACE%-deploy}-masters-0 -- openssl x509 -enddate -noout -in /usr/share/opensearch/config/tls-http/tls.crt
notAfter=Jun  2 12:29:00 2026 GMT

1) Follow Kubernetes/Kubectl/Cheat Sheet#Force refresh of a cert-manager certificate

$ kube-env ${NAMESPACE}-deploy dse-k8s-eqiad
$ kubectl patch certificate/opensearch-wmf --type=json --patch='[{"op": "replace", "path": "/spec/renewBefore", "value": "1440h"}]'
$ sleep 10
$ kubectl patch certificate/opensearch-wmf --type=json --patch='[{"op": "remove", "path": "/spec/renewBefore"}]'

2) Rolling-restart all pods in the cluster to take this new certificate into account

# on deploy2002.codfw.wmnet
# I suggest running this in a tmux if the cluster is large
$ /home/brouberol/restart-sts-pods -e dse-k8s-eqiad -n ${NAMESPACE} ${NAMESPACE}-masters
$ /home/brouberol/restart-sts-pods -e dse-k8s-eqiad -n ${NAMESPACE} ${NAMESPACE}-data # might not exist

What do to when the pods get stuck

If you're running the script and you see

Readiness: ............................................................................

for more than 5 minutes:

A. Check container status:

kubectl get po | grep -i creat
opensearch-semantic-search-data-1      0/1     ContainerCreating   0             3h22m

If you see a pod stuck in "ContainerCreating", it's likely the physical volume (PV) associated with that pod was never properly detached from the previous pod's host.

B. Find the persistent volume claim (PVC) associated with the pod:

kubectl get pvc | grep opensearch-semantic-search-data-1
data-opensearch-semantic-search-data-1      Bound    pvc-54990147-2c50-47b7-9b5a-3d87d3c387e6   170Gi      RWO

The third field (pvc-54990147-2c50-47b7-9b5a-3d87d3c387e6 in this example) is the PV ID. Copy it to your clipboard.

C. Figure out which k8s worker is holding on to the PV:

Login to a cumin host and check all k8s workers in the datacenter for lingering mounts:

sudo cumin A:dse-k8s-worker-eqiad 'mount | grep pvc-54990147-2c50-47b7-9b5a-3d87d3c387e6'
24 hosts will be targeted:
dse-k8s-worker[1001-1019,1024-1028].eqiad.wmnet
OK to proceed on 24 hosts? Enter the number of affected hosts to confirm or "q" to quit: 24
===== NODE GROUP =====
(1) dse-k8s-worker1025.eqiad.wmnet
----- OUTPUT of 'mount | grep pvc...b5a-3d87d3c387e6' -----
/dev/rbd4 on /var/lib/kubelet/plugins/kubernetes.io/csi/pv/pvc-54990147-2c50-47b7-9b5a-3d87d3c387e6/globalmount/0001-0024-6d4278e1-ea45-4d29-86fe-85b44c150813-0000000000000007-a659f72b-1e1f-11f1-be9c-5a028bb344e6 type ext4 (rw,relatime,stripe=1024,_netdev)

In the above example, dse-k8s-worker1025.eqiad.wmnet is the host that's holding onto the mount. Let's go there next.

D. Unmount the PV manually from the k8s worker:

#umount /dev/rbd4

It will take 5-10 minutes for the PV to be completely freed up, but that should do it.

Note that you can also delete the pod and its PVC to get the same effect, but this is not recommended because:

  1. It completely wipes out the data for that pod. OpenSearch is self-healing, so it will recover the data from other pods. But it's better not to waste resources on the recovery process if we don't have to.
  2. The PV will not be deleted. It's hard to find these unused PVs (FIXME:add HOWTO for finding them) and they waste space on the Ceph cluster.

3) Verify that your endpoint is serving the new certificate:

$ gnutls-cli --print-cert ${NAMESPACE}.svc.eqiad.wmnet:30443 | grep expire
  - subject `CN=opensearch-wmf', issuer `CN=discovery,OU=SRE Foundations,O=Wikimedia Foundation\, Inc,L=San Francisco,C=US', serial 0x69ad031ab6ce85e4abf0a1be545c95e0d716daef, RSA key 2048 bits, signed using ECDSA-SHA512, activated `2026-02-05 19:35:00 UTC', expires `2026-03-05 19:35:00 UTC', pin-sha256="AcCR/RPH1ogjJfEnPi91ltR4uCLEY7bFtwcVIgSFfCU="

4) Confirm that all certificate alerts have cleared. The Blackbox probes that monitor certificate expiration come from outside K8s, so they could be hitting any one of the pods. If the alerts don't all clear, you may have forgotten to cycle through all of the pods.

Completely Wiping Out/Redeploying an OpenSearch on K8s Cluster

If you need to wipe out and redeploy your cluster, here are the steps:

  • Login to the deploy host
  • Become the -deploy user: kube_env ${namespace}-deploy dse-k8s-${site}
  • Navigate to the correct helmfile.d directory:
    cd /srv/deployment-charts/helmfile.d/dse-k8s-services/${namespace}
    
  • Destroy using helmfile command helmfile -e dse-k8s-${site} -i destroy
  • Check for/destroy unused PVCs:

1.

kubectl get pvc
NAME                                        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
data-opensearch-semantic-search-masters-0   Bound    pvc-fc747e0f-cafc-40c4-8345-bbf7abe88f3c   40Gi       RWO            ceph-rbd-ssd   21d
data-opensearch-semantic-search-masters-1   Bound    pvc-fde85989-6cef-4021-b3e7-10e362f3f997   40Gi       RWO            ceph-rbd-ssd   21d
data-opensearch-semantic-search-masters-2   Bound    pvc-497f0df2-22a5-4330-b9a6-9d78629f4593   40Gi       RWO            ceph-rbd-ssd   21d
data-opensearch-semantic-search-masters-3   Bound    pvc-4fe56d26-2613-4936-9cb3-4ff9cf41279c   30Gi       RWO            ceph-rbd-ssd   154m
data-opensearch-semantic-search-masters-4   Bound    pvc-9d3f9612-0230-43e8-b685-452ad055817e   30Gi       RWO            ceph-rbd-ssd   153m
data-opensearch-semantic-search-masters-5   Bound    pvc-18431dad-59c1-42d6-bdf7-cc34e3ef50d4   30Gi       RWO            ceph-rbd-ssd   151m
data-opensearch-semantic-search-masters-6   Bound    pvc-6e87fe5f-b966-4662-af9e-c0fc7f537de4   30Gi       RWO            ceph-rbd-ssd   150m

2.

for n in {0..6}; do kubectl delete pvc data-${namespace}-masters-${n}; done

3. Verify things are completely gone:

kubectl get all
kubectl get pvc

After that, you should be clear to redeploy.

API Calls

Audit Logging

PT=43885; curl -H 'Content-type: Application/json' -XPOST -k -u ${PW}  https://0:${PT}/security-auditlog-2025.10.31/_search?pretty

Forcing Shard Reallocation

! You will need the `operator` OpenSearch user as opposed to the typical `opensearch` user!

Dynamic and Static Configuration settings

Aspect Static Dynamic
Where to set opensearch.yml. If using the opensearch-cluster helm chart, additionalConfig is added to opensearch.yml set via the OpenSearch API
Activating You have to restart the OpenSearch service. Theoretically, this should be handled by the operator, but you may need to rollout restart to apply the settings if the settings don't change Settings are active as soon as OpenSearch accepts your API call
How to tell if setting is known to OpenSearch You will NOT see the changes in configMaps or inside the pod's opensearch.yml, as the operator adds new settings as environment variables. You can run env inside the pod to check if the setting exists. Check via OpenSearch API
How to tell if setting is applied OpenSearch config follows Postel's law, and it will accept invalid config (For example, see this change where I added config that OpenSearch accepted, but never really worked). You may need to do more investigation to verify the setting is:

1) correct and 2)active. OpenSearch will ignore some settings even when they are correct and active if they would have detrimental effects on the cluster (for example, it will allow shards to land on a banned node if there is no other place to put them).

Same

Disk Usage

How to Check

By default, the OpenSearch on K8s pods have 30 GB disks. You can check disk usage via the following API call:

$ curl -s  https://opensearch-ipoid.svc.eqiad.wmnet:30443/_cat/allocation?v
shards disk.indices disk.used disk.avail disk.total disk.percent host                       ip           node
    21        8.3gb     8.3gb     20.9gb     29.3gb           28 opensearch-ipoid-masters-2 10.67.28.219 opensearch-ipoid-masters-2
    21        4.6gb     4.6gb     24.6gb     29.3gb           15 opensearch-ipoid-masters-0 10.67.26.116 opensearch-ipoid-masters-0
    22         13gb      13gb     16.2gb     29.3gb           44 opensearch-ipoid-masters-1 10.67.28.6   opensearch-ipoid-masters-1

Disk usage is also visible via this panel from the OpenSearch on K8s dashboard

How to Expand

1. Merge a change that raises the disk size. Note that in the current version of the opensearch-cluster helm chart (2.7.0), you will have to override the entire nodePools object within the opensearchCluster object, see this CR for an example.

2. Apply the change using our standard deployment process (helmfile apply).

3. The change should be visible in OpenSearch immediately. Verify by using the same call as above:

$ curl -s  https://opensearch-ipoid.svc.eqiad.wmnet:30443/_cat/allocation?v
shards disk.indices disk.used disk.avail disk.total disk.percent host                       ip           node
    25        6.4gb     6.4gb     32.7gb     39.2gb           16 opensearch-ipoid-masters-0 10.67.29.37  opensearch-ipoid-masters-0
    24        4.6gb     4.6gb     34.5gb     39.2gb           11 opensearch-ipoid-masters-2 10.67.30.227 opensearch-ipoid-masters-2
    24       12.8gb    12.8gb     26.3gb     39.2gb           32 opensearch-ipoid-masters-1 10.67.28.5   opensearch-ipoid-masters-1

Now we have ~40GB of disk space per pod instead of the previous ~30GB!

OpenSearch versions

Current versions

See our Docker images repo for the available versions.

Version update cadence

The platform owners will update the OpenSearch version at least once per quarter. Since our docker images are sourced from upstream Debian packages, we can only update as far as what's available there.

Application owners are welcome to request newer versions at any time.

OpenSearch Plugins

If your application requires a plugin that's not in the default OpenSearch install, open a Phab task to request the plugin. Plugins will be evaluated on a case-by-case basis. Criteria to be evaluated:

  • Whether or not the plugin is built by the OpenSearch team
  • Whether or not the plugin is owned by WMF
  • Security
  • Resources consumed by plugin
  • Responsiveness of plugin developers

You can see a list of existing plugins by looking for the phrase opensearch-plugin install in the Blubberfile that creates the Docker image for your OpenSearch version (example ). FIXME: improve discoverability of installed plugins


Updating the OpenSearch Version

The OpenSearch image is built from Debian packages. We mirror the upstream packages in repo components such as

thirdparty/opensearch3 trixie-wikimedia

Before we can update the OpenSearch docker image version, we'll need to update the OpenSearch package version we mirror.

From the apt servers, check for updates. Example command:

$ reprepro --component thirdparty/opensearch3 checkupdate trixie-wikimedia

If updates are available, you'll see something like:

Calculating packages to get...
Updates needed for 'trixie-wikimedia|thirdparty/opensearch3|amd64':
'opensearch': '3.3.2' will be upgraded to '3.5.0' (from 'thirdparty/opensearch3'):
 files needed: pool/thirdparty/opensearch3/o/opensearch/opensearch_3.5.0_amd64.deb

Note that reprepro only keeps a single version of the package. Thus, after you update the package version, you will need to immediately update the Docker image repo with new version of the package, including all plugins and their checksums.

reprepro does support rolling back to an older package version if things go wrong, assuming that the package is still available from upstream.

Miscellenia

Block device optimizations

Per phab:T419041 , we have a script that sets /sys/block/rbd[0-9]*/queue/read_ahead_kb on all OpenSearch pods. This script run on all dse k8s workers and is applied every 5 minutes via a systemd timer. The script requires `crictl` to function.

Troubleshooting

Check the journal on the dse-k8s-worker:

journalctl -u set-rbd-readahead.service
Mar 24 19:15:00 dse-k8s-worker2001 set-rbd-readahead.py[922404]: 2026-03-24 19:15:00,455 INFO rbd5 read_ahead_kb already set to 64, not making changes...

To prove it's making changes:

journalctl -u set-rbd-readahead.service --since '1 day ago' | grep Set | egrep -v "(Starting|Finished)"
Mar 23 19:40:01 dse-k8s-worker2001 set-rbd-readahead.py[3605610]: 2026-03-23 19:40:01,014 INFO Set rbd3 read_ahead_kb -> 64
Mar 23 19:50:01 dse-k8s-worker2001 set-rbd-readahead.py[3616266]: 2026-03-23 19:50:01,010 INFO Set rbd5 read_ahead_kb -> 64
Mar 23 20:05:00 dse-k8s-worker2001 set-rbd-readahead.py[3633968]: 2026-03-23 20:05:00,994 INFO Set rbd6 read_ahead_kb -> 64

FIXME: Improve logging to show prior read_ahead_kb setting and make it easier to follow which pods are associated with the RBD device.