Edited at

elasticsearch on Kubernetes(Docker for Mac)

More than 1 year has passed since last update.


はじめに

[ pires/kubernetes-elasticsearch-cluster - Github ] を参考にelasticsearchクラスタを立てます。

Kubernetesを使用して拡張性を高く(スケールしやすい等)、

冗長化とx-pack無しのBasic認証導入とデータの永続化までやります。

デフォルトの設定を加えるためGCPのelasticsearchイメージに設定ファイルやプラグインのインストールコマンド、環境変数を加えたものを使用します。


動作確認


  • Docker CE for Mac(Edge): Version 18.02.0

  • Kubectl: Version 1.8

  • elasticsearch: Version 5.6

現時点(2018年2月)ではkubernetesを使うためDocker for Mac edge版を使います。

インストールしていない場合はDocker for Mac with Kubernetes - Qiita等を参考に


elasticsearchのノードの概要

Elasticsearch クラスタ概説 より



  • client

    Master (eligible) node でも data node でもないノード

    各種リクエストを受けるだけのロードバランサー的な何か

  • data

    シャードを保管するノード

    データにまつわる操作 (CRUD, search, etc...) を扱う

  • master (eligible)

    Master node はクラスタ全体の処理(シャードの配置等)を行うノード

    Master eligible node は master に候補になるノード

    Master node が死んだときには master eligible node から新たな master が選ばれる


この3つの役割ごとにpodを分けて作成します。


イメージの準備


設定ファイルの用意

elasticsearchの設定ファイル等を用意します。

$ mkdir elasticsearch_k8s

$ cd elasticsearch_k8s
$ mkdir config; cd $_

作成したconfigディレクトリの中にelasticsearch.ymllog4j2.propertiesファイルを作成します。


elasticsearch.yml

http.host: 0.0.0.0

transport.host: 0.0.0.0

cluster:
name: elasticsearch

node:
master: ${NODE_MASTER}
name: ${NODE_NAME}
data: ${NODE_DATA}
ingest: ${NODE_INGEST}
max_local_storage_nodes: ${MAX_LOCAL_STORAGE_NODES}
network.host: ${NETWORK_HOST}

bootstrap:
memory_lock: false

discovery:
zen:
ping.unicast.hosts: ${DISCOVERY_SERVICE}
minimum_master_nodes: ${NUMBER_OF_MASTERS}


ノードの名前などはクラスタの中で一意である必要がありますが、ここで環境変数を使って設定することで全てのノードで同じファイルを使うことができます。


log4j2.properties

status = error

appender.console.type = Console
appender.console.name = console
appender.console.layout.type = PatternLayout
appender.console.layout.pattern = [%d{ISO8601}][%-5p][%-25c{1.}] %marker%m%n
rootLogger.level = info
rootLogger.appenderRef.console.ref = console

ログに関する設定で、これはGCPのイメージと同じです。

$ tree

config
├── elasticsearch.yml
└── log4j2.propertie

configディレクトリのなかにこの2ファイルが無いとelasticsearchは動きませんでした…。

今回はビルドの中に入れてしまいましたが、開発時などこまめに変更して挙動を確認したい場合はKubernetesのconfigMapとして付けてしまうのも良いかもしれません。というかそちらのほうが正しいのかもしれません?


Dockerイメージ作成

元々はGKEで立てていたため、GCPのランチャーからElasticsearch 5のイメージを使用しています。


Dockerfile

FROM launcher.gcr.io/google/elasticsearch5:5.6

MAINTAINER cmmmli

RUN yes | bin/elasticsearch-plugin install analysis-kuromoji
RUN yes | bin/elasticsearch-plugin install analysis-icu
RUN yes | bin/elasticsearch-plugin install org.codelibs:elasticsearch-analysis-kuromoji-neologd:5.6.1

ADD config /usr/share/elasticsearch/config/

