目次

Introduction

全文検索機能を作りたくて筆者は以前CloudRunの中にElasticSearchを入れて運用しようとしましたがなんたって辛い…
頑張ればなんとかいけるのですが、KibanaやElasticsearchへのimportデーモンなど疎通が難しく。また料金を抑えようとしてCloudRunのコールドスタートを使用しようとしたのですが、なんたって相性が悪すぎる…使い方がそもそも無理あるのでデファクトスタンダードであるkubernetes上でElasticsearchを構築したのでその備忘録になります。

今回作成するもの

  • GKE上にElasticsearch環境の構築
  • mongoAtlasからデータを同期する
  • helmなどは使用しない

完成系

環境

$ gcloud -v
Google Cloud SDK 420.0.0
gke-gcloud-auth-plugin 0.4.0

$ kubectl version --short
Client Version: v1.26.1
Kustomize Version: v4.5.7

GKE: 1.24.9-gke.3200
ECK: v2.6.1
Elasticsearch: v8.6.2
Kibana: v8.6.2
monstache 6.7.11

前提

  • kubectlの基本的なコマンドを使用できる
  • Elasticsearchの概要を知っている
  • gcpプロジェクトを作成している
  • gcloudコマンドをinstallしている
  • gke-gcloud-auth-pluginをinstallしている
  • MongoDBAtlasに登録している

handsOn

今回は、「わかりやすく解説するため」&「解説する主題ではない部分」とのことから機密情報部分(Secret)を直書きします。読者の皆様は機密情報には気をつけてください。

GKEセッティング

まずは、gcpにログインしプロジェクトセットします

$ gcloud auth login

$ gcloud config set project {PROJECT_ID}

次にElasticsearch用のGKEクラスターを作成していきます。
料金節約のためspotで作成します。ここからGCEの料金がかかるので注意です

$ gcloud container clusters create es-cluster \
  --zone=asia-northeast1-a \
  --spot \
  --num-nodes=2 \
  --machine-type=e2-medium

数分待つとクラスターができます。

作成されたら以下のコマンドでGKEを操作できるか確認してください

$ kubectl config current-context
gke_{project-id}_asia-northeast1-a_es-cluster

ECK(Elasticsearch)環境構築

次に以下のチュートリアルに沿ってECK環境をinstallしていきます https://www.elastic.co/guide/en/cloud-on-k8s/current/k8s-quickstart.html

まずは、ECKのCustom Resource Definitions(CRD)を作成

$ kubectl create -f https://download.elastic.co/downloads/eck/2.6.1/crds.yaml

次にeck-operatorをinstallします

$ kubectl apply -f https://download.elastic.co/downloads/eck/2.6.1/operator.yaml

必要なリソースのinstallが終わったらElasticsearchを作成します

k8s
 L es.yaml(new)
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: es-sample
spec:
  version: 8.6.2
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
$ kubectl apply -f k8s/es.yaml

少し時間が経ったら作成されていることが確認できます

$ kubectl get pods
NAME                     READY   STATUS    RESTARTS   AGE
es-sample-es-default-0   1/1     Running   0          2m19s

外部から接続できるようにロードバランサーを追加します
今回はhttps化しないでいきます

k8s
 L es.yaml(modify)
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: es-sample
spec:
  version: 8.6.2
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
  http:
    service:
      spec:
        type: LoadBalancer
    tls:
      selfSignedCertificate:
        disabled: true

Elasticsearchのpodが立ったらrootパスワードを取得します

$ kubectl get secret
es-sample-es-elastic-user                 Opaque   1      12m
(他secret)

$ kubectl get secret es-sample-es-elastic-user -o go-template='{{.data.elastic | base64decode}}'
5Esma90y3459ppeh7ubsnU78 # 今回handsOn後Clusterを削除するのでパスワードを記載します

ElasticSearchに疎通してみましょう

$ kubectl get services
NAME                         TYPE           CLUSTER-IP      EXTERNAL-IP     PORT(S)          AGE
es-sample-es-http            LoadBalancer   10.120.11.252   34.146.210.75   9200:32020/TCP   10m
(他service)

$ curl -u "elastic:5Esma90y3459ppeh7ubsnU78" "http://34.146.210.75:9200"
{
  "name" : "es-sample-es-default-0",
  "cluster_name" : "es-sample",
  "cluster_uuid" : "YNw1s2ywRoKukHh9mWdiTA",
  "version" : {
    "number" : "8.6.2",
    "build_flavor" : "default",
    "build_type" : "docker",
    "build_hash" : "2d58d0f136141f03239816a4e360a8d17b6d8f29",
    "build_date" : "2023-02-13T09:35:20.314882762Z",
    "build_snapshot" : false,
    "lucene_version" : "9.4.2",
    "minimum_wire_compatibility_version" : "7.17.0",
    "minimum_index_compatibility_version" : "7.0.0"
  },
  "tagline" : "You Know, for Search"
}

