AWS CDKでAmazon Elasticsearch Serviceのドメイン(クラスタ)を作ってみた

AWS CDKで、Amazon Elasticsearch Serviceのドメイン(クラスタ)を作ってみました。
2019.10.28

はじめに

AWS CDKで、Amazon Elasticsearch Serviceのドメイン(クラスタ)を作ってみました。
シンプルにシングルノードでElasticsearchクラスタを作っていますが、設定値を少し変えれば、複数台の専用マスターノードとデータノードからなるクラスタを構築できます。

今回のソースコードはこちらで公開しています。
https://github.com/shoito/aws-cdk-elasticsearch

バージョン情報

$ sw_vers
ProductName:    Mac OS X
ProductVersion: 10.14.6
BuildVersion:   18G103

$ cdk --version
1.14.0 (build 261a1bf)

$ node --version
v10.15.3

プロジェクトの設定

プロジェクトのファイルは以下のようになります。

$ tree -L 1
.
├── cdk.json
├── index.ts
├── package-lock.json
├── package.json
└── tsconfig.json

package.jsonのハイライト部分ですが、synth, deploy, destroyを簡単に実行できるようにしています。 -c sourceIp=`curl -s https://checkip.amazonaws.com` の部分は、手元のPCからElasticsearchやKibanaのエンドポイントにアクセスするために、CDKのContextを利用して、ローカルPCのIPアドレスをCDKスタックに渡すためのものです。

package.json

{
  "name": "aws-cdk-elasticsearch",
  "version": "0.1.0",
  "license": "MIT",
  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "cdk": "cdk",
    "synth": "cdk synth --no-staging -c stage=dev -c sourceIp=`curl -s https://checkip.amazonaws.com` > template.yaml",
    "deploy": "cdk deploy -c stage=dev -c sourceIp=`curl -s https://checkip.amazonaws.com`",
    "destroy": "cdk destroy -c stage=dev"
  },
  ...省略
}

次にcdk.jsonですが、コンテキスト変数を用いて、Elasticsearch Serviceのノード数などのパラメータを定義しています。
後ほど紹介するindex.tsのコード内から、こちらで定義されている値を利用します。
devやprodなど、ステージ毎に、Elasticsearch Serviceをカスタマイズしています。

cdk.json

{
  "app": "npx ts-node index.ts",
  "context": {
    "es": {
      "version": "7.1"
    },
    "dev": {
      "es": {
        "domainName": "test-dev-domain",
        "instanceType": "t2.small.elasticsearch",
        "instanceCount": 1,
        "volumeSize": 10
      }
    },
    "prod": {
      "es": {
        "domainName": "test-prod-domain",
        ...省略
    }
  }
}

クラスタの作成

今回は検証のため、Elasticsearchクラスタはシングルノードで構築します。
なお、マルチノード構成に必要な設定はコードコメントで残してますので、何か参考になればと。

Elasticsearch Serviceは CfnDomainクラス を用いて作成します。

Amazon Elasticsearch Service Construct Library
https://docs.aws.amazon.com/cdk/api/latest/typescript/api/aws-elasticsearch.html

以下では、ESDomainStackクラスと上記cdk.jsonからのコンテキスト変数esの型として、ESContextインターフェースを定義しています。

index.ts

import cdk = require('@aws-cdk/core');
import { CfnDomain } from '@aws-cdk/aws-elasticsearch';

// ステージ毎に、ノード数やデータの暗号化有無などを可変にできるように、cdk.jsonのContextを用いる。
// cdk.jsonのcontextで定義しているes contextの型定義。
interface ESContext {
  // Elasticsearchのバージョン
  readonly version: string;
  // Elasticsearch Serviceのドメイン名(クラスタ名)
  readonly domainName: string;
  // 専用マスターノードのインスタンスタイプ
  readonly masterInstanceType: string;
  // データノードのインスタンスタイプ
  readonly instanceType: string;
  // データノードのノード数
  readonly instanceCount: number;
  // ボリュームサイズ
  readonly volumeSize: number;
  // アベイラビリティゾーン数
  readonly availabilityZoneCount: 1 | 2 | 3;
  // AZ分散有無
  readonly zoneAwareness: boolean;
  // 専用マスタノードの有無
  readonly dedicatedMaster: boolean;
  // データ暗号化の有無
  readonly encryption: boolean;
}

class ESDomainStack extends cdk.Stack {
  // LambdaなどからElasticsearch Serviceエンドポイントの参照用
  public endpoint: string;

  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // cdkコマンドで、-c stage=devのように、devやprodを指定する
    const stage: string = this.node.tryGetContext('stage');
    // Elasticsearchのバージョン
    const esVersion: string = this.node.tryGetContext('es').version;
    // cdk.jsonからesコンテキストのオブジェクトを取得する
    const esContext: ESContext = this.node.tryGetContext(stage).es;
    // アクセスポリシー設定のため、cdk deployコマンド実行時にパラメーターで自身のIPを渡す -c sourceIp=`curl -s https://checkip.amazonaws.com`
    const sourceIp: string = this.node.tryGetContext('sourceIp');

    const domain = new CfnDomain(this, esContext.domainName || 'domain', {
      // アクセスポリシー設定
      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: {
              IpAddress: {
                // 自身のIPからElasticsearchやKibanaのエンドポイントにアクセスできるようにする
                'aws:SourceIp': `${sourceIp || '127.0.0.1'}`
              }
            }
          },
          {
            Effect: 'Allow',
            Principal: {
              AWS: [
                // 自身のAWSアカウント環境からのElasticsearchへの操作を許可する
                cdk.Stack.of(this).account
              ]
            },
            Action: [
              'es:*'
            ],
            Resource: `arn:aws:es:${cdk.Stack.of(this).region}:${cdk.Stack.of(this).account}:domain/${esContext.domainName}/*`
          }
        ]
      },
      domainName: esContext.domainName,
      ebsOptions: {
        ebsEnabled: true,
        volumeSize: esContext.volumeSize,
        volumeType: 'gp2',
      },
      elasticsearchClusterConfig: {
        instanceCount: esContext.instanceCount,
        // T2 インスタンスタイプは、保管時のデータの暗号化をサポートしていない
        // https://docs.aws.amazon.com/ja_jp/elasticsearch-service/latest/developerguide/aes-supported-instance-types.html
        instanceType: esContext.instanceType,
        // 専用マスターノードを使う構成にしたい場合は以下のオプションを設定する
        // dedicatedMasterEnabled: true,
        // dedicatedMasterCount: 3,
        // dedicatedMasterType: esContext.masterInstanceType,
        // zoneAwarenessEnabled: true,
        // zoneAwarenessConfig: {
        //   availabilityZoneCount: esContext.availabilityZoneCount
        // }
      },
      elasticsearchVersion: esVersion,
      encryptionAtRestOptions: {
        enabled: esContext.encryption
        // kmsKeyId: ''
      },
      nodeToNodeEncryptionOptions: {
        enabled: false
      },
      snapshotOptions: {
        automatedSnapshotStartHour: 0
      },
      // tags: [],
      // vpcOptions: {
      //   subnetIds: [],
      //   securityGroupIds: []
      // },
    });

    this.endpoint = domain.attrDomainEndpoint;
  }
}