ENV ES_JAVA_OPTS "-Xms256m -Xmx256m"
ENV CLUSTER_NAME elasticsearch
ENV NODE_MASTER true
ENV NODE_DATA true
ENV NODE_INGEST true
ENV HTTP_ENABLE true
ENV NETWORK_HOST _site_
ENV HTTP_CORS_ENABLE true
ENV HTTP_CORS_ALLOW_ORIGIN *
ENV NUMBER_OF_MASTERS 1
ENV MAX_LOCAL_STORAGE_NODES 1
ENV SHARD_ALLOCATION_AWARENESS ""
ENV SHARD_ALLOCATION_AWARENESS_ATTR ""
ENV MEMORY_LOCK false
ENV DISCOVERY_SERVICE elasticsearch-discovery


このDockerfileをconfigディレクトリと同じレベルに置いておきましょう。

プラグイン(kuromojiやneologd)のインストールとelasticsearchの設定ファイル等を加えて環境変数を設定しているだけです。プラグインは必要なければ消しましょう。

podの環境変数に同じキーを指定すると上書きすることが出来るので、ES_JAVA_OPTSNUMBER_OF_MASTERSの値はここでは小さくしています。

$ docker build -t elasticsearch:local .

$ docker images
=> REPOSITORY TAG IMAGE ID CREATED SIZE
elasticsearch local 463f4c837920 2 hours ago 564MB

以上でイメージの作成は完了です!

ローカルでビルドしたイメージをDockerHubやGCRにプッシュせずそのまま使えるのは便利ですね!


Kubernetes


namespace

$ kubectl create namespace elasticsearch


deployment

deploymentはpodのテンプレートとそのレプリカの数を指定して作成することで、podとreplicaSetを自動で作成します。

今回はelasticsearchノードの役割ごとに以下の3種類を作成します。


  • master-deployment

  • data-deployment

  • client-deployment


master-node


deployment-master.yaml

apiVersion: extensions/v1beta1

kind: Deployment
metadata:
name: elasticsearch-master
namespace: elasticsearch
labels:
app: elasticsearch
role: master
spec:
replicas: 1
template:
metadata:
labels:
app: elasticsearch
role: master
spec:
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- image: elasticsearch:local
name: elasticsearch-master
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: "NUMBER_OF_MASTERS"
value: "1"
- name: NODE_MASTER
value: "true"
- name: NODE_DATA
value: "false"
- name: NODE_INGEST
value: "false"
- name: "ES_JAVA_OPTS"
value: "-Xms256m -Xmx256m"
ports:
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: storage
mountPath: /usr/share/elasticsearch/data
volumes:
- name: "storage"
emptyDir:
medium: ""

$ kubectl apply -f deployment-master.yaml

spec.template.metadata.labels


  • app: elasticsearch

  • role: master

を指定しています。この2つのラベルを使うことで、elasticsearchのアプリケーションであることの判断、そのdeploymentが何の役割のものなのかを判断出来ることになります。

initContainers はelasticsearchが要求するvm.max_map_countを満たすために最初に走らせるコンテナです。以下コマンドを叩いています。

$ sysctl -w vm.max_map_count=262144

Virtual memory | Elasticsearch Reference [5.5] | Elastic

containersのelasticsearchの部分で環境変数をいくつか書いています。

NUMBER_OF_MASTERSはelasticsearch.ymlのminimum_master_nodesの部分に使われます。この値はマスター候補のノード数(ここではmaster-deploymentのレプリカ数)をnとすると、

( n/2 ) + 1 が適正値です。

ここで設定した値よりmaster候補のノードが少なくなるとelasticsearchは動かなくなります。

今回は1つしか動かさないので1にしています。


data-node


deployment-data.yaml

apiVersion: extensions/v1beta1

kind: Deployment
metadata:
name: elasticsearch-data
namespace: elasticsearch
labels:
app: elasticsearch
role: data
spec:
replicas: 1
template:
metadata:
labels:
app: elasticsearch
role: data
spec:
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- image: elasticsearch:local
name: elasticsearch-data
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_MASTER
value: "false"
- name: NODE_DATA
value: "true"
- name: NODE_INGEST
value: "false"
- name: "ES_JAVA_OPTS"
value: "-Xms256m -Xmx256m"
ports:
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: storage
mountPath: /usr/share/elasticsearch/data
volumes:
- name: "storage"
emptyDir:
medium: ""