これでElasticsearchの疎通はできましたね!
しかしデータを入れないとElasticsearchの意味はありません
次にmongoDBとElasticsearchを同期させる方法について記載します

mongoDBとElasticsearchを同期させる

今回はデータベースとしてmongoDB Atlasを利用します。既にmongoAtlasをセッティングしている前提で説明します。
同期させるデーモンにはmonstacheを利用します。

MongoDB-Atlasのデータサンプル

まず初めにElasticsearchにmonstache用のId,Passwordを設定します。

k8s
 L es.yaml
   es-realm-secret.yaml(new)
kind: Secret
apiVersion: v1
metadata:
  name: es-realm-secret
stringData:
  users: |-
    monstache:password    
  users_roles: |-
    superuser:monstache    
$ kubectl apply -f k8s/es-realm-secret.yaml

$ kubectl get secret es-realm-secret -o go-template='{{.data.users | base64decode}}'
monstache:password

パスワードは今回テスト用なのでそのままにしますが、secretを利用する際はちゃんとmaskして管理してください
また権限は今回、最高権限にしているので各自調整してください

このrealmで作成したsecretをElasticsearchに読み込ませます

k8s
 L es.yaml(modify)
   es-realm-secret.yaml
apiVersion: elasticsearch.k8s.elastic.co/v1
kind: Elasticsearch
metadata:
  name: es-sample
spec:
  version: 8.6.2
  auth:
    fileRealm:
    - secretName: es-realm-secret
  nodeSets:
  - name: default
    count: 1
    config:
      node.store.allow_mmap: false
  http:
    service:
      spec:
        type: LoadBalancer
    tls:
      selfSignedCertificate:
        disabled: true
$ kubectl apply -f k8s/es.yaml

$ curl -u "monstache:password" "http://34.146.210.75:9200"
{
  # Elasticsearch-Info
}

monstache用のパスワードでも疎通できることが確認できましたね

次にmonstache自体を構築していきます
最初にmonstacheの軽い説明をします。monstacheとはmongoDBとElasticsearchをリアルタイムで同期させることのできるデーモンになります。比較的簡単な設定項目で動かせることがメリットになります。
余談ですが最初は、Elastic社が出しているLogstashを利用しようとしたのですが、mongoDBからデータを取得するプラグインのlogstash-input-mongodbが正式版でないことと、コミットが2017年から止まっている(2023/03現在)ので候補から外しました。

ではmonstacheを作成していきます まずmonstacheとElasticsearchとで疎通するための情報をConfigMapとSecretで作成していきます

k8s
 L es.yaml
   es-realm-secret.yaml
   monstache.yaml(new)
apiVersion: v1
kind: ConfigMap
metadata:
  name: monstache-mount-config
data:
  config.toml: |-
    direct-read-namespaces = ["sample-database.sample-collection"]
    change-stream-namespaces = ["sample-database.sample-collection"]

    [[mapping]]
    namespace = "sample-database.sample-collection"
    index = "sample-collection"    

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: monstache-config
data:
  elasticsearch-url: http://es-sample-es-internal-http:9200

---
apiVersion: v1
kind: Secret
metadata:
  name: monstache-secret
stringData:
  mongodb-url: "mongodb+srv://sample-database:password@sample-project.xxxxxxx.mongodb.net"
  es-user: monstache
  es-password: password

elasticsearch-urlのhttp://es-sample-es-internal-http:9200はelasticsearchのserviceです。cluster内ではservice名を指定することでも通信することが可能になります。

$ kubectl get services
NAME                         TYPE           CLUSTER-IP     EXTERNAL-IP    PORT(S)          AGE
es-sample-es-default         ClusterIP      None           <none>         9200/TCP         43h
es-sample-es-http            LoadBalancer   10.120.6.54    34.146.210.75  9200:30861/TCP   43h
es-sample-es-internal-http   ClusterIP      10.120.5.234   <none>         9200/TCP         43h # このserviceのNAME
es-sample-es-transport       ClusterIP      None           <none>         9300/TCP         43h
kubernetes                   ClusterIP      10.120.0.1     <none>         443/TCP          3d

次にmonstacheのDeploymentを作成します
monstacheに設定する環境変数は以下を参照ください
https://rwynn.github.io/monstache-site/config/#elasticsearch-user

k8s
 L es.yaml
   es-realm-secret.yaml
   monstache.yaml(modify)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: monstache-deployment
  labels:
    app: monstache
spec:
  replicas: 1
  selector:
    matchLabels:
      app: monstache
  template:
    metadata:
      labels:
        app: monstache
    spec:
      containers:
      - name: monstache
        image: rwynn/monstache:6.7.11
        ports:
        - containerPort: 80
        command: ["monstache", "-f", "/monstache/config.toml"]
        env:
        - name: MONSTACHE_ES_URLS
          valueFrom:
            configMapKeyRef:
              name: monstache-config
              key: elasticsearch-url
        - name: MONSTACHE_MONGO_URL
          valueFrom:
            secretKeyRef:
              name: monstache-secret
              key: mongodb-url
        - name: MONSTACHE_ES_USER
          valueFrom:
            secretKeyRef:
              name: monstache-secret
              key: es-user
        - name: MONSTACHE_ES_PASS
          valueFrom:
            secretKeyRef:
              name: monstache-secret
              key: es-password
        volumeMounts:
        - name: monstache-volume
          mountPath: /monstache
      volumes:
      - name: monstache-volume
        configMap:
          name: monstache-mount-config

--- # 以下は同じ
apiVersion: v1
kind: ConfigMap
metadata:
  name: monstache-mount-config
data:
  config.toml: |-
    direct-read-namespaces = ["sample-database.sample-collection"]
    change-stream-namespaces = ["sample-database.sample-collection"]

    [[mapping]]
    namespace = "sample-database.sample-collection"
    index = "sample-collection"    

---
apiVersion: v1
kind: ConfigMap
metadata:
  name: monstache-config
data:
  elasticsearch-url: http://es-sample-es-internal-http:9200

---
apiVersion: v1
kind: Secret
metadata:
  name: monstache-secret
stringData:
  mongodb-url: "mongodb+srv://sample-database:password@sample-project.xxxxxxx.mongodb.net"
  es-user: monstache
  es-password: password

applyします

$ kubectl apply -f k8s/monstache.yaml

monstacheが正しく動いているか確認してみましょう

$ kubectl get pods
NAME                                   READY   STATUS    RESTARTS   AGE
es-sample-es-default-0                 1/1     Running   0          44h
monstache-deployment-5f9b8fdb4-9rnrt   1/1     Running   0          5s

$ kubectl logs monstache-deployment-5f9b8fdb4-9rnrt
INFO 2023/03/16 06:49:41 Started monstache version 6.7.11
INFO 2023/03/16 06:49:41 Go version go1.17.4
INFO 2023/03/16 06:49:41 MongoDB go driver v1.10.3
INFO 2023/03/16 06:49:41 Elasticsearch go driver 7.0.31
INFO 2023/03/16 06:49:41 Successfully connected to MongoDB version 5.0.15
INFO 2023/03/16 06:49:41 Successfully connected to Elasticsearch version 8.6.2
INFO 2023/03/16 06:49:41 Listening for events
INFO 2023/03/16 06:49:41 Watching changes on collection sample-database.sample-collection

問題なく動いてそうですね

Elasticsearchにちゃんとデータが入っているかみてみましょう
rootユーザーでElasticsearchにアクセスします

$ curl -u "elastic:5Esma90y3459ppeh7ubsnU78" http://34.146.210.75:9200/_cat/indices
yellow open sample-collection          H9Z5mfJLQJCfIXuSXZbCwg 1 1  2  0    5kb    5kb


$ curl -u "monstache:password" http://34.146.210.75:9200/sample-collection/_search | jq .
{
  "took": 1,
  "timed_out": false,
  "_shards": {
    "total": 1,
    "successful": 1,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 2,
      "relation": "eq"
    },
    "max_score": 1,
    "hits": [
      {
        "_index": "sample-collection",
        "_id": "641270d0bee8466c1c3edf95",
        "_score": 1,
        "_source": {
          "name": "サンプルドキュメント2"
        }
      },
      {
        "_index": "sample-collection",
        "_id": "6412709cbee8466c1c3edf94",
        "_score": 1,
        "_source": {
          "name": "サンプルドキュメント1"
        }
      }
    ]
  }
}

ちゃんとデータが入っていることが確認できました!これでElasticsearchのセットアップは完了になります。
あとはアプリケーションからSDK経由でデータを取得したり、 全文検索エンジンで使用したりするなど様々な使用用途があるのでお好きに

Kibana(ECK)構築

ついでですが、KibanaもECKだといい感じにElasticsearchと通信してくれます。

k8s
 L es.yaml
   es-realm-secret.yaml
   monstache.yaml
   kibana.yaml(new)
apiVersion: kibana.k8s.elastic.co/v1
kind: Kibana
metadata:
  name: kibana-sample
spec:
  version: 8.6.2
  count: 1
  elasticsearchRef:
    name: es-sample
  http:
    service:
      spec:
        type: LoadBalancer
    tls:
      selfSignedCertificate:
        disabled: true

終わりに

今回は、Elasticsearchの構築、mongoDBとの連携まで行いました。意外とmongoDBとの連携記事がないので参考になればなと思います。
まだipでElasticsearchにアクセスしているので、次の記事ではドメインでのアクセスとssl化する方法について執筆できればなと思います。
最後に念を押しておきます。
機密情報を管理する際はSecretで作成したからといってGitHubに平文でpushするのはやめましょう。後々苦労します…
あと、LoadBalancerは個人で使用するにはかなり高いので作成しっぱなしには気をつけてください。後々後悔します…

参考