この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
Elastic Stack (Elasticsearch) Advent Calendar 2019の12月3日の記事です。
以前、AWS CDKでAmazon Elasticsearch Serviceのクラスタを作ってみました。
しかし、これだけだとクラスタ構築後にローカルPCからは接続できるんですが、AWS Lambda関数からは接続できません。 今回は、CDKでREST APIを構築して、API Gateway -> Lambda関数を通して、Elasticsearchへ接続できるようにします。
今回のソースコードはこちらで公開しています。 https://github.com/shoito/aws-cdk-es-with-api
バージョン情報
$ sw_vers
ProductName: Mac OS X
ProductVersion: 10.15.1
BuildVersion: 19B88
$ cdk --version
1.18.0 (build bc924bc)
$ node --version
v10.15.3
プロジェクトの設定
プロジェクトのファイルは以下のようになります。
.
├── cdk.json
├── bin/ - CDKのエントリポイント
├── lib/ - CDKのスタック定義(api, elasticsaerch)
├── src/ - APIの裏側のLambda関数の定義
├── package-lock.json
└── package.json
次にcdk.jsonですが、コンテキスト変数を用いて、Elasticsearch Serviceのノード数などのパラメータを定義しています。 後ほど紹介するlib/elasticsearch.tsのコード内から、こちらで定義されている値を利用します。 devやprodなど、ステージ毎に、Elasticsearch Serviceをカスタマイズしています。
cdk.json
{
"app": "npx ts-node bin/index.ts",
"context": {
"es": {
"version": "7.1"
},
"dev": {
"es": {
"domainName": "elasticsearch-dev-domain",
"instanceType": "t2.small.elasticsearch",
"instanceCount": 1,
"volumeSize": 10
}
},
"prod": {
"es": {
"domainName": "elasticsearch-prod-domain",
...省略
}
}
}
クラスタの作成
cdk.jsonで定義したコンテキストに従って、dev環境では、Elasticsearchクラスタはシングルノードで構築し、prod環境では、マルチノード構成で構築します。
Elasticsearchクラスタの構築スタックは lib/elasticsearch.ts
で定義しています。
定義の解説はこちらをご参照ください。
差異としては、ElasticsearchへLambda関数からアクセスできるようにするためのアクセスポリシーの設定追加(Condition.StringLikeを通じてAWSアカウントのIAMロールで制限)、Elasticsearchへクエリを投げるLambda用に、CloudFormationのOutputを使用し、Elasticsearchのエンドポイントを参照できるようにしている点です。
lib/elasticsearch.ts
accessPolicies: {
Version: "2012-10-17",
Statement: [
...
{
Effect: "Allow",
Principal: {
AWS: ["*"]
},
Action: ["es:*"],
Resource: `arn:aws:es:${cdk.Stack.of(this).region}:${
cdk.Stack.of(this).account
}:domain/${esContext.domainName}/*`,
Condition: {
StringLike: {
"aws:PrincipalArn": `arn:aws:iam::${
cdk.Stack.of(this).account
}:*`
}
}
}
]
...
new cdk.CfnOutput(this, "elasticsearchEndpoint", {
value: domain.attrDomainEndpoint,
exportName: "elasticsearch-endpoint"
});
...
API GatewayやLambda関数の定義をしている lib/api.ts
では、 cdk.Fn.importValue
を通じて、Lambda関数の環境変数にElasticsearchのエンドポイントを渡します。
lib/api.ts
...
const catLambda = new lambda.Function(this, "cat", {
runtime: lambda.Runtime.NODEJS_10_X,
handler: "handlers/index.handler",
layers: [layer],
code: lambda.Code.fromAsset("src/lambda", { exclude: ["**/*.ts"] }),
functionName: "cat",
environment: {
ELASTICSEARCH_ENDPOINT: cdk.Fn.importValue("elasticsearch-endpoint")
}
});
...
デプロイ
CDKスタックをビルド・デプロイします。 なお、私の環境ではデプロイに10分程度かかります。
$ npm i
$ npm run build
$ npm run deploy
デプロイ確認
AWSマネジメントコンソールからやElasticsearch、Kibanaのエンドポイントなどが作られていることは前回確認しました。 今回は、CDKで構築したREST APIにアクセスしてみます。 ElasticsearchのCat APIのhealthをProxyするAPIになってますので、Elasticsearchクラスタのヘルスステータスが確認できました。 (レスポンスの一部IDなどは書き換えています)
$ curl https://xxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/cat | jq
{
"body": [
{
"epoch": "1575365795",
"timestamp": "09:36:35",
"cluster": "123456:elasticsearch-dev-domain",
"status": "green",
"node.total": "1",
"node.data": "1",
"discovered_master": "true",
"shards": "1",
"pri": "1",
"relo": "0",
"init": "0",
"unassign": "0",
"pending_tasks": "0",
"max_task_wait_time": "-",
"active_shards_percent": "100.0%"
}
],
"statusCode": 200,
"headers": {
"date": "Tue, 03 Dec 2019 09:36:35 GMT",
"content-type": "application/json; charset=UTF-8",
"content-length": "312",
"connection": "keep-alive",
"access-control-allow-origin": "*"
},
"warnings": null,
"meta": {
"context": null,
"request": {
"params": {
"method": "GET",
"path": "/_cat/health",
"body": null,
"querystring": "format=json",
"headers": {
"User-Agent": "elasticsearch-js/7.1.0 (linux 4.14.138-99.102.amzn2.x86_64-x64; Node.js v10.17.0)"
},
"timeout": 30000
},
"options": {
"warnings": null
},
"id": 1
},
"name": "elasticsearch-js",
"connection": {
"url": "https://search-elasticsearch-dev-domain-xxxxxx.ap-northeast-1.es.amazonaws.com/",
"id": "https://search-elasticsearch-dev-domain-xxxxxx.ap-northeast-1.es.amazonaws.com/",
"headers": null,
"deadCount": 0,
"resurrectTimeout": 0,
"_openRequests": 0,
"status": "alive",
"roles": {
"master": true,
"data": true,
"ingest": true,
"ml": false
}
},
"attempts": 0,
"aborted": false
}
}
削除
確認が終わって不要になったので、cdk destroyコマンドでスタックを削除します。 ここでは、npm run destroyで間接的に上記コマンドを呼んでいます。 Amazon Elasticsearch Serviceドメインの削除は時間がかかるので、気長に待ちましょう。
$ npm run destroy
Lambda関数からElasticsearch Node.js clientを使うために
Amazon Elasticsearch ServiceにLambda関数(Node.js)からリクエストを送るためには、リクエストに署名をする必要があります。 この件は以下の公式ドキュメントに説明があります。
Amazon Elasticsearch Service への HTTP リクエストの署名 https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/es-request-signing.html#es-request-signing-node
ドメインアクセスポリシーに IAM ユーザーまたはロールが含まれる場合、Elasticsearch API へのリクエストに署名する必要があります。
そのため、 Elasticsearch Node.js client をそのまま使うのが適さない構成もありますが、今回は@acuris/aws-es-connectionライブラリを利用して、リクエストに署名をするようにしました。
src/lambda/es.ts
import {
createAWSConnection,
awsCredsifyAll,
awsGetCredentials
} from "@acuris/aws-es-connection";
import { Client } from "@elastic/elasticsearch";
export const createClient = async (node: string): Promise<Client> => {
const awsCredentials = await awsGetCredentials();
const AWSConnection = createAWSConnection(awsCredentials);
return awsCredsifyAll(
new Client({
node: `https://${node}`,
Connection: AWSConnection
})
);
};
上記コードで、Elasticsearch Clientをラップする関数を用意し、Lambda関数内で、以下のようにcat.health APIを利用しています。 Elasticsearch Clientが使えるおかげで、クエリを組み立てる手間が少なくすみます。
src/lambda/index.ts
...
export const handler = async () => {
const health = await (await createClient(ELASTICSEARCH_ENDPOINT)).cat.health({
format: "json"
});
...
さいごに
CDKで、Amazon Elasticsearch ServiceクラスタとそれにアクセスするREST APIを構築してみました。 Elasticsearch Node.js clientを使うためにひと手間必要だったり、Amazon Elasticsearch Service特有のことがありますし、誰かの参考になればと思います。