環境変数でNODE_DATA: trueにしている以外はmasterと同じです。


client-node


deployment-client.yaml

apiVersion: extensions/v1beta1

kind: Deployment
metadata:
name: elasticsearch-nginx
namespace: elasticsearch
spec:
replicas: 1
template:
metadata:
labels:
app: elasticsearch
role: client
spec:
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- image: launcher.gcr.io/google/nginx1
name: nginx
volumeMounts:
- name: site-conf
mountPath: /etc/nginx/conf.d
- name: site-top-html
mountPath: /usr/share/nginx/html
- name: htpasswd
mountPath: /etc/nginx/htpasswd
- image: elasticsearch:local
name: elasticsearch-client
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: NODE_MASTER
value: "false"
- name: NODE_DATA
value: "false"
- name: "ES_JAVA_OPTS"
value: "-Xms256m -Xmx256m"
ports:
- containerPort: 9200
name: http
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: storage
mountPath: /usr/share/elasticsearch/data
volumes:
- name: site-conf
configMap:
name: site-conf
- name: site-top-html
configMap:
name: site-top-html
- name: "storage"
emptyDir:
medium: ""
- name: htpasswd
secret:
secretName: htpasswd

clientのdeploymentはこれまでの2つと少し違っていて、

containersでelasticsearchとnginxのimageを指定しているため、このdeploymentによって作成されるpodではelasticsearchとnginxのコンテナが1つずつの計2つ動いています。

そしてnginxのコンテナではconfigMapとsecretを使って設定ファイル等を付けているので、それらを作成します!


configmap.yaml

apiVersion: v1

kind: ConfigMap
metadata:
name: site-conf
namespace: elasticsearch
data:
site.conf: |
server_tokens off;
server {
listen 80;

location / {
root /usr/share/nginx/html;
index index.html index.htm;
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}

location /search/ {
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/htpasswd/.htpasswd;
proxy_pass http://localhost:9200/;
}
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: site-top-html
namespace: elasticsearch
data:
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>elasticsearch</title>
<style>
body {
width: 35em;
margin: 0 auto;
}
</style>
</head>
<body>
<h1>HealthCheck OK!</h1>

<p>elasticsearch is <a href="/search">here</a></p>

</body>
</html>


$ kubectl apply -f configmap.yaml

.htpasswdファイルを作成して、それを基にsecretを作ります。

.htpasswdファイルの作り方はこちらを参考にしました。

$ kubectl create secret generic htpasswd -n elasticsearch --from-file=.htpasswd

nginxコンテナはGCPランチャーのイメージを使っています。

このコンテナでは、volumeMounts でnginxの設定ファイルが入ったディレクトリとリダイレクト前のトップページのhtmlを読み込んでいます。このディレクトリとhtmlはconfigMapで作成したものです。

そもそもnginxを受け手として付けた理由ですが、今回はelasticsearchのプラグインであるx-packを入れていません。

そのため公開したelasticsearchにはどこからでもアクセスして編集可能な状態になってしまうため、一応認証としてbasic認証を付けています。

elasticsearchコンテナでもstorageをemptyDirのvolumeとしてマウントしています。


これによって、podがリスタートしてもデータを保持出来ます(再生成はダメ)。

clientのpodではserviceからnginxコンテナでリクエストを受け取り、basic認証を通ったものを同podのelasticsearchコンテナにリダイレクトしています。


service


services.yaml

kind: Service

apiVersion: v1
metadata:
name: elasticsearch
namespace: elasticsearch
labels:
app: elasticsearch
role: client
spec:
type: LoadBalancer
selector:
app: elasticsearch
role: client
ports:
- protocol: TCP
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-discovery
namespace: elasticsearch
labels:
component: elasticsearch
role: master
spec:
type: NodePort
selector:
app: elasticsearch
role: master
ports:
- name: transport
port: 9300
protocol: TCP

外部からnginxのコンテナまで繋ぐためのサービスと、

elasticsearchが内部で通信に使うサービスを作成しています。

docker-for-macではtype: LoadBalancerとすることでブラウザからlocalhostで繋がるみたいです。

Docker for Mac にKubernetesがやって来た!(ちょっと追記)

これまで作成した以下のファイルを作成すると、http://localhost:80 にアクセス出来ます!


  • deployment-master.yaml

  • deployment-data.yaml

  • deployment-client.yaml

  • configmap.yaml

  • services.yaml

ss1

elasticsearchにアクセスするにはhttp://localhost/search です。

basic認証としてユーザ名とパスワードを求められます。

user: elastic

pass: changeme

このパスワードなどはnginxコンテナにつけているconfigMapの.htpasswdを変更することで変更できます。

ss2

$ kubectl get pod -n elasticsearch

NAME READY STATUS RESTARTS AGE
elasticsearch-data-db656cf4c-zpbc4 1/1 Running 0 3h
elasticsearch-master-f94b866f4-kjzlf 1/1 Running 0 2h
elasticsearch-nginx-c76c7cc6b-twrrl 2/2 Running 0 2h

$ curl -L http://localhost:80/search/_cat/nodes -u 'elastic:changeme'

10.1.0.9 27 97 12 0.79 0.65 0.67 d - elasticsearch-data-db656cf4c-zpbc4
10.1.0.11 49 97 12 0.79 0.65 0.67 i - elasticsearch-nginx-c76c7cc6b-twrrl
10.1.0.12 30 97 12 0.79 0.65 0.67 m * elasticsearch-master-f94b866f4-kjzlf

data, ingest, masterそれぞれのノードが動いていることが確認できます。

$ curl -L 'http://localhost:80/search/_cluster/health?pretty' -u 'elastic:changeme'

{
"cluster_name" : "elasticsearch",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 3,
"number_of_data_nodes" : 1,
"active_primary_shards" : 0,
"active_shards" : 0,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}

良さそうですね。

冗長化はそれぞれのdeploymentのレプリカ(spec.replicas)を増やせば出来ます!


データの永続化


statefulSet

ここまででelasticsearchは動きましたが、データの永続化が出来ていません。

imageの変更などでdeploymentをapplyし直すとデータが飛んでしまう状態です。

これを解決するためにdeploymentではなくstatefulSetを使ってみようと思います。

statefulSetはdeploymentで作成されるpodに一意性と状態を持たせることが出来るようにしたものです。

podの名前が ***-0 のようになり順番に作成されます。

podはvolumeClaimTemplatesによってpvcを作り、そのpvcがpersistentVolumeを作る事によってデータの永続化が出来ています。storage-classを定義して適用すれば様々なストレージを使うことが出来ます。

deploymentではdata, master, clientとそれぞれの役割ごとに作成しましたが、statefulSetでは全部入りのpodを作成しようと思います。

必要なクラスタが小規模であれば全部入りを数台構成で運用可能であるため、それに対応するためです。

statefulSetを作成する前に先程作ったdeploymentを消してしまいます。

$ kubectl delete deployment -n elasticsearch --all


statefulSet-full.yaml

apiVersion: apps/v1beta1

kind: StatefulSet
metadata:
name: elasticsearch-full-sample
namespace: elasticsearch
labels:
app: elasticsearch
spec:
replicas: 1
serviceName: elasticsearch-discovery
updateStrategy:
type: RollingUpdate
rollingUpdate:
podManagementPolicy: Parallel
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: init-sysctl
image: busybox
imagePullPolicy: IfNotPresent
command: ["sysctl", "-w", "vm.max_map_count=262144"]
securityContext:
privileged: true
containers:
- image: launcher.gcr.io/google/nginx1
name: nginx
volumeMounts:
- name: site-conf
mountPath: /etc/nginx/conf.d
- name: site-top-html
mountPath: /usr/share/nginx/html
- name: htpasswd
mountPath: /etc/nginx/htpasswd
- image: elasticsearch:local
name: elasticsearch-full
env:
- name: NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: "NUMBER_OF_MASTERS"
value: "1"
- name: NODE_MASTER
value: "true"
- name: NODE_DATA
value: "true"
- name: "ES_JAVA_OPTS"
value: "-Xms1024m -Xmx1024m"
ports:
- containerPort: 9200
name: http
protocol: TCP
- containerPort: 9300
name: transport
protocol: TCP
volumeMounts:
- name: storage
mountPath: /usr/share/elasticsearch/data
volumes:
- name: site-conf
configMap:
name: site-conf
- name: site-top-html
configMap:
name: site-top-html
- name: htpasswd
secret:
secretName: htpasswd
volumeClaimTemplates:
- metadata:
name: storage
annotations:
volume.beta.kubernetes.io/storage-class: hostpath
spec:
accessModes: [ "ReadWriteOnce" ]
resources:
requests:
storage: 5Gi

$ kubectl apply -f statefulSet-full.yaml

$ kubectl get pod,sts -n elasticsearch
NAME READY STATUS RESTARTS AGE
po/elasticsearch-full-sample-0 2/2 Running 0 1m

NAME DESIRED CURRENT AGE
statefulsets/elasticsearch-full-sample 1 1 1m

作成出来ましたがlocalhostにアクセスしてもコンテナまで繋がらないと思います。

これは作成したサービスがroleも見ているためです。そこだけ編集します。


services-full.yaml

kind: Service

apiVersion: v1
metadata:
name: elasticsearch
namespace: elasticsearch
labels:
app: elasticsearch
spec:
type: LoadBalancer
selector:
app: elasticsearch
ports:
- protocol: TCP
port: 80
---
apiVersion: v1
kind: Service
metadata:
name: elasticsearch-discovery
namespace: elasticsearch
labels:
component: elasticsearch
spec:
type: NodePort
selector:
app: elasticsearch
ports:
- name: transport
port: 9300
protocol: TCP

$ kubectl apply -f services-full.yaml

service "elasticsearch" configured
service "elasticsearch-discovery" configured

以上で繋がるようになっています。

statefulSetを使うことで、ノードが再起動する等してもデータを永続化することが出来ます。

image更新の際にもデータは引き継がれます。


GKE(Google Kubernetes Engine)で動かすために

基本的にはそのまま動くと思います。バージョンによってapiが違っていたりするので、そこは使用しているバージョンのリファレンスを見ましょう。


GKEでのHTTPS化

https化はまた別の記事にでも書こうかなと思っていますが、

ingressとkube-legoを使ってlet's encryptの証明書を取得することで出来ます。

GKE でサービスを HTTPS と HTTP/2 に対応する(kube-lego 編) - Qiita

更に、helmを使ってhttps化をより簡単にすることが出来ます。

しかしkube-legoは開発終了してしまったみたいなので、引き継がれたcert-managerを使ったほうがいいのかもしれません(試してないです)。

そしてelasticsearchの検索クエリでは、パラメータ付GETが使われているのですが、GCPのロードバランサがそれを弾いてしまうため、POSTで検索クエリを投げるようにする等結構面倒でした。

そしてGCEのingress controllerを使ってingressを作成するとBackend serviceのヘルスチェックが行われます。

ここでやっかいなのはデフォルトではserviceへのアクセスで200を返さなければ、そのサービスは停止しているとみなされingressが機能しません。

livenessProbe等を用いると解決できそうですが、elasticsearch単体でx-packを用いてbasic認証をかけると全てのアクセスで認証が必要になるため、ヘルスチェックが通らずingressを使うことが出来ませんでした。

今回nginxを通したのはそのヘルスチェックへの対応策でもあります。


片付け!

$ kubectl delete -n elasticsearch cm,svc,pv,pvc,sts --all

$ kubectl delete namespace elasticsearch


おわりに

インフラとelasticsearchについてほぼ無知な状態から始まり、なんとか動かすことが出来ました。

elasticsearchのconfigディレクトリをイメージのビルド時に付けているので、辞書の更新時にイメージを更新してapplyする必要があり、その際にダウンタイムが発生してしまっているのでそこをどうにか出来れば良いなと思っています。

加えて、共通部分が多いyamlファイルが複数出来てしまっているので、管理の手間を何とかしたいです。


参考