const app = new cdk.App();
new ESDomainStack(app, "ESDomainStack");

デプロイ

CDKスタックをビルド・デプロイします。
なお、私の環境ではデプロイに11分程度かかりました。

$ npm i
$ npm run build

> aws-cdk-elasticsearch@0.1.0 build /Users/shoito/workspaces/aws-cdk-elasticsearch
> tsc

# cdk bootstrapを過去に実行していれば不要
$ cdk bootstrap -c stage=dev
 ⏳  Bootstrapping environment aws://123456789/ap-northeast-1...
CDKToolkit: creating CloudFormation changeset...
 0/1 | 17:16:36 | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack | CDKToolkit 
 1/1 | 17:16:36 | UPDATE_COMPLETE      | AWS::CloudFormation::Stack | CDKToolkit 
 ✅  Environment aws://123456789/ap-northeast-1 bootstrapped.

$ npm run deploy

> aws-cdk-elasticsearch@0.1.0 deploy /Users/shoito/workspaces/aws-cdk-elasticsearch
> cdk deploy -c stage=dev -c sourceIp=`curl -s https://checkip.amazonaws.com`

ESDomainStack: deploying...
ESDomainStack: creating CloudFormation changeset...
 0/3 | 17:25:17 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata 
 0/3 | 17:25:17 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | test-dev-domain (testdevdo
main) 
 0/3 | 17:25:19 | CREATE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata Resource creat
ion Initiated
 1/3 | 17:25:19 | CREATE_COMPLETE      | AWS::CDK::Metadata         | CDKMetadata 
 1/3 | 17:25:19 | CREATE_IN_PROGRESS   | AWS::Elasticsearch::Domain | test-dev-domain (testdevdomain) Resource creation Initiated
1/3 Currently in progress: testdevdomain
 2/3 | 17:36:23 | CREATE_COMPLETE      | AWS::Elasticsearch::Domain | test-dev-domain (testdevdomain) 
 3/3 | 17:36:25 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | ESDomainStack 

 ✅  ESDomainStack

Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789:stack/ESDomainStack/f479bb70-f700-11e9-b8c5-0e36924addac

デプロイ確認

AWSマネジメントコンソールからAmazon Elasticsearch Serviceにアクセスします。
するとこのようにドメインやElasticsearch、Kibanaのエンドポイントなどが作られていることが確認できます。

Elasticsearch Serviceダッシュボード

Kibanaのエンドポイントにアクセスし、サンプルデータを読み込ませたUI

Elasticsearch Serviceドメイン設定確認画面

削除

不要になった場合は、cdk destroyコマンドでスタックを削除します。
ここでは、npm run destroyで間接的に上記コマンドを呼んでいます。

$ npm run destroy

> aws-cdk-elasticsearch@0.1.0 destroy /Users/shoito/workspaces/aws-cdk-elasticsearch
> cdk destroy -c stage=dev

Are you sure you want to delete: ESDomainStack (y/n)? y
ESDomainStack: destroying...
   0 | 18:27:58 | DELETE_IN_PROGRESS   | AWS::CloudFormation::Stack | ESDomainStack User Initiated
   0 | 18:27:59 | DELETE_IN_PROGRESS   | AWS::Elasticsearch::Domain | test-dev-domain (testdevdomain) 
   0 | 18:27:59 | DELETE_IN_PROGRESS   | AWS::CDK::Metadata         | CDKMetadata 
   1 | 18:28:01 | DELETE_COMPLETE      | AWS::CDK::Metadata         | CDKMetadata 
  1 Currently in progress: ESDomainStack, testdevdomain

 ✅  ESDomainStack: destroyed

さいごに

CDKで、Amazon Elasticsearch Serviceのクラスタを構築してみました。
クラスタを構築するだけならそこまで手間取らなかったのですが、手元のPCから接続できるようにアクセスポリシーを設定する部分で悩みました。
今回のようにCDKでElasticsearchクラスタを構築する情報が少なかったので、誰かの参考になればと思います。