Also at Deasil Works · txn2 · Plexara
Profiles GitHub · X · LinkedIn
Theme Light · Auto · Dark
Professional notes by Craig Johnston
long-form, short-form, working drafts · since 2008
VOL. XIX · MMXXVI
111 NOTES IN PRINT
FOLIO XXXIV 2018-07-25 · 11 MIN · LONG-FORM

Remote Query Elasticsearch on Kubernetes

Local workstation-based microservices development

Diagram · folio xxxiv
flowchart LR
  DEV([Local IDE or curl])
  DEV --> KFW{port-forward channel}
  KFW --> SVC[Elasticsearch Service]
  SVC --> POD[(ES pod)]
  POD -.->|JSON results| DEV

Developing on our local workstations has always been a conceptual challenge for my team when it comes to remote data access. Local workstation-based development of services that intend to connect to a wide range of remote services that may have no options for external connections poses a challenge. Mirroring the entire development environment is possible in many cases, just not practical.

In days before Kubernetes, writing code in IDEs on our local workstation meant we had only a few options for developing server-side-API-style services that needed to connect to a database. We could set up a database server on our local workstation manually or use packages like MAMP/WAMP, or run big virtual servers managed with Vagrant. Even after we got the database running, we needed a good set of data to work with, and that often meant asking a DBA or Sysadmin for SQL dumps from an environment in which we have no access.

Kubernetes gives us new ways to connect to remote services, some of these services are never intended to be connected to outside the cluster, having no ingress or firewall rules, yet we still need to connect from the outside when developing on a local workstation.

Port-forward a Kubernetes service:

kubectl port-forward svc/elasticsearch 9200:9200 -n the-project

In the command above, I’m forwarding the Kubernetes Service elasticsearch, and its port 9200 from the-project namespace to port 9200 on my local workstation. Studying this command may be enough to get you going, and if you followed my guide, Production Grade Elasticsearch on Kubernetes, you could run this same command.

If you don’t understand that command, or running it produces an error, read on.


§2026 Update

Good news: this one is almost entirely still current. kubectl port-forward to a Service, then querying it with curl or Postman, works exactly as shown. That is a Kubernetes mechanic and it has not changed. Two things to flag.

I query OpenSearch now, not Elasticsearch. The REST calls in this post, _cat/indices, _mapping, and _search, are identical on OpenSearch, so everything here transfers directly. The license background for the move is in the 2026 update on the Elasticsearch setup post.

The response shapes below are from Elasticsearch 6 and changed in 7. Two visible differences if you run these against a current Elasticsearch or OpenSearch:

  • Mapping types are gone. The _mapping output below nests fields under a doc type ("mappings": { "doc": { "properties": ... } }), and the search hits carry a "_type": "doc". Elasticsearch removed mapping types in 7.0. Today the mapping is just "mappings": { "properties": ... } with no type level, and there is no _type in results.
  • hits.total is now an object. The search output shows "total": 3. In 7.0 and later (and on OpenSearch) that became "total": { "value": 3, "relation": "eq" }.

The queries themselves, the _search body, the range filter, and the _cat calls, are unchanged.


Original article below. Everything from here down is the post as originally written. The 2026 Update above covers what’s changed since.

§Port Forward elasticsearch Kubernetes Service

Since Kubernetes v1.10, the command kubectl port-forward “allows using resource name, such as a service name, to select a matching pod to port forward to.” kubectl port-forward also works with deployments, daemon sets, replica sets and so on.

Get a list of services running in the-project namespace:

kubectl get services -n the-project

Services expose a port and map it to another port on a pod. In the case of connecting to Elasticsearch, we don’t care what pod we are connecting to, in fact, we want to connect the same way our application will, and that is through a Service. It so happens that the elasticsearch Service in the-project namespace uses Port 9200, the default for elasticsearch; however this could have easily been customized to some other port, and this is why we always want to list our services and ports they expose.

kubectl get services

Now that we have the Service name and port we can issue a port-forward command with kubectl:

To port-forward the Elasticsearch Service to my local workstation I need to provide a local port that is available. I would need to choose a port other than 9200 if I had something already on that port.

kubectl port-forward svc/elasticsearch 9200:9200 -n the-project

The following diagram is a breakdown of the kubectl port-forward command:

kubectl port-forward

If the above command is running without error, open another terminal and use Curl to get the cluster stats:

curl "http://localhost:9200/_cluster/stats?human&pretty"

If the command above returned a large JSON object describing the Elasticsearch cluster in your Kubernetes environment, congratulations. If you encountered an error, read on.

Permission Denied

If kubectl port-forward claims you don’t have permission to port forward a Service on the cluster, you need to review the RBAC Permissions for the credentials used for remote access to the Kubernetes cluster.

If you are getting an error along the lines of 127.0.0.1:9200, 16): Connection refused it is most likely that Elasticsearch is not binding to all local interfaces on the Pod and is defaulting to its hostname. Since you can not provide a hostname through simple port-forward, you want to check your elasticsearch.yml configuration and ensure that Elasticsearch listens on all interfaces by specifying network.host: 0.0.0.0. However, if you are using the configuration from the article Production Grade Elasticsearch on Kubernetes, the value of network.host is set from the environment variable NETWORK_HOST for the Pod. See the Deployment for Client and Ingest Nodes configuration, paying special attention to the env: section where the environment variable setting NETWORK_HOST is 0.0.0.0.

§Port-forwarding RBAC Permissions

In the article, Kubernetes Team Access - RBAC for developers and QA, I set up the Roles developer and deployer. In this example, I am using the kubectl config context with the developer service account. Whatever Role you are bound to must have the following RBAC rules applied to port forward:

- apiGroups:
  - '*'
  resources:
  - 'pods/portforward'
  - 'services/portforward'
  verbs:
  - create

See Roles and RoleBindings for a better understanding of how these rules apply to our access.

§Postman

In the following examples, I use my favorite (and free) HTTP client, Postman. When working with HTTP based APIs, I typically use Postman. However, I’ll be exporting the Postman calls to curl commands so they can also be displayed here and tested on the command line.

If you want access to my Postman calls to run for yourself:

Run in Postman

§Query Elasticsearch Indices and Mappings

If the command kubectl port-forward svc/elasticsearch 9200:9200 -n the-project is running without error in a terminal we are ready to start working with Elasticsearch on our local workstation through port 9200.

§Get Indices

Get a list of Indices:

curl -X GET \
  'http://localhost:9200/_cat/indices?format=json&pretty=true'

Output:

[
    {
        "health": "green",
        "status": "open",
        "index": ".kibana",
        "uuid": "9qr9qjD4T1-2KhWi70zcYw",
        "pri": "1",
        "rep": "0",
        "docs.count": "6",
        "docs.deleted": "0",
        "store.size": "33.1kb",
        "pri.store.size": "33.1kb"
    },
    {
        "health": "green",
        "status": "open",
        "index": "wx-rtbeat-2018.07",
        "uuid": "nMHK2dxfTbmbnLubKvLTRQ",
        "pri": "5",
        "rep": "1",
        "docs.count": "5566",
        "docs.deleted": "0",
        "store.size": "4.8mb",
        "pri.store.size": "4.8mb"
    }
]

The output of _cat/indices returns a JSON array of Indices. In the article, High Traffic JSON Data into Elasticsearch on Kubernetes I demonstrate collecting weather data from the Dark Sky API. The weather data populates an Elasticsearch index based on the year and month. As of the time of this writing, I only have one index wx-rtbeat-2018.07. The index .kibana is used by my Kibana service.

§Get Index Mapping

The Get Mapping call gives us the field mapping of an index or a group of indexes. In this case, we want all of the fields any index starting with wx-rtbeat-* (* being a wildcard.) Postman generates the Curl call with the URL encoding %2A for the * character.

curl -X GET \
  http://localhost:9200/wx-rtbeat-%2A/_mapping

Output:

{
    "wx-rtbeat-2018.07": {
        "mappings": {
            "doc": {
                "_meta": {
                    "version": "7.0.0-alpha1"
                },
                "dynamic_templates": [
                    {
                        "fields": {
                            "path_match": "fields.*",
                            "match_mapping_type": "string",
                            "mapping": {
                                "type": "keyword"
                            }
                        }
                    },
                    {
                        "strings_as_keyword": {
                            "match_mapping_type": "string",
                            "mapping": {
                                "ignore_above": 1024,
                                "type": "keyword"
                            }
                        }
                    }
                ],
                "date_detection": false,
                "properties": {
                    "@timestamp": {
                        "type": "date"
                    },
                    "beat": {
                        "properties": {
                            "hostname": {
                                "type": "keyword",
                                "ignore_above": 1024
                            },
                            "name": {
                                "type": "keyword",
                                "ignore_above": 1024
                            },
                            "version": {
                                "type": "keyword",
                                "ignore_above": 1024
                            }
                        }
                    },
                    "clientIp": {
                        "type": "keyword",
                        "ignore_above": 1024
                    },
                    "error": {
                        "properties": {
                            "code": {
                                "type": "long"
                            },
                            "message": {
                                "type": "text",
                                "norms": false
                            },
                            "type": {
                                "type": "keyword",
                                "ignore_above": 1024
                            }
                        }
                    },
                    "fields": {
                        "type": "object"
                    },
                    "rxtxMsg": {
                        "properties": {
                            "key": {
                                "type": "keyword",
                                "ignore_above": 1024
                            },
                            "label": {
                                "type": "keyword",
                                "ignore_above": 1024
                            },
                            "payload": {
                                "properties": {
                                    "alerts": {
                                        "properties": {
                                            "description": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "expires": {
                                                "type": "float"
                                            },
                                            "regions": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "severity": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "time": {
                                                "type": "float"
                                            },
                                            "title": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "uri": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            }
                                        }
                                    },
                                    "currently": {
                                        "properties": {
                                            "apparentTemperature": {
                                                "type": "half_float"
                                            },
                                            "cloudCover": {
                                                "type": "half_float"
                                            },
                                            "dewPoint": {
                                                "type": "half_float"
                                            },
                                            "humidity": {
                                                "type": "half_float"
                                            },
                                            "icon": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "nearestStormBearing": {
                                                "type": "long"
                                            },
                                            "nearestStormDistance": {
                                                "type": "long"
                                            },
                                            "ozone": {
                                                "type": "half_float"
                                            },
                                            "precipIntensity": {
                                                "type": "half_float"
                                            },
                                            "precipIntensityError": {
                                                "type": "half_float"
                                            },
                                            "precipProbability": {
                                                "type": "half_float"
                                            },
                                            "precipType": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "pressure": {
                                                "type": "half_float"
                                            },
                                            "summary": {
                                                "type": "keyword",
                                                "ignore_above": 1024
                                            },
                                            "temperature": {
                                                "type": "half_float"
                                            },
                                            "time": {
                                                "type": "long"
                                            },
                                            "uvIndex": {
                                                "type": "half_float"
                                            },
                                            "visibility": {
                                                "type": "half_float"
                                            },
                                            "windBearing": {
                                                "type": "half_float"
                                            },
                                            "windGust": {
                                                "type": "half_float"
                                            },
                                            "windSpeed": {
                                                "type": "half_float"
                                            }
                                        }
                                    },
                                    "latitude": {
                                        "type": "half_float"
                                    },
                                    "longitude": {
                                        "type": "half_float"
                                    },
                                    "offset": {
                                        "type": "byte"
                                    },
                                    "timezone": {
                                        "type": "keyword",
                                        "ignore_above": 1024
                                    }
                                }
                            },
                            "producer": {
                                "type": "keyword",
                                "ignore_above": 1024
                            },
                            "seq": {
                                "type": "long"
                            },
                            "time": {
                                "type": "date"
                            },
                            "uuid": {
                                "type": "keyword",
                                "ignore_above": 1024
                            }
                        }
                    },
                    "tags": {
                        "type": "keyword",
                        "ignore_above": 1024
                    },
                    "type": {
                        "type": "keyword",
                        "ignore_above": 1024
                    }
                }
            }
        }
    }
}

If you followed along with the High Traffic JSON Data into Elasticsearch on Kubernetes guide, the mapping returned by the _mapping call should match the fields.yml section of the wx-rtbeat ConfigMap.

§Query Elasticsearch Data

Get the last ten minutes of weather data from our wx-rtbeat-* indices:

curl -X POST \
  http://localhost:9200/wx-rtbeat-%2A/_search \
  -H 'Content-Type: application/json' \
  -d '{ "query": {
           "range" : {
              "@timestamp" : {
                 "gt" : "now-5m"
               }
            }
         }
      }'

Output:

{
    "took": 19,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 3,
        "max_score": 1,
        "hits": [
            {
                "_index": "wx-rtbeat-2018.07",
                "_type": "doc",
                "_id": "pv8y2GQB4YMKAkYm7rjf",
                "_score": 1,
                "_source": {
                    "@timestamp": "2018-07-26T20:06:12.982Z",
                    "type": "wx-rtbeat-f744b8497-ch4ln",
                    "rxtxMsg": {
                        "producer": "cron",
                        "label": "/wx-data",
                        "key": "collector",
                        "payload": {
                            "longitude": -117.826581,
                            "offset": -7,
                            "timezone": "America/Los_Angeles",
                            "currently": {
                                "apparentTemperature": 87.99,
                                "nearestStormDistance": 8,
                                "windGust": 10.26,
                                "windSpeed": 6.29,
                                "uvIndex": 11,
                                "visibility": 10,
                                "cloudCover": 0.15,
                                "icon": "clear-day",
                                "ozone": 303.81,
                                "precipIntensity": 0,
                                "summary": "Clear",
                                "time": 1532635571,
                                "humidity": 0.56,
                                "temperature": 84.9,
                                "windBearing": 221,
                                "dewPoint": 67.59,
                                "nearestStormBearing": 98,
                                "precipProbability": 0,
                                "pressure": 1013.35
                            },
                            "latitude": 33.8148455
                        },
                        "seq": "2018726000000005612",
                        "time": "2018-07-26T20:06:11.456058885Z",
                        "uuid": "7bbb1b5b-a360-44bb-bd2e-9c0cce898623"
                    },
                    "clientIp": "192.168.247.255",
                    "beat": {
                        "name": "wx-rtbeat-f744b8497-ch4ln",
                        "hostname": "wx-rtbeat-f744b8497-ch4ln",
                        "version": "7.0.0-alpha1"
                    }
                }
            },
            {
                "_index": "wx-rtbeat-2018.07",
                "_type": "doc",
                "_id": "p_802GQB4YMKAkYmxLi4",
                "_score": 1,
                "_source": {
                    "@timestamp": "2018-07-26T20:08:13.263Z",
                    "clientIp": "192.168.247.255",
                    "beat": {
                        "name": "wx-rtbeat-f744b8497-ch4ln",
                        "hostname": "wx-rtbeat-f744b8497-ch4ln",
                        "version": "7.0.0-alpha1"
                    },
                    "type": "wx-rtbeat-f744b8497-ch4ln",
                    "rxtxMsg": {
                        "seq": "2018726000000005613",
                        "time": "2018-07-26T20:08:11.893558485Z",
                        "uuid": "4fe80f2b-54d2-4df6-8b60-e9152e4c2754",
                        "producer": "cron",
                        "label": "/wx-data",
                        "key": "collector",
                        "payload": {
                            "currently": {
                                "pressure": 1013.33,
                                "uvIndex": 11,
                                "windSpeed": 6.35,
                                "precipIntensity": 0,
                                "time": 1532635691,
                                "visibility": 10,
                                "windBearing": 221,
                                "humidity": 0.56,
                                "nearestStormBearing": 98,
                                "nearestStormDistance": 8,
                                "ozone": 303.8,
                                "windGust": 10.3,
                                "cloudCover": 0.15,
                                "precipProbability": 0,
                                "summary": "Clear",
                                "apparentTemperature": 88.08,
                                "dewPoint": 67.58,
                                "icon": "clear-day",
                                "temperature": 84.99
                            },
                            "latitude": 33.8148455,
                            "longitude": -117.826581,
                            "offset": -7,
                            "timezone": "America/Los_Angeles"
                        }
                    }
                }
            },
            {
                "_index": "wx-rtbeat-2018.07",
                "_type": "doc",
                "_id": "qP822GQB4YMKAkYmc7hg",
                "_score": 1,
                "_source": {
                    "@timestamp": "2018-07-26T20:10:03.510Z",
                    "beat": {
                        "name": "wx-rtbeat-f744b8497-ch4ln",
                        "hostname": "wx-rtbeat-f744b8497-ch4ln",
                        "version": "7.0.0-alpha1"
                    },
                    "type": "wx-rtbeat-f744b8497-ch4ln",
                    "rxtxMsg": {
                        "label": "/wx-data",
                        "key": "collector",
                        "payload": {
                            "currently": {
                                "nearestStormDistance": 8,
                                "pressure": 1013.31,
                                "summary": "Clear",
                                "windBearing": 221,
                                "windGust": 10.33,
                                "humidity": 0.56,
                                "precipIntensity": 0,
                                "time": 1532635802,
                                "visibility": 10,
                                "icon": "clear-day",
                                "dewPoint": 67.57,
                                "nearestStormBearing": 98,
                                "uvIndex": 11,
                                "windSpeed": 6.4,
                                "cloudCover": 0.15,
                                "ozone": 303.79,
                                "precipProbability": 0,
                                "temperature": 85.06,
                                "apparentTemperature": 88.16
                            },
                            "latitude": 33.8148455,
                            "longitude": -117.826581,
                            "offset": -7,
                            "timezone": "America/Los_Angeles"
                        },
                        "seq": "2018726000000005614",
                        "time": "2018-07-26T20:10:02.217602453Z",
                        "uuid": "9e10b07e-d618-4c71-940a-8634d35db682",
                        "producer": "cron"
                    },
                    "clientIp": "192.168.247.255"
                }
            }
        ]
    }
}

Explore the Elasticsearch official documentation: Search in Depth.

§Port Forwarding / Local Development

Check out kubefwd for a simple command line utility that bulk forwards services of one or more namespaces to your local workstation.

§Resources

← back to all notes