はじめに
ラズパイkubernetesクラスタ用のelasticsearchクラスタ(3ノード)をstatefulsetでデプロイしたくなって、kubernetesのstatefulsetなどの勉強も兼ねて模索しました。なかなか汎用的なyamlを作るのは難しいですね...
まだkubernetes初心者マークが外れない感じです。
ゴールは、cephストレージを使用したelasticsearchを3ノード構成で冗長化し、更にanalysis-kuromojiのインストールとBASIC認証も設定します。BASIC認証については、無償(BASIC)ライセンスではtransportの暗号化もしなくてはなりませんので、一旦、BASIC認証なしの構成を構築して証明書の作成をします。
また、私の自宅のネットワーク環境はノードからはインターネットへの接続ができますが、コンテナからインターネットへの接続はできませんので、analysis-kuromojiのインストールは、masterノードで動いているNode-RED(http)でプラグインファイルを共有しています。
色々探したんですけど、elasticsearchのプラグインとBASIC認証を有効にしたオンプレkubernetesのyamlの事例が無いんですよね...
前提環境
手抜きですみません。オンプレkubernetesです。(いわゆる「一家に一台、おうちkubernetes」です)
# kubectl get nodes -o wide
NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME
chino Ready master 20d v1.19.2 10.0.0.1 <none> Debian GNU/Linux 10 (buster) 5.4.51-v8+ docker://19.3.13
chiya Ready worker 20d v1.19.2 10.0.0.5 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://19.3.13
cocoa Ready worker 13d v1.19.2 10.0.0.2 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://19.3.13
rize Ready worker 13d v1.19.2 10.0.0.3 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://19.3.13
syaro Ready worker 13d v1.19.2 10.0.0.4 <none> Debian GNU/Linux 10 (buster) 5.4.65-v8+ docker://19.3.13
構築
ネームスペースの作成とcephストレージアクセス用secretの登録
elasticsearchクラスタをデプロイするネームスペース「elastic」を作成します。
前提部分になるので、ご自身で使われている環境に合わせて設定してください。
# kubectl create namespace elastic
namespace/elastic created
で、次は任意なのですが、私の構成では永続ストレージにcephを利用するので、このネームスペースでもsecretを登録します。
apiVersion: v1
kind: Secret
metadata:
name: csi-rbd-secret
namespace: elastic
stringData:
userID: kubernetes
userKey: AQBrNmZfVCowLBAAeN3EYjhOPBG9442g4NF/bQ==
# kubectl apply -f csi-rbd-secret.yaml
secret/csi-rbd-secret created
サービスリソースの定義
elasticsearch用のサービス(http,transport)を定義します。
httpだけNodePortしたかったのですが、transportもNodePrtになってしまいました。
細かく制御するのはまた後の宿題にします。
apiVersion: v1
kind: Service
metadata:
name: elasticsearch
namespace: elastic
labels:
app: elasticsearch
spec:
type: NodePort
ports:
- name: http
port: 9200
targetPort: 9200
nodePort: 31920
protocol: TCP
- name: transport
port: 9300
targetPort: 9300
nodePort: 31930
protocol: TCP
selector:
app: elasticsearch
# kubectl apply -f elasticsearch-svc.yaml
service/elasticsearch created
statefulsetでの3ノードクラスタのデプロイ
次に、elasticsearchクラスタをstatefulsetでデプロイする設定です。
この時点で、elasticsearchは以下の設定にしています。これで良いならこのstatefulsetをデプロイしておしまいです。
- xpack.security.enabled:false (認証なし)
- kuromoji-analysis (日本語解析器プラグインインストール)
- 冗長性担保のために、3ノードはkubernetesの物理ノードに1ノードずつデプロイする
elasticsearchのpluginについては、永続ストレージを用意し、initContainersでpluginのインストールコマンドを実行します。
気を付けたいのは、pluginsディレクトリにはプラグイン以外のファイルがあるとelasticsearchが起動時にスキャンしてエラーになるので、事前に「rm -rf」であらかじめディレクトリ内を掃除をしておきます。
他には、ノード名はkubernetesの内部DNSで解決できるFQDN形式で記載します。
そもそもFQDN形式がわからない場合(私がそうでした)は、elasticsearch-0のRunning後、Errorになるまでに時間があるので「ping elasticsearch-0」をしてみてください。
おそらく、「<ポッド名>.<サービス名>.<ネームスペース名>.svc.cluster.local」となっています。
# kubectl exec -it elasticsearch-0 -n elastic -- ping elasticsearch-0
PING elasticsearch-0.elasticsearch.elastic.svc.cluster.local (172.16.2.106) 56(84) bytes of data.
64 bytes from elasticsearch-0.elasticsearch.elastic.svc.cluster.local (172.16.2.106): icmp_seq=1 ttl=64 time=0.135
あとは、メモリやCPUはリソースが許す限りのお好みで設定してください。
requestsにはデプロイする際のリソースのしきい値を定義し、limitsには最大で利用できる値を設定しますが、同じ値で良いと思います。
elasticsearchのJavaオプション-Xms/-Xmxについても同じ値で良いと思います。目安としてはlimitsの25〜50%減らした値が良いです。limitsと同じにしたり、多く指定するとOOMでコンテナが起動失敗します。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: elastic
spec:
selector:
matchLabels:
app: elasticsearch
serviceName: "elasticsearch"
replicas: 3
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: plugins-install
image: elasticsearch:7.9.2
command: ['sh', '-c']
args:
- |
rm -rf /usr/share/elasticsearch/plugins/*; /usr/share/elasticsearch/bin/elasticsearch-plugin install http://10.0.0.1:1880/analysis-kuromoji-7.9.2.zip
volumeMounts:
- name: es-plugins
mountPath: /usr/share/elasticsearch/plugins
containers:
- name: elasticsearch
image: elasticsearch:7.9.2
resources:
requests:
cpu: 1
memory: 1Gi
limits:
cpu: 1
memory: 1Gi
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: network.host
value: "0.0.0.0"
- name: node.name
value: $(NODE_NAME).elasticsearch.elastic.svc.cluster.local
- name: transport.host
value: $(NODE_NAME).elasticsearch.elastic.svc.cluster.local
- name: cluster.name
value: elasticsearch_cluster
- name: discovery.seed_hosts
value: elasticsearch-0.elasticsearch.elastic.svc.cluster.local,elasticsearch-1.elasticsearch.elastic.svc.cluster.local,elasticsearch-2.elasticsearch.elastic.svc.cluster.local
- name: cluster.initial_master_nodes
value: elasticsearch-0.elasticsearch.elastic.svc.cluster.local,elasticsearch-1.elasticsearch.elastic.svc.cluster.local,elasticsearch-2.elasticsearch.elastic.svc.cluster.local
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
- name: xpack.ml.enabled
value: "false"
ports:
- name: http
containerPort: 9200
- name: transport
containerPort: 9300
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: es-plugins
mountPath: /usr/share/elasticsearch/plugins
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- elasticsearch
topologyKey: "kubernetes.io/hostname"
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: csi-rbd-sc
- metadata:
name: es-plugins
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
storageClassName: csi-rbd-sc
statefulsetのデプロイは1Podずつシーケンシャルに行われるので時間がかかりますが、私の運用ではそんなに頻繁に再起動するものでもないので問題ありません。
もし、迅速なデプロイが必要なシステムなのであれば、毎回同じノードに同じストレージが使われるようdeploymentで並行してデプロイするようなOperatorを開発すると良いかと思ってます。
# kubectl apply -f elasticsearch-sts.yaml
statefulset.apps/elasticsearch created
状態確認です。
# kubectl get sts,pod,pvc -n elastic -o wide
NAME READY AGE CONTAINERS IMAGES
statefulset.apps/elasticsearch 3/3 24m elasticsearch elasticsearch:7.9.2
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
pod/elasticsearch-0 1/1 Running 0 24m 172.16.4.74 syaro <none> <none>
pod/elasticsearch-1 1/1 Running 0 23m 172.16.3.83 rize <none> <none>
pod/elasticsearch-2 1/1 Running 0 23m 172.16.2.65 cocoa <none> <none>
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE VOLUMEMODE
persistentvolumeclaim/es-data-elasticsearch-0 Bound pvc-19d4e20b-490f-4ba8-9ceb-c26056d07c87 10Gi RWO csi-rbd-sc 139m Filesystem
persistentvolumeclaim/es-data-elasticsearch-1 Bound pvc-2d491f80-7a87-492a-98da-602955a86b03 10Gi RWO csi-rbd-sc 138m Filesystem
persistentvolumeclaim/es-data-elasticsearch-2 Bound pvc-195a45a4-c59c-49fd-b423-5cbaa70fcca6 10Gi RWO csi-rbd-sc 138m Filesystem
persistentvolumeclaim/es-plugins-elasticsearch-0 Bound pvc-f468536b-b589-49d4-8391-b7f43db515de 50Mi RWO csi-rbd-sc 11h Filesystem
persistentvolumeclaim/es-plugins-elasticsearch-1 Bound pvc-f74d780d-56d2-4845-baab-87690dc96e23 50Mi RWO csi-rbd-sc 11h Filesystem
persistentvolumeclaim/es-plugins-elasticsearch-2 Bound pvc-fa5bc7db-4f4d-485c-94b1-5b0a6f3a12cf 50Mi RWO csi-rbd-sc 11h Filesystem
BASIC認証をしないのであれば、これで作業は終了です。お疲れ様でした。
クライアント証明書の作成
elasticsearchクラスタを起動した状態で、transportを暗号化する際に使用する証明書の作成をします。
まずCA証明書の作成をして、お目当てのクライアント証明書の作成をして、コンテナ外にコピーしてconfigmapにします。
とりあえず、コンテナ内の悪影響の無い場所にCA証明書を作成します。
# kubectl exec -it elasticsearch-0 -n elastic -- /usr/share/elasticsearch/bin/elasticsearch-certutil ca --out /elastic_cluster.p12 --pass ''
CA証明書を指定して、クライアント証明書を作成します。
# kubectl exec -it elasticsearch-0 -n elastic -- /usr/share/elasticsearch/bin/elasticsearch-certutil cert --ca /elastic_cluster.p12 --ca-pass '' --out /elastic-cert.p12 --pass ''
クライアント証明書を操作している環境へコピーします。
# kubectl cp -n elastic elasticsearch-0:/elastic-cert.p12 elastic-cert.p12
クライアント証明書をconfigmapにします。※secretでもかまいません。
# kubectl create configmap elastic-cert -n elastic --from-file=elastic-cert.p12
configmap/elastic-cert created
statefulsetでの3ノードクラスタのデプロイ(BASIC認証あり版)
kubernetesから、稼働しているelasticsearchクラスタを破棄します。
# kubectl delete -f elasticsearch-sts.yaml
statefulset.apps "elasticsearch" deleted
といっても、先ほど作成したマニフェストファイルは、elasticsearchのBASIC認証あり(かつ、transportの暗号化)のベースにします。
# cp elasticsearch-sts.yaml elasticsearch-sts-sec.yaml
次にelasticsearchのBASIC認証あり、かつ、transportの暗号化のstatefulsetのymalを作成します。
追加部分は、elasticsearchのセキュリティ設定(環境変数)と、クライアント証明書のconfigmapをファイル単体でvolumemountしています。
elasticsearchの特権ユーザ「elastic」のパスワードは、「ELASTIC_PASSWORD」に設定している「elastic」です。
他のユーザ含めてパスワード設定する場合は「ELASTIC_PASSWORD」の定義を削除してください。
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: elasticsearch
namespace: elastic
spec:
selector:
matchLabels:
app: elasticsearch
serviceName: "elasticsearch"
replicas: 3
template:
metadata:
labels:
app: elasticsearch
spec:
initContainers:
- name: plugins-install
image: elasticsearch:7.9.2
command: ['sh', '-c']
args:
- |
rm -rf /usr/share/elasticsearch/plugins/*; /usr/share/elasticsearch/bin/elasticsearch-plugin install http://10.0.0.1:1880/analysis-kuromoji-7.9.2.zip
volumeMounts:
- name: es-plugins
mountPath: /usr/share/elasticsearch/plugins
containers:
- name: elasticsearch
image: elasticsearch:7.9.2
resources:
requests:
cpu: 1
memory: 1Gi
limits:
cpu: 1
memory: 1Gi
env:
- name: NODE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: network.host
value: "0.0.0.0"
- name: node.name
value: $(NODE_NAME).elasticsearch.elastic.svc.cluster.local
- name: transport.host
value: $(NODE_NAME).elasticsearch.elastic.svc.cluster.local
- name: cluster.name
value: elasticsearch_cluster
- name: discovery.seed_hosts
value: elasticsearch-0.elasticsearch.elastic.svc.cluster.local,elasticsearch-1.elasticsearch.elastic.svc.cluster.local,elasticsearch-2.elasticsearch.elastic.svc.cluster.local
- name: cluster.initial_master_nodes
value: elasticsearch-0.elasticsearch.elastic.svc.cluster.local,elasticsearch-1.elasticsearch.elastic.svc.cluster.local,elasticsearch-2.elasticsearch.elastic.svc.cluster.local
- name: ES_JAVA_OPTS
value: "-Xms512m -Xmx512m"
- name: ELASTIC_PASSWORD
value: "elastic"
- name: xpack.ml.enabled
value: "false"
- name: xpack.security.enabled
value: "true"
- name: xpack.security.transport.ssl.enabled
value: "true"
- name: xpack.security.transport.ssl.verification_mode
value: "certificate"
- name: xpack.security.transport.ssl.keystore.path
value: "elastic-cert.p12"
- name: xpack.security.transport.ssl.truststore.path
value: "elastic-cert.p12"
ports:
- name: http
containerPort: 9200
- name: transport
containerPort: 9300
volumeMounts:
- name: es-data
mountPath: /usr/share/elasticsearch/data
- name: es-plugins
mountPath: /usr/share/elasticsearch/plugins
- name: elastic-cert
mountPath: /usr/share/elasticsearch/config/elastic-cert.p12
subPath: elastic-cert.p12
volumes:
- name: elastic-cert
configMap:
name: elastic-cert
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values:
- elasticsearch
topologyKey: "kubernetes.io/hostname"
volumeClaimTemplates:
- metadata:
name: es-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
storageClassName: csi-rbd-sc
- metadata:
name: es-plugins
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Mi
storageClassName: csi-rbd-sc
# kubectl apply -f elasticsearch-sts-sec.yaml
statefulset.apps/elasticsearch created
「ELASTIC_PASSWORD」を定義しなかった場合や、kibana等で使用するパスワードを初期化したい場合は、elasticsearchのコンテナに入り、「elasticsearch-setup-password」コマンドでパスワード設定をしてください。
コマンドで設定したパスワードは、elasticsearch内のインデックス(永続ストレージ)に書き込まれるので、再起動後も保持されます。※elasticユーザのパスワードは、環境変数の「ELASTIC_PASSWORD」より優先されます。
# kubectl exec -it elasticsearch-0 -n elastic -- /usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive
Initiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.
You will be prompted to enter passwords as the process progresses.
Please confirm that you would like to continue [y/N]y
Enter password for [elastic]:
Reenter password for [elastic]:
Enter password for [apm_system]:
Reenter password for [apm_system]:
Enter password for [kibana_system]:
Reenter password for [kibana_system]:
Enter password for [logstash_system]:
Reenter password for [logstash_system]:
Enter password for [beats_system]:
Reenter password for [beats_system]:
Enter password for [remote_monitoring_user]:
Reenter password for [remote_monitoring_user]:
Changed password for user [apm_system]
Changed password for user [kibana_system]
Changed password for user [kibana]
Changed password for user [logstash_system]
Changed password for user [beats_system]
Changed password for user [remote_monitoring_user]
Changed password for user [elastic]
動作確認です。
# curl -u elastic http://localhost:31920
Enter host password for user 'elastic':
{
"name" : "elasticsearch-1.elasticsearch.elastic.svc.cluster.local",
"cluster_name" : "elasticsearch_cluster",
"cluster_uuid" : "u_mabT4gRDS-evcYFOsgMQ",
"version" : {
"number" : "7.9.2",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "d34da0ea4a966c4e49417f2da2f244e3e97b4e6e",
"build_date" : "2020-09-23T04:28:49.179747Z",
"build_snapshot" : false,
"lucene_version" : "8.6.2",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
# curl -u elastic http://localhost:31920/_cat/nodes?v
Enter host password for user 'elastic':
ip heap.percent ram.percent cpu load_1m load_5m load_15m node.role master name
172.16.3.118 53 47 88 2.42 2.04 1.54 dimrt * elasticsearch-1.elasticsearch.elastic.svc.cluster.local
172.16.2.103 48 48 81 1.99 1.78 1.50 dimrt - elasticsearch-0.elasticsearch.elastic.svc.cluster.local
172.16.4.99 34 45 98 1.61 1.58 1.34 dimrt - elasticsearch-2.elasticsearch.elastic.svc.cluster.local
おわりに
pluginのインストールはそこそこ面倒でしたが、勉強になりました。「あらかじめプラグインをインストールしたストレージを準備して...」も考えたのですが、レプリカ数を変更できるようにもしたかったので、デプロイに時間はかかるけど汎用性を持たせました。
証明書の作成は手順が煩雑なわりに手数少なくできたと思います。http側を暗号化していないのですが、自宅外から利用する場合は、ngrokでSSL化しているので問題ないと思ってます。
kubernatesといっても「Raspberry Pi」を使っているので、いつもリソースに余裕は無く、「最低限のリソースで冗長化したい!」という事が多いのですが、cephに続きelasticsearchもなんとか構築できました。他にも、クラスタモニタリング(prometheus,grafanaなど)も稼働してますし、dashboardやkibanaも動かしています。