TypeScriptで書いたスクリプトでAmazon Elasticsearch Serviceに接続する
こんにちは、CX事業本部のうらわです。
TypeScriptでhttps://github.com/aws/aws-sdk-jsを使用してAmazon Elasticsearch Service(以下、ES)に接続しようとしてつまずきました。本記事はつまずいた内容とその解決策を記載します。
※ どうしてもTypeScriptで書きたいので、そもそもTypeScriptを使わない、という方法は除外します。
環境
$ sw_vers ProductName: Mac OS X ProductVersion: 10.15.7 BuildVersion: 19H15 $ node -v v14.15.4 $ npm -v 6.14.10 $ yarn -v 1.22.10
ESドメインの作成
テスト用のESドメインを作成します。以下のように設定して作成しました。表に記載のない項目はデフォルトのままとしています。
項目 | 内容 |
---|---|
デプロイタイプ | 開発およびテスト |
Elasticsearchのバージョン | 7.10 |
Elasticsearchドメイン名 | aws-sdk-js-test-domain |
自動調整 | 有効化 |
インスタンスタイプ | t2.small.elasticsearch |
ネットワーク構成 | パブリックアクセス |
ドメインアクセスポリシー | AssumeRoleで引き受けるIAMロール(対象アカウントでESへのFullアクセス権限がある)のArnのアクセスを許可する |
aws-sdk-js(v2)を使うがうまくいかず
以下のAWS公式ドキュメントにあるようなコードでESへの接続を試みました。
しかし、AWS.Signers
の型定義が存在しないため、TypeScriptではドキュメントと同様のコードでは実現できませんでした。これはissueもあがっています。
aws-sdk-js-v3を使う
上記のissueのコメントにもありますが、aws-sdk-js-v3
ではこれらの問題が解決されているとのことです。
さっそくaws-sdk-js-v3
を使ってみたのですが、aws-sdk-js(v2)
とは大きくSDKの構造が変わっていて、どうやってESに接続するためのコードを書けば良いのか全くわかりませんでした。
aws-sdk-js-v3
のissueを探してみたところ、接続するためのコード例を記載してくれているissueがありました。このissueのコード例を利用して接続してみます。
How to send signed AWS Elasticsearch request? · Issue #2099 · aws/aws-sdk-js-v3
まずは各種ライブラリを準備します。
# 準備 $ mkdir es-connect-test && cd es-connect-test $ npm init -y # TypeScript $ yarn add -D typescript ts-node @types/node # aws-sdk-js-v3関連 $ yarn add -D @aws-sdk/{signature-v4,protocol-http,credential-provider-node,node-http-handler}
続いて接続テストのためのTypeScriptのコードを用意します。上記issueのコード例を参考にしつつ、/_cat/health
でESのヘルスチェックを行うコードに変更しました。
import { SignatureV4 } from "@aws-sdk/signature-v4"; import { HttpRequest } from "@aws-sdk/protocol-http"; import { Sha256 } from "@aws-crypto/sha256-js"; import { defaultProvider } from "@aws-sdk/credential-provider-node"; import { NodeHttpHandler } from "@aws-sdk/node-http-handler"; import { IncomingMessage } from "http"; interface CreateSignHttpRequestParams { body?: string; headers?: Record<string, string>; hostname: string; method?: string; path?: string; port?: number; protocol?: string; query?: Record<string, string>; service: string; } export async function createSignedHttpRequest({ headers, hostname, method = "GET", path = "/", port = 443, protocol = "https:", query, service, }: CreateSignHttpRequestParams): Promise<HttpRequest> { const httpRequest = new HttpRequest({ headers, hostname, method, path, port, protocol, query, }); const sigV4Init = { credentials: defaultProvider(), region: "ap-northeast-1", service, sha256: Sha256, }; const signer = new SignatureV4(sigV4Init); return signer.sign(httpRequest) as Promise<HttpRequest>; } const nodeHttpHandler = new NodeHttpHandler(); (async () => { const hostname = "<your domain name>.ap-northeast-1.es.amazonaws.com"; const signedHttpRequest = await createSignedHttpRequest({ method: "GET", headers: { "Content-Type": "application/json", host: hostname, }, hostname, path: "_cat/health", service: "es", }); console.log(signedHttpRequest); try { const res = await nodeHttpHandler.handle(signedHttpRequest); const body = await new Promise((resolve, reject) => { const incomingMessage = res.response.body as IncomingMessage; let body = ""; incomingMessage.on("data", (chunk) => { body += chunk; }); incomingMessage.on("end", () => { resolve(body); }); incomingMessage.on("error", (err) => { reject(err); }); }); console.log(body); } catch (err) { console.error("Error:"); console.error(err); } })();
対象アカウントでESへのFullAccessの権限があるIAMロールにAssumeRoleしてコードを実行してみます。ちなみに私はremind101/assume-roleというCLIツールを使用して一時的セキュリティ認証情報を環境変数にセットしています。
$ assume-role role名 $ npx ts-node src/index.ts // 省略 1619766241 07:04:01 accountId:aws-sdk-js-test-domain green 1 1 true 1 1 0 0 0 0 - 100.0%
_cat/health
の結果を確認することができました。
別のライブラリを使う
上記コード例のissueのこのコメントには、aws-sdk-js-v3
ではなく@elastic/elasticsearch
と@acuris/aws-es-connection
という別のライブラリを使用して接続するコード例が記載されています。こちらも試してみます。
追加でライブラリをインストールします。ちなみにこちらの方法ではaws-sdk-js(v2)
に依存しています。
$ yarn add -D aws-sdk @elastic/elasticsearch @types/elasticsearch @acuris/aws-es-connection
@acuris/aws-es-connectionのREADMEを参考にしてESへの接続を試すコードを用意します。
import { Client } from "@elastic/elasticsearch"; import { createAWSConnection, awsGetCredentials, } from "@acuris/aws-es-connection"; const hostname = "<your domain name>.ap-northeast-1.es.amazonaws.com"; (async () => { const awsCredentials = await awsGetCredentials(); const AWSConnection = createAWSConnection(awsCredentials); const client = new Client({ ...AWSConnection, node: `https://${hostname}`, }); const res = await client.cat.health(); console.log(res.body); })();
aws-sdk-js-v3
の時と同様にAssumeRoleして実行してみます。
$ npx ts-node src/main.ts 1619766263 07:03:39 accountId:aws-sdk-js-test-domain green 1 1 true 1 1 0 0 0 0 - 100.0%
こちらもうまくいきました。
おわりに
最初はTypeScriptでもRubyやPythonと同様に@elastic/elasticsearch
とSignature V4を作成するライブラリを組み合わせれば簡単に接続できるのかな思っていたのですが、そうではなかったので苦労しました。
ESへの接続において、TypeScriptの場合はaws-sdk-js(v2)
だと厳しいのでaws-sdk-js-v3
か別のライブラリを使うのが良さそうです。
参考記事
Elasticsearch クラスターにアクセスしようとすると、「User: anonymous is not authorized」というエラーが表示されます