Skip to content

Instantly share code, notes, and snippets.

@parthdesai93
Last active April 8, 2021 06:18
Show Gist options
  • Save parthdesai93/1bd3a25ad4cf788d49ce4a00a1bb3268 to your computer and use it in GitHub Desktop.
Save parthdesai93/1bd3a25ad4cf788d49ce4a00a1bb3268 to your computer and use it in GitHub Desktop.
http-aws-es compatible with new Elasticsearch client.
/* requires AWS creds to be updated.
* if they aren't, update using AWS.config.update() method before instatiing the client.
*
* import this module where you instantiate the client, and simply pass this module as the connection class.
*
* eg:
* const client = new Client({
* node,
* Connection: AwsConnector
* });
*/
import AWS from 'aws-sdk';
import { Connection } from '@elastic/elasticsearch';
class AwsConnector extends Connection {
async request(params, callback) {
try {
const creds = await this.getAWSCredentials();
const req = this.createRequest(params);
const { request: signedRequest } = this.signRequest(req, creds);
super.request(signedRequest, callback);
} catch (error) {
throw error;
}
}
createRequest(params) {
const endpoint = new AWS.Endpoint(this.url.href);
let req = new AWS.HttpRequest(endpoint);
Object.assign(req, params);
req.region = AWS.config.region;
if (!req.headers) {
req.headers = {};
}
let body = params.body;
if (body) {
let contentLength = Buffer.isBuffer(body)
? body.length
: Buffer.byteLength(body);
req.headers['Content-Length'] = contentLength;
req.body = body;
}
req.headers['Host'] = endpoint.host;
return req;
}
getAWSCredentials() {
return new Promise((resolve, reject) => {
AWS.config.getCredentials((err, creds) => {
if (err) {
if (err && err.message) {
err.message = `AWS Credentials error: ${e.message}`;
}
reject(err);
}
resolve(creds);
});
});
}
signRequest(request, creds) {
const signer = new AWS.Signers.V4(request, 'es');
signer.addAuthorization(creds, new Date());
return signer;
}
}
export { AwsConnector };
import { AwsConnector } from './aws_es_connector';
import { Client } from '@elastic/elasticsearch';
import AWS from 'aws-sdk';
// load aws keys and region, ideally using env variables.
let accessKey = '****';
let secretKey = '****';
let region = '**'; //eg: us-east-1
let node = '***'; //node url
AWS.config.update({
credentials: new AWS.Credentials(accessKey, secretKey),
region
});
const client = new Client({
node,
Connection: AwsConnector
});
//use this client to talk to AWS managed ES service.
export default client;
@villasv
Copy link

villasv commented Aug 21, 2019

This is my TypeScript version:

import { Connection as UnsignedConnection } from '@elastic/elasticsearch';
import * as AWS from 'aws-sdk';
import RequestSigner from 'aws-sdk/lib/signers/v4';
import { ClientRequest, RequestOptions, IncomingMessage } from 'http';

class AwsElasticsearchError extends Error {}


class AwsSignedConnection extends UnsignedConnection {
  public request(
    params: RequestOptions,
    callback: (err: Error | null, response: IncomingMessage | null) => void,
  ): ClientRequest {
    const signedParams = this.signParams(params);
    return super.request(signedParams, callback);
  }

  // TODO: better type after https://github.com/elastic/elasticsearch-js/issues/951
  private signParams(params: any): RequestOptions {
    const region = AWS.config.region || process.env.AWS_DEFAULT_REGION;
    if (!region) {
      throw new AwsElasticsearchError('missing region configuration');
    }

    const endpoint = new AWS.Endpoint(this.url.href);
    const request = new AWS.HttpRequest(endpoint, region);

    request.method = params.method;
    request.path = params.querystring
      ? `${params.path}/?${params.querystring}`
      : params.path;
    request.body = params.body;

    request.headers = params.headers;
    request.headers.Host = endpoint.host;

    const signer = new RequestSigner(request, 'es');
    signer.addAuthorization(AWS.config.credentials, new Date());
    return request;
  }
}

export { AwsSignedConnection, UnsignedConnection, AwsElasticsearchError };

@fewstera
Copy link

We've released an NPM package that works with @elastic/eleasticsearch and AWS elasticsearch clusters. It signs the requests for AWS and refreshes the credentials when they're about to expire.

https://www.npmjs.com/package/@acuris/aws-es-connection
https://github.com/mergermarket/acuris-aws-es-connection

@mohitkhanna
Copy link

@fewstera Thank you for posting this. Will test it out.

@mohitkhanna
Copy link

@fewstera A question. You have a list of whitelisted function names that you wrap in awsCredsifyAll. How would you keep them always in sync with any new ES release?

@fewstera
Copy link

fewstera commented Sep 11, 2019

@mohitkhanna those whitelisted keys come from the first level of this object: https://github.com/elastic/elasticsearch-js/blob/master/api/index.js#L18. So they could easily be updated using a script, but there is no automatic setup in place to currently do this.

I don't see them changing often, but I will check them occasionally to ensure they don't go out of sync. If a whitelisted key is missed, the client will continue to work, just the AWS credentials will not be refreshed by that method call.

@mohitkhanna
Copy link

mohitkhanna commented Sep 11, 2019 via email

@sabareeshkk
Copy link

Figured it out.
AWSHTTPRequest uses path to send query parameters and there is no key as "querystring" here in docs.
So a small change in aws_es_connector.js solves the problem.
In createRequest() before sending the req object
req.path+='/?'+req.querystring; delete req.querystring;
Now all parameters will work fine.

cool .thanks saved my day

@sboyd
Copy link

sboyd commented Mar 22, 2020

Thank you @parthdesai93! Was very helpful

@niezgoda
Copy link

Hey. I find I spot an issue.
Look at this:
`

const x = 'ߤߤߤߤ';
x.length
4
w = Buffer.from(x)

> w.byteLength 8 `

Content-Length is provided in bytes. So the header is going to have invalid value - 4 bytes instead of 8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment