AWS Elasticsearch JavaScript Client

I have spent some time working with the AWS Elasticsearch Service lately. Regrettably, I found the threshold before being productive was higher than I anticipated. One of my obstacles was to get an AWS Elasticsearch JavaScript client working inside an AWS Lambda function, so I thought I’d better make a note of my solution in case I run into a similar problem in the future.

Elasticsearch IAM Policy Document

Before looking at the client implementation, we need to make sure that it is allowed to access the Elasticsearch domain. As always, this requires that the client is associated with an IAM Policy Document. Adhering to the AWS guideline of principle of least privileges the policy is as strict as possible.

    "Version": "2012-10-17",
    "Statement": [{
        "Effect": "Allow",
        "Action": "es:ESHttp*", 
        "Resource": "arn:aws:es:eu-west-1:111122223333:my-domain/*"

The * character at the end of the es:ESHttp* value implies that all HTTP methods are allowed. You may choose to lock down the policy even further. One example is to use "es:ESHttpGet" for just permitting reading data from Elasticsearch. Another example is ["es:ESHttpPost", "es:ESHttpPut"] for clients that only add data to the domain. Finally, the Resource property tells us that the policy statement only affects the Elasticsearch domain with the specified ARN.

Elasticsearch Client

My first naive attempt was to use a HTTP client to make requests to the Elasticsearch HTTP API of my domain. It failed misearably, AWS requires that HTTP requests are signed with Signature Version 4 to be valid. The AWS SDK handles this internally so usually you do not need to bother. Realizing that, I took a closer look at what functionality the ES class in the AWS JavaScript SDK offers. It does indeed provide an Elasticsearch API, but it is all about domain configuration, management and it does not provide any client features. Next, when I studied the AWS Elasticsearh developer guide, I found an JavaScript client snippet. It had some limitations in my opinion (it uses global variables for request configuration and response handling just logs HTTP status code and response body). For this reason, I chose to rewrite it to a more generic elasticsearch-client.js file:

'use strict';

const path = require('path');
const AWS = require('aws-sdk');

const { AWS_REGION, ELASTICSEARCH_DOMAIN } = process.env;
const endpoint = new AWS.Endpoint(ELASTICSEARCH_DOMAIN);
const httpClient = new AWS.HttpClient();
const credentials = new AWS.EnvironmentCredentials('AWS');

 * Sends a request to Elasticsearch
 * @param {string} httpMethod - The HTTP method, e.g. 'GET', 'PUT', 'DELETE', etc
 * @param {string} requestPath - The HTTP path (relative to the Elasticsearch domain), e.g. '.kibana'
 * @param {Object} [payload] - An optional JavaScript object that will be serialized to the HTTP request body
 * @returns {Promise} Promise - object with the result of the HTTP response
function sendRequest({ httpMethod, requestPath, payload }) {
    const request = new AWS.HttpRequest(endpoint, AWS_REGION);

    request.method = httpMethod;
    request.path = path.join(request.path, requestPath);
    request.body = JSON.stringify(payload);
    request.headers['Content-Type'] = 'application/json';
    request.headers['Host'] = ELASTICSEARCH_DOMAIN;

    const signer = new AWS.Signers.V4(request, 'es');
    signer.addAuthorization(credentials, new Date());

    return new Promise((resolve, reject) => {
        httpClient.handleRequest(request, null,
            response => {
                const { statusCode, statusMessage, headers } = response;
                let body = '';
                response.on('data', chunk => {
                    body += chunk;
                response.on('end', () => {
                    const data = {
                    if (body) {
                        data.body = JSON.parse(body);
            err => {

module.exports = sendRequest;

Example Usage

The above implementation enables you to implement all methods in the Elasticsearch HTTP API. The only missing part is an environment variable called ELASTICSEARCH_DOMAIN that should have the value of your AWS hosted Elasticsearch domain such as To create a new Elasticsearch index called my-index you execute the function call by providing the required parameters in the corresponding Create Index API:

'use strict';

const sendElasticsearchRequest = require('./elasticsearch-client');

const params = {
    httpMethod: 'PUT',
    requestPath: 'my-index',
    payload: {
        // see link above for details
        settings: {
            index: {
                number_of_shards: 3,
                number_of_replicas: 2
    .then(response => {;

And the result may look something like:

{ statusCode: 200,
  statusMessage: 'OK',
   { date: 'Wed, 05 Sep 2018 20:24:24 GMT',
     'content-type': 'application/json; charset=UTF-8',
     'content-length': '67',
     connection: 'keep-alive',
     'access-control-allow-origin': '*' },
   { acknowledged: true,
     shards_acknowledged: true,
     index: 'my-index' } }


  • The Elasticsearch client above returns a Promise. Timeouts and unknown domain URLs result in Promise.reject() whereas successful HTTP request/response results in Promise.resolve(). The resolved JavaScript object has three or four properties, namely the HTTP statusCode, the HTTP statusMessage, the HTTP headers and body in case there is a HTTP response body. Consequently, the promise will be resolved successfully by any 4XX client error codes (e.g. 404 – Not Found) and 5XX server errors (e.g. 503 Service Unavailable). Feel free to modify the code to reject the promise on HTTP errors if you prefer such behaviour.
  • The client uses the AWS.EnvironmentCredentials class for obtaining valid credentials since it is being deployed as part of a Lambda function. This is not the only Node.js runtime environment and for this reason this is not the only credential class in the SDK. Please study the Setting Credentials in Node.js chapter in the AWS JavaScript developer guide for other alternatives.
  • A different approach to connect to an AWS Elasticsearch domain is to use the official Elasticsearch JavaScript client. Like my HTTP client attempt, it cannot be used directly since it does not have the AWS Signature Version 4 capability. However, it has a pluggable architecture and there is a community extension called http-aws-es that solves this problem. I have not tried this method, but they are both available as npm dependencies. Please check elasticsearch and http-aws-es for more information.

Mattias Severson

Mattias is a senior software engineer specialized in backend architecture and development with experience of cloud based applications and scalable solutions. He is a clean code proponent who appreciates Agile methodologies and pragmatic Test Driven Development. Mattias has experience from many different environments, including everything between big international projects that last for years and solo, single day jobs. He is open-minded and curious about new technologies. Mattias believes in continuous improvement on a personal level as well as in the projects that he is working on. Additionally, Mattias is a frequent speaker at user groups, companies and conferences.

This Post Has 13 Comments

  1. Abdoo

    Thanks a lot, you saved me a lot of time. Nice and well explained article!

  2. Alejandro

    I really liked your implementation, it also pointed me to some valuable resources, thanks!
    Right now I’m debating whether I should query my ES domain from a Lambda function with AWS API Gateway or have the client (front-end/browser) app send directly the network request. Do you have any thoughts in the matter?

    PS: For this use case, and as you tightly control your function implantation is fine to use path.join to join urls, nonetheless it is not recommended as pointed out here

    1. Mattias Severson

      @Alejandro: Thanks for the link and for your comments.

      Regarding your question I have only been involved in projects where Elasticsearch has been used as part of a server side application and not directly accessible by clients (c.f. a database or a Redis cache). That said, it is not uncommon to see ES directly behind NGINX which in turn receives client requests and I can imagine a similar solution where API Gateway and Lamba being used as reverse proxy instead of NGINX. Chances are that you already have some authentication / authorization flow in place that you can leverage if you configure a reverse proxy. Moreover, a reverse proxy typically has some kind of rate limiting or throttling that can be enabled to protect your service in case of DDoS attacks.

      With a plain ES service you need to carefully tailor the ES access management, at least the parts that modifies the stored data and indices configuration. Presumably, you would like to expose the Search API to your users, but probably neither the Indices API nor the Documents API, to name a few. Additionally, Kibana is prebuilt with the AWS Elasticsearch service, i.e. it will also be available for clients (and the rest of the Internet) by default unless you take some action. That said, you can configure IAM policies on ES resource level, see the Policy Element Reference documentation in the Amazon Elasticsearch Service Developer Guide.

  3. Lucas

    Hi, I’m having trouble making http requests with signature for authentication with an IAM user
    The function responds with the following error when making a request
    Error: Error [ERR_TLS_CERT_ALTNAME_INVALID]: Hostname / IP does not match certificate’s altnames: Host: https. is not in the cert’s altnames: DNS: *.

    Also try to do it with the following article but the result is the same:

    Someone had the same problem?
    I’m using node v10.15.3

    1. Bjorn

      Same issue as Lucas… would love to know how to solve this. @Matthias, do you have his e-mail address?

      1. Hermes

        Hey guys, I had the same problem.

        I realized that my endpoint was wrong. The correct format is without https://

  4. Krishna Kumar

    What a simple, clear, wonderfully written article.
    If only the rest of the AWS documentation was written this way…

  5. Ian

    Great example, could you make it clear what the license is on this code? Thanks!

  6. Ankit

    It would be nice to also give a test case example to test this code.

  7. Mayank Taparia

    Well Explained. Just wanted to add a point in this.
    As per my understanding, DELETE won’t execute as request.headers[‘Content-Length’] = Buffer.byteLength(request.body) is missing. We should add Content-Length header in case DELETE request.

  8. Ryan O'Connor

    const { Client } = require(‘@elastic/elasticsearch’);
    const { AmazonConnection } = require(‘aws-elasticsearch-connector’);

    const esClient = new Client({
    node: process.env.ELASTICSEARCH_DOMAIN_URL,
    Connection: AmazonConnection,

  9. Leonardo Lima

    Thanks for the article. I’m facing this problem when executing the request: “The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details”.Any ideas?

Leave a Reply