2019年6月16日日曜日

k8s 上に ElasticSearch7.1 クラスタを構築してみた

概要

前回 docker-compose で構築しました
今回は k8s 上に構築するのにチャレンジしました
StatefulSet + Headless Service で構築しています
シングル構成から複数台構成まで順を追って説明します

環境

  • macOS 10.14.5
  • minikube 1.1.0
  • ElasticSearch 7.1

とりあえず run (Deployment)

まずは kubctl run でシングル構成で立ち上げます

  • kubectl run elasticsearch --image=docker.elastic.co/elasticsearch/elasticsearch:7.1.1 --env="discovery.type=single-node" --port=9200

kubectl run は Deployment を使っています
YAML 定義を確認するには -o yaml --dry-run オプションを付与します

作成されたリソースを確認するには Deployment, ReplicaSet, Pod を確認します

  • kubectl get deploy,rs,po
NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/elasticsearch   1/1     1            1           12s

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.extensions/elasticsearch-775d564d6c   1         1         1       12s

NAME                                 READY   STATUS    RESTARTS   AGE
pod/elasticsearch-775d564d6c-h4hxs   1/1     Running   0          12s

動作確認は exec を使って直接 Pod の localhost にアクセスしてみましょう

  • kubectl exec elasticsearch-775d564d6c-h4hxs curl localhost:9200/_cat/health
  • kubectl exec elasticsearch-775d564d6c-h4hxs curl localhost:9200/_cat/nodes

リソースの削除は Deployment を削除すれば OK です

  • kubectl delete deploy elasticsearch

シングル構成 (Pod)

次に YAML ファイルを定義してシングル構成を構築してみます
先程 run で Deployment 経由の構築はしたので今回は Pod リソースを使った YAML ファイルを定義してみます
※先程の run に --restart=Never を付与すれば Pod のみになりますが勉強のため自分で YAML ファイルを作成しています

  • vim es_master_pod.yml
apiVersion: v1
kind: Pod
metadata:
  name: es-pod
  labels:
    app: es
spec:
  containers:
  - name: es
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    env:
    - name: discovery.type
      value: single-node
    ports:
    - containerPort: 9200
      name: api
    - containerPort: 9300
      name: gossip
  restartPolicy: Never

ポイントは env で discovery.type を渡している点です
これを apply しましょう

  • kubectl apply -f es_master_pod.yml

作成されるリソースは Pod になります

  • kubectl get po
NAME     READY   STATUS    RESTARTS   AGE
es-pod   1/1     Running   0          4s

動作確認します

  • kubectl exec es-pod curl localhost:9200/_cat/health

リソースの削除は Pod を削除すれば OK です

  • kubectl delete po es-pod

シングル構成 (StatefulSet)

次にシングル構成を StatefulSet を使って構築します
今回は 3 台構成のクラスタを StatefulSet を使って構築します
そのための準備としてまずはシングル構成を構築します

  • vim kubectl apply -f es_master_sts1.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: es-cluster
spec:
  selector:
    matchLabels:
      app: es
  serviceName: "es-cluster"
  replicas: 1
  template:
    metadata:
      labels:
        app: es
    spec:
      containers:
      - name: es
        image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
        env:
        - name: discovery.type
          value: single-node
        ports:
        - containerPort: 9200
          name: api
        - containerPort: 9300
          name: gossip
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

PVC (PersistentVolumeClaim) を追加しています
template.spec.containers は先程の Pod の定義とほぼ同じです
selector.matchLabels と template.metadata.labels は同じになるようにします
これで起動してみましょう

  • kubectl apply -f es_master_sts1.yml

作成されるリソースは StatefulSet, Pod, PVC になります

  • kubectl get sts,po,pvc
NAME                          READY   AGE
statefulset.apps/es-cluster   1/1     49s

NAME               READY   STATUS    RESTARTS   AGE
pod/es-cluster-0   1/1     Running   0          48s

NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-es-cluster-0   Bound    pvc-ae9aa9e8-8e3a-11e9-8372-080027d5c49e   1Gi        RWO            standard       48s

動作確認しましょう

  • kubectl exec es-cluster-0 curl localhost:9200/_cat/health

ホスト名も確認してみましょう
StatefulSet を使っているのでホスト名が固定されているのが確認できると思います

  • kubectl exec es-cluster-0 hostname

削除は StatefulSet と PVC を削除すれば OK です

  • kubectl delete sts es-cluster
  • kubectl delete pvc data-es-cluster-0

3 台クラスタ構成 (StatefulSet + Headless Service)

さてここからが本番です
3 台構成のクラスタを StatefulSet を使って構築します
また Headless Service も使います
Headless Service を使うとホスト名+サブドメイン (サービス名) で他の Pod にアクセスすることができます
クラスタを構成するにあたり IP ではなくホスト名を使いたいので Headless Service を使います

  • vim es_master_svc.yml
apiVersion: v1
kind: Service
metadata:
  name: es-cluster
  labels:
    app: es
spec:
  clusterIP: None
  ports:
  - port: 9200
    name: api
  - port: 9300
    name: gossip
  selector:
    app: es

まずは Headless Service を作成します

  • kubectl apply -f es_master_svc.yml

これで同じ namespace 配下の Pod 間で名前解決ができるようになります
次に StatefulSet を作成します

  • vim kubectl apply -f es_master_sts3.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: es-cluster
spec:
  selector:
    matchLabels:
      app: es
  serviceName: "es-cluster"
  replicas: 3
  template:
    metadata:
      labels:
        app: es
    spec:
      containers:
      - name: es
        image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
        env:
        - name: HOSTNAME_NAME
          valueFrom:
            fieldRef:
              fieldPath: metadata.name
        - name: node.name
          value: $(HOSTNAME_NAME).es-cluster
        - name: cluster.name
          value: k8s-cluster
        - name: discovery.seed_hosts
          value: es-cluster-0.es-cluster,es-cluster-1.es-cluster,es-cluster-2.es-cluster
        - name: cluster.initial_master_nodes
          value: es-cluster-0.es-cluster,es-cluster-1.es-cluster,es-cluster-2.es-cluster
        - name: ES_JAVA_OPTS
          value: "-Xms512m -Xmx512m"
        ports:
        - containerPort: 9200
          name: api
        - containerPort: 9300
          name: gossip
        volumeMounts:
        - name: data
          mountPath: /data
  volumeClaimTemplates:
  - metadata:
      name: data
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 1Gi

だいぶ長くなりました
が一つずつ見ればそこまで複雑ではありません
serviceName: "es-cluster" は Headless Service の metadata.name と同じにします
3 台構成にするので replicas: 3 にしましょう
一番のポイントは env になります
ElasticSearch のクラスタを構成する場合 node.name と discovery.seed_hosts, cluster.initial_master_nodes そしてマシンの hostname はすべて合わせたほうが無難です
IP でもできるようですが個人的にはハマりやすいのでできれば名前でアクセスできるようにしましょう
今回はそのために Headless Service を使っています

まず HOSTNAME_NAME に fieldPath: metadata.name でホスト名を環境変数に設定しています
今回の 3 台構成であれば es-cluster-0, es-cluster-1, es-cluster-2 がそれぞれ動的に代入されます
そしてそれを使って node.name を $(HOSTNAME_NAME).es-cluster にしています
なぜわざわざマシンのホスト名にサブドメインの .es-cluster を付与しているのかというと Headless Service の場合他の Pod にアクセスするにはホスト名だけではなくサブドメインも必要になります
なので discovery.seed_hosts と cluster.initial_master_nodes は es-cluster-0.es-cluster という感じにしなければクラスタに所属させるホストを見つけることができません
でここに指定した値と node.name は同じにする必要があるためわざわざこのような方法にしています
(もしかするともっと簡単な方法があるかもしれませんが)

  • kubectl apply -f es_master_sts3.yml

あとはこれを適用して確認してみます

  • kubectl get sts,po,pvc,svc
NAME                          READY   AGE
statefulset.apps/es-cluster   3/3     45s

NAME               READY   STATUS    RESTARTS   AGE
pod/es-cluster-0   1/1     Running   0          45s
pod/es-cluster-1   1/1     Running   0          40s
pod/es-cluster-2   1/1     Running   0          35s

NAME                                      STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
persistentvolumeclaim/data-es-cluster-0   Bound    pvc-f581cf6c-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       45s
persistentvolumeclaim/data-es-cluster-1   Bound    pvc-f8371d19-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       40s
persistentvolumeclaim/data-es-cluster-2   Bound    pvc-fae468fa-8e3c-11e9-8372-080027d5c49e   1Gi        RWO            standard       36s

NAME                 TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)             AGE
service/es-cluster   ClusterIP   None         <none>        9200/TCP,9300/TCP   17h
service/kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP             17h

クラスタを構築するまでに少し時間がかかるので待ちましょう

  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/health
1560473021 00:43:41 k8s-cluster green 3 3 0 0 0 0 0 0 - 100.0%
  • kubectl exec es-cluster-0 curl localhost:9200/_cat/nodes
172.17.0.7 26 98 36 1.61 2.13 1.32 mdi * es-cluster-1.es-cluster
172.17.0.6 14 98 41 1.61 2.13 1.32 mdi - es-cluster-0.es-cluster
172.17.0.8 16 98 33 1.61 2.13 1.32 mdi - es-cluster-2.es-cluster

こんな感じで取得できるようになれば OK です

minikube を使っている場合

minikube の場合初期リソースが少ないため以下を実施しないとクラスタ構成時に Pod が起動しませんでした

メモリ増設

  • minikube config set memory 4096
  • minikube delete
  • minikube start

max_map_count 増設

  • minikube ssh
  • sudo sysctl -w vm.max_map_count=262144

failover を確認する

master になっている Pod を 1 台削除してみましょう

  • kubectl delete po es-cluster1

StatefulSet なので Pod が削除されても自動で復活します
再作成中はしばらく 2 台構成のクラスタ状態になります
復活したら再度 health と nodes を確認してみましょう

  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/health
1560473297 00:48:17 k8s-cluster green 3 3 0 0 0 0 0 0 - 100.0%
  • kubectl exec es-cluster-0 curl -- -s localhost:9200/_cat/nodes
172.17.0.6 19 98 32 1.05 1.43 1.20 mdi - es-cluster-0.es-cluster
172.17.0.8 23 98 32 1.05 1.43 1.20 mdi * es-cluster-2.es-cluster
172.17.0.7 14 98 26 1.05 1.43 1.20 mdi - es-cluster-1.es-cluster

failover し master が es-cluster-1 から es-cluster-2 になっているのがわかります
また再作成された es-clsuter-1 も自動的にクラスタに再ジョインいしていることがわかります
リソースの削除は以下の通りです

  • kubectl delete sts es-cluster
  • kubectl delete pvc data-es-cluster-0 data-es-cluster-1 data-es-cluster-2
  • kubectl delete svc es-clsuter

トラブルシューティング

今回の 3 台構成にたどり着くまでに何度もトライアンドエラーしました
今回のポイントは Headless Service を使ってホスト名+サブドメインでクラスタ間のアクセスをする点かなと思います
あとは minikube のリソース周りもはまりポイントかなと思います

これらのミスに気づいた方法としては minikube ssh してログインし直接 docker logs で ElasticSearch コンテナのログを確認していました
StatefulSet は Pod の作成に失敗すると自動でコンテナを再作成します
なのでコンテナが削除される前に docker logs -f f93400366819 でログを眺めるのが一番良いと思います
リソースが足りない場合にエラーは大抵これで解決できます

StatefulSet の場合 restartPolicy を Never に設定できません
なので今回のように restartPolicy=Never にできる Pod リソースだけを使って原因調査するのもありかなと思います

最後に

ElasticSearch7.1 のクラスタ構成を k8s 上に構築してみました
記事内では StatefulSet + Headless Service を使って構築しています
調べると他にも CRD (Custom Resource Definition) を使ったり Deployment を使って構築しているサンプルもありました
個人的に試した感じだとクラスタ間はホスト名を使ってアクセスできるようにしたほうが運用も楽になる感じがするのが自分は StatefulSet + Headless Service のリソースを使って構築した感じです

この辺りのリソースやコントローラのチョイスは経験が大きく影響するので自分にあったものを選択するのが良いかなと思います

参考サイト

2019年6月15日土曜日

docker-compose で ElasticSearch7.1 クラスタを構築してみた

概要

公式のドキュメントに docker-compose で立ち上げる YAML があったので試してみました
公式だとうまく動作しない部分があったので少し変更しています
※公式だとノードを追加した際にうまく動作しません

環境

  • macOS 10.14.5
  • docker 18.09.2
  • docker-compose 1.23.2
  • ElasticSearch 7.1.1

P.S 20190621 docker for mac の場合メモリの割り当て上限を増やすこと

デフォルトは 2.0GiB なので 4.0GiB に変更しましょう
そうすれば公式のデフォルトの YAML ファイルでも動作します

docker-compose

  • vim docker-compose.yml
version: '2.2'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    container_name: es01
    hostname: es01
    environment:
      - cluster.name=docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - esnet
  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    container_name: es02
    hostname: es02
    environment:
      - cluster.name=docker-cluster
      - discovery.seed_hosts=es01,es02
      - cluster.initial_master_nodes=es01,es02
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata02:/usr/share/elasticsearch/data
    networks:
      - esnet

volumes:
  esdata01:
    driver: local
  esdata02:
    driver: local

networks:
  esnet:

2 台構成になっています
ボリュームプラグインは local を使っています
docker on Mac の場合、データ領域は専用の VM になるので注意してください (参考)

ネットワークは esnet という名前の専用ネットワークを作成しています
このネットワーク上では各ノードは services に定義した名前でアクセスできます
あとはクラスタを構成するための環境変数の設定とコンテナの ulimit の設定になります

起動したらクラスタのステータスを確認しましょう
green になっていれば OK です

  • curl http://127.0.0.1:9200/_cat/health
1560309775 03:22:55 docker-cluster green 2 2 2 1 0 0 0 0 - 100.0%
  • curl localhost:9200/_cat/nodes
192.168.160.3 31 89 3 1.13 0.71 0.27 mdi - es02
192.168.160.2 32 89 3 1.13 0.71 0.27 mdi * es01

es01 が master になりました

設定ファイルを確認してみる

環境変数に設定したデータは設定ファイルに反映されています
コンテナとして立ち上げた場合設定ファイルは /usr/share/elasticsearch/config/elasticsearch.yml に配置されています

  • docker-compose exec es01 cat /usr/share/elasticsearch/config/elasticsearch.yml
cluster.name: "docker-cluster"
network.host: 0.0.0.0

中身はほとんど書かれていないようです
では設定した環境変数はどこに渡されているかというとプログラムを実行する引数に渡されています
わかりやすいようにコマンドを工夫しています

  • for i in $(docker exec es01 ps aux); do echo ${i} | grep '^-E'; done
-Ecluster.initial_master_nodes=es01,es02
-Ecluster.name=docker-cluster
-Ediscovery.seed_hosts=es01,es02

こんな感じで渡されていました
おそらく環境変数は上記のようにすべてプログラムに渡すようになっているので他のパラメータも環境変数で渡すことができると思います

テストデータを入れてみる

テストデータも提供してくれているのでそれを使います

  • curl -O https://download.elastic.co/demos/kibana/gettingstarted/7.x/shakespeare.json

まずはマッピングを作成します

curl -X PUT "localhost:9200/shakespeare" -H 'Content-Type: application/json' -d'
{
  "mappings": {
    "properties": {
    "speaker": {"type": "keyword"},
    "play_name": {"type": "keyword"},
    "line_id": {"type": "integer"},
    "speech_number": {"type": "integer"}
    }
  }
}
'

作成できたらデータを投入します
bulk API というのがあるのでこれを使います
データが大きいので少し時間がかかります

  • curl -s -o /dev/null -H 'Content-Type: application/x-ndjson' -XPOST 'localhost:9200/shakespeare/_bulk?pretty' --data-binary @shakespeare.json

成功したら確認しましょう

  • curl -X GET "localhost:9200/_cat/indices?v"
health status index       uuid                   pri rep docs.count docs.deleted store.size pri.store.size
green  open   shakespeare egTnaoHnS3SQ5fWuOZkgfg   1   1     111396            0     39.1mb         19.5mb
  • curl -X GET "localhost:9200/shakespeare/_count"
{"count":111396,"_shards":{"total":1,"successful":1,"skipped":0,"failed":0}}

一度コンテナを削除し立ち上げ直してもデータが残っていることが確認できると思います

  • docker-compose down
  • docker-compose up -d
  • curl -X GET "localhost:9200/shakespeare/_count"

=> 111396

ノードを追加するには

試しにスケールしてみましたがコンテナ名を指定しているため当然アウトでした

  • docker-compose up --scale es02=2

=> ERROR: for es02 Cannot create container for service es02: Conflict. The container name "/es02" is already in use by container

docker-compose.yml に es03 を追記してクラスタに es03 も追加するように記載してみます

  • docker-compose down

一旦コンテナを削除します
-v は付けずにデータボリュームは残します
そして 3 台目のホスト情報を YAML ファイルに記載します

  • vim docker-compose.yml
version: '2.2'
services:
  es01:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    container_name: es01
    hostname: es01
    environment:
      - cluster.name=docker-cluster
      - discovery.seed_hosts=es01,es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata01:/usr/share/elasticsearch/data
    ports:
      - 9200:9200
    networks:
      - esnet
  es02:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    container_name: es02
    hostname: es02
    environment:
      - cluster.name=docker-cluster
      - discovery.seed_hosts=es01,es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata02:/usr/share/elasticsearch/data
    networks:
      - esnet
  es03:
    image: docker.elastic.co/elasticsearch/elasticsearch:7.1.1
    container_name: es03
    hostname: es03
    environment:
      - cluster.name=docker-cluster
      - discovery.seed_hosts=es01,es02,es03
      - cluster.initial_master_nodes=es01,es02,es03
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ulimits:
      memlock:
        soft: -1
        hard: -1
    volumes:
      - esdata03:/usr/share/elasticsearch/data
    networks:
      - esnet

volumes:
  esdata01:
    driver: local
  esdata02:
    driver: local
  esdata03:
    driver: local

networks:
  esnet:

services に es03 が追加になったのとそれに伴い volumes も追加しています
また各ホストの discovery.seed_hosts と cluster.initial_master_nodes に es03 の情報を追加しています
これで再度コンテナを起動しましょう

  • docker-compose up -d

すると今度は 3 台構成で起動します

  • curl localhost:9200/_cat/health
1560386296 00:38:16 docker-cluster green 3 3 0 0 0 0 0 0 - 100.0%
  • curl localhost:9200/_cat/nodes
192.168.176.3 16 96 88 5.93 2.61 1.03 mdi - es02
192.168.176.4 20 96 88 5.93 2.61 1.03 mdi * es01
192.168.176.2 17 96 88 5.93 2.61 1.03 mdi - es03

master は変わらず es01 でした

failover させてみる

では es01 をダウンさせてみましょう

  • docker-compose stop es01

しばらくしてから es02 に問い合わせると 2 台構成になっているのが確認できると思います

  • docker-compose exec es02 curl localhost:9200/_cat/health
1560386492 00:41:32 docker-cluster green 2 2 0 0 0 0 0 0 - 100.0%
  • docker-compose exec es02 curl localhost:9200/_cat/nodes
192.168.176.2 40 73 3 0.38 1.41 0.85 mdi * es03
192.168.176.3 40 73 3 0.38 1.41 0.85 mdi - es02

master は es01 -> es03 になったようです
これで再度 es01 を起動してみましょう

  • docker-compose start es01

また helth と nodes を確認すると 3 台構成に戻っているのが確認できると思います
また master は変わらず es03 になっていると思います

ポイント解説

過去に Ubuntu 上で ElasticSearch7.1 のクラスタ環境を構築したときとほぼ同じになります
カーネルパラメータ系はイメージ側で対応してくれています
公式の docker-compose.yml で ulimit だけ指定があったのでそのまま使っています
コンテナの hostname を指定し elasticsearch を起動時のオプションで node.name を指定しないようにしました
あと bootstrap.memory_lock=true も不要だったので削除しました

また今回はノードを追加する際に down -> up で追加しましたが down させないで docker-compose.yml を編集して up しても追加できると思います

最後に

docker-compose.yml で ElasticSearch7.1 のクラスタを構築してみました
公式のままだとノード追加がうまくいかなかったので少し修正しています

Kibana や Cerebo と連携するときは同じ docker-compose.yml 内に定義してあげるかホストの 9200 を見に行くようにすれば OK です
ただ今回は es01 しか 9200 を LISTEN していないので es01 がダウンした場合は必ず restart してあげる必要があります
もしくはそれが面倒なら 3 台の ElasticSearch の前に nginx などを立てて 9200 をバランシングしてあげる必要があるかなと思います

参考サイト

2019年6月14日金曜日

Ubuntu16.04 上に ElasticSearch7.1 クラスタを構築してみた

概要

ElasticSearch の 7.1 で 3 台構成のクラスタを組んでみました
Vagrant で 3 台の Ubuntu を構築しそれぞれで ElasticSearch のプロセスを 1 つずつ起動してクラスタを組んでみます
今回は Vagrant を使いましたが EC2 や GCE などのクラウドリソースでも問題ないです

環境

  • Ubuntu 16.04 LTS
  • Vagrant 2.1.1
  • ElasticSearch 7.1.1

Vagrantfile

3 台作成します
メモリは 1024 にしていますが余裕があればもっと高い値を設定してください
box ファイルは ubuntu/xenial64 を使用しています
ホスト間で通信できるようにインタフェースを一つ追加してます

Vagrant.configure("2") do |config|
  config.vm.define "vm01" do |v|
    v.vm.box = "ubuntu/xenial64"
    v.vm.network "private_network", ip: "192.168.99.200"
    v.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
  end
  config.vm.define "vm02" do |v|
    v.vm.box = "ubuntu/xenial64"
    v.vm.network "private_network", ip: "192.168.99.201"
    v.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
  end
  config.vm.define "vm03" do |v|
    v.vm.box = "ubuntu/xenial64"
    v.vm.network "private_network", ip: "192.168.99.202"
    v.vm.provider "virtualbox" do |vb|
      vb.memory = "1024"
    end
  end
end
  • vagrant up

VM 設定

それぞれに ssh して作業します

  • vagrant ssh vm01
  • vagrant ssh vm02
  • vagrant ssh vm03

Vagrant ユーザで作業する上でカーネルパラメータなどのチューニングが必要なので行います

  • sudo vim /etc/security/limits.conf
vagrant hard nproc 4096
vagrant soft nproc 4096
vagrant hard nofile 65535
vagrant soft nofile 65535

各ホストをホスト名でアクセスできるように hosts ファイルに記載します

  • sudo vim /etc/hosts
192.168.99.200 vm01
192.168.99.201 vm02
192.168.99.201 vm02

あとはホスト名を設定しましょう
ここで設定したホスト名は ElasticSearch のクラスタを構成する際に使われるノード名 (node.name) でそのまま使用します

  • sudo hostnamectl set-hostname vm01
  • sudo hostnamectl set-hostname vm02
  • sudo hostnamectl set-hostname vm02

最後に再起動しましょう

  • sudo reboot -h now

elasticsearch インストール

各ホストで行います
tar.gz ファイルをダウンロードして解凍するだけで OK です

  • curl -L -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.1.1-linux-x86_64.tar.gz
  • tar -xvf elasticsearch-7.1.1-linux-x86_64.tar.gz

elasticsearch 起動 (vm01, vm02)

まずは 2 台構成のクラスタを構築します
どちらも同じコマンドを実行するのでそれぞれに ssh して実行しましょう
直前に sysctl を実行していますが「VM 設定」のところで /etc/sysctl.conf に記載しても OK です

  • cd elasticsearch-7.1.1/
  • sudo sysctl -w vm.max_map_count=262144
  • ES_JAVA_OPTS="-Xms512m -Xmx512m" ./bin/elasticsearch -d -p $(hostname).pid -Ecluster.name=my_cluster -Ediscovery.seed_hosts=vm01,vm02 -Ecluster.initial_master_nodes=vm01,vm02 -Enetwork.host=_enp0s9_

いろいろとパラメータを指定しています
-d はデーモンとして実行、-p は pid ファイルを生成します

クラスタを構築する上で大事なのは -E 付きのオプションです
まず cluster.name=my_cluster はクラスタ名を指定します
好きな名前を付けて OK です
他のホストで起動するときも同じクラスタ名を指定することで同一クラスタに所属されることができます
discovery.seed_hosts=vm01,vm02 はクラスタに所属させるホストを指定します
cluster.initial_master_nodes=vm01,vm02 はクラスタ構築時にマスターノードになれるホストを指定します
network.host=_enp0s9_ はホスト間でやり取りするネットワークを指定します
enp0s9 はインタフェースを指定しており 192.168.99 帯の固定 IP が振られたインタフェースを指定しています
また今回は node.name を指定していません
指定しなかった場合は ElasticSearch が自動的にホスト名を引っ張ってノード名に設定するので今回は指定しませんでした

しばらくすると 9200 と 9300 が 192.168.99 の IP で LISTEN します
2 台のホストで LISTEN したらクラスタが構築されているか確認しましょう

クラスタの状態確認

API を叩きて確認します
vm01, vm02 どちらのホストでも問題ないので API をコールしてみましょう

$curl $(hostname):9200/_cat/health
1560334522 10:15:22 my_cluster green 2 2 0 0 0 0 0 0 - 100.0%
$ curl $(hostname):9200/_cat/nodes
192.168.99.200 37 94 14 0.30 0.21 0.08 mdi - vm01
192.168.99.201 25 93 14 0.31 0.26 0.14 mdi * vm02

ノードが 2 台ありかつステータスが green になっていれば問題なくクラスタが構築されています
なお今回は vm02 が初期状態では master に選ばれました

ホスト追加 (vm03)

では vm03 も追加してみます
先程の vm01, vm02 と同じように elasticsearch のプロセスを起動すれば OK です

  • cd elasticsearch-7.1.1/
  • sudo sysctl -w vm.max_map_count=262144
  • ES_JAVA_OPTS="-Xms512m -Xmx512m" ./bin/elasticsearch -d -p $(hostname).pid -Ecluster.name=my_cluster -Ediscovery.seed_hosts=vm01,vm02,vm03 -Ecluster.initial_master_nodes=vm01,vm02,vm03 -Enetwork.host=_enp0s9_

discovery.seed_hosts と cluster.initial_master_nodes に vm03 が追加されています
あとは同じです
9200 と 9300 が LISTEN したら同じようにクラスタの状態を API で確認しましょう
3 台になっていることが確認できれば OK です
なおホストを追加しても master は変わりませんでした

$ curl $(hostname):9200/_cat/health
1560337529 11:05:29 my_cluster green 3 3 0 0 0 0 0 0 - 100.0%
$ curl $(hostname):9200/_cat/nodes
192.168.99.202 33 93 0 0.08 0.12 0.05 mdi - vm03
192.168.99.200 19 92 0 0.07 0.03 0.01 mdi - vm01
192.168.99.201 36 93 0 0.00 0.00 0.00 mdi * vm02

failover 確認 (vm02)

最後に failover を確認しましょう
master であった vm02 のプロセスを kill してみます

  • kill -SIGTERM $(cat vm02.pid)

すると別のホストが master に昇格しステータスが green に戻ります
今回の場合は特にデータやアクセスもないので確認が難しいですが failover 中はステータスが red or yellow になり瞬間的にクラスタが使えない状態になるようです

$ curl $(hostname):9200/_cat/health
1560338344 11:19:04 my_cluster green 2 2 0 0 0 0 0 0 - 100.0%
$ curl $(hostname):9200/_cat/nodes
192.168.99.202 26 93 0 0.00 0.00 0.00 mdi - vm03
192.168.99.200 38 92 1 0.00 0.02 0.00 mdi * vm01

再度 vm02 の elasticsearch を起動しましょう
vm02 を再度起動する場合はなぜか discovery.seed_hosts と cluster.initial_master_nodes に vm03 を追加しないでも大丈夫でした (もしかしたら追加したほうがいいかも)

  • ES_JAVA_OPTS="-Xms512m -Xmx512m" ./bin/elasticsearch -d -p $(hostname).pid -Ecluster.name=my_cluster -Ediscovery.seed_hosts=vm01,vm02 -Ecluster.initial_master_nodes=vm01,vm02 -Enetwork.host=_enp0s9_

これで vm02 が再度クラスタに加わりました

$ curl $(hostname):9200/_cat/health
1560338437 11:20:37 my_cluster green 3 3 0 0 0 0 0 0 - 100.0%
$ curl $(hostname):9200/_cat/nodes
192.168.99.200 24 92 0 0.00 0.01 0.00 mdi * vm01
192.168.99.202 38 93 0 0.00 0.00 0.00 mdi - vm03
192.168.99.201 25 94 0 0.23 0.10 0.03 mdi - vm02

ハマったポイント

Vagrant の場合デフォルトのホスト名が ubuntu-xenial になっています
これで node.name を指定してやっていたのですがうまくできなかったので hostname を設定して行いました
また、discovery.seed_hosts と cluster.initial_master_nodes を IP で指定していたのですがうまくいかなかったのでホスト名を指定するようにしました
ホスト名にした場合ホスト間の通信はホスト名で引ける必要があるので /etc/hosts に記載しています

また基本はトライアンドエラーを何度も行ったのですが data/ に前の間違ったクラスタ情報があると何度やっても失敗したので次にトライする場合はフォルダごと削除するようにしましょう

最後に

ElasticSearch 7.1 でクラスタ構成を組んでみました
VM で作業したのでカーネルパラメータなどのチューニングも必要になります
基本は起動する際のパラメータを駆使してクラスタを構成することができますがパラメータではなく config/elasticsearch.yml に記載しても効果は同じなのでパラメータが嫌いな方は設定ファイルに記載しても OK です

また今回、ホストはすべて node.master=true で行いました
ノードの種類もいろいろあるらしいので興味があれば見てください
よくよく調べると今回のやり方は Bootstrap Cluster というやり方のようです
docker でクラスタを構成場合も同じ方法を使っていました

参考サイト

2019年6月13日木曜日

【Hashicorp】Vault を試してみた

概要

Vault (ボールト) はパスワードや認証キーなどのセンシティブなデータを暗号化して管理するためのツールです
登録したパスワードなどは API や CLI を使って取得したり更新したりすることができます
今回は Mac 上でインストールし公式の GettingStarted をやってみました

環境

  • macOS 10.14.5
  • vault 1.1.3

インストール

Mac の場合 Homebrew でインストール可能です
ダウンロードページからバイナリをダウンロードして PATH 上に配置しても OK です

  • brew install vault

サーバ起動

開発モード (-dev) で Vault サーバを起動しましょう

  • vault server -dev

フォアグラウンドで動作するので別ターミナルを開いて操作してみます

クライアント設定

Vault サーバの情報を環境変数に設定します
先程サーバを起動した際にアドレスの情報と Root Token の情報が標準出力に表示されいてるのでそれを使ってください

  • export VAULT_ADDR='http://127.0.0.1:8200'
  • export VAULT_DEV_ROOT_TOKEN_ID='s.ClxVsF2FBJPRZSqE5DT1flZa'

status コマンドを叩いて問題なければサーバにアクセスできています

  • vault status
Key             Value
---             -----
Seal Type       shamir
Initialized     true
Sealed          false
Total Shares    1
Threshold       1
Version         1.1.3
Cluster Name    vault-cluster-8a3a8aeb
Cluster ID      832c632e-de9c-ca66-af9a-610f573f35c8
HA Enabled      false

基本操作

まずは Vault に対して基本操作をしてみます

登録してみる

kv put コマンドを使います
シークレットを保存するパスと key=value 形式でシークレットを指定します

  • vault kv put secret/hello foo=world
Key              Value
---              -----
created_time     2019-06-09T23:28:16.494828Z
deletion_time    n/a
destroyed        false
version          1

シークレットを保存する場合はファイルまたは標準入力から読み込むことを推奨しています
理由としてはコマンドで実行した場合 bash などの履歴に残るため平文のパスワードなどが確認できてしまう恐れがあるためです

  • vault kv put secret/hello foo=world excited=yes
Key              Value
---              -----
created_time     2019-06-09T23:29:17.718256Z
deletion_time    n/a
destroyed        false
version          2

既存のパスに対して再度 put すると上書きされます
また key=value 情報を追加することもできます

取得する

kv get を使います
引数にパスを指定すれば取得できます

  • vault kv get secret/hello
====== Metadata ======
Key              Value
---              -----
created_time     2019-06-09T23:29:17.718256Z
deletion_time    n/a
destroyed        false
version          2

===== Data =====
Key        Value
---        -----
excited    yes
foo        world

-format=json を指定すれば JSON 形式で取得できます

  • vault kv get -format=json secret/hello

削除する

kv delete コマンドを使います

  • vault kv delete secret/hello

Success! Data deleted (if it existed) at: secret/hello

これで登録した key=value のシークレットデータは削除されますが secret/hello のパスは残っています

シークレットエンジンとパス

Vault はシークレットエンジンと呼ばれる key=value を保存する機能を持っています
シークレットエンジンには様々な種類がありどのシークレットエンジンにアクセスするかをパスで判断しています
例えばこれまで使ってきたシークレットエンジンは「kv」で kv シークレットエンジンにアクセスするには secret/ にアクセスする必要があります
公式ではこのような機能は VFS (Virtual File System) に似ていると説明しています

Vault は AWS の IAM をシークレットエンジンにすることもできます
IAM をシークレットエンジンは aws という名前で定義されています

シークレットエンジンとパスはデフォルトで決まっていますが自分で定義することもできます
例えば kv シークレットエンジンを secret/ ではなく kv/ でアクセスできるようにするには secrets enable コマンドを使います

  • vault secrets enable -path=kv kv

=> Success! Enabled the kv secrets engine at: kv/

これで kv/ にアクセスしても kv エンジンを使えます
ただしシークレットエンジンは同じであっても secret/ に保存したシークレットを kv/ からアクセスできるわけではないので注意してください
あくまでも別のパスを追加するだけです

シークレットエンジンとパスの組み合わせは secrets list コマンドを使います

  • vault secrets list
Path          Type         Accessor              Description
----          ----         --------              -----------
cubbyhole/    cubbyhole    cubbyhole_03e682aa    per-token private secret storage
identity/     identity     identity_364f85b0     identity store
kv/           kv           kv_f77a9201           n/a
secret/       kv           kv_0f47c39e           key/value secret storage
sys/          system       system_dca27662       system endpoints used for control, policy and debugging

新しいパスを使ってデータを登録してみましょう

$ vault write kv/my-secret value="s3c(eT"
Success! Data written to: kv/my-secret
$ vault write kv/hello target=world
Success! Data written to: kv/hello
$ vault write kv/airplane type=boeing class=787
Success! Data written to: kv/airplane
$ vault list kv
Keys
----
airplane
hello
my-secret

こんな感じでアクセスできます
冒頭で登録した secret/hello は表示されていないことも確認できます

有効にしたパスは当然無効にすることもできます

  • vault secrets disable kv/

=> Success! Disabled the secrets engine (if it existed) at: kv/

put や write, get ができなくなると思います
また secrets list にも表示されなくなります
一度 disable にしたパスを再度 enable にしても前のシークレットデータは戻らないので注意してください

ダイナミックシークレット

簡単に言えばアクセスしたときに初めてシークレットエンジンが生成される仕組みです
ダイナミックシークレットは使わない時にはアクセス不可にできるので不正アクセスの防止を行うことができます
ダイナミックシークレットを試すには aws シークレットエンジンを使います
AWS のアカウントが必要になるので事前に準備しておきましょう (ちなみにお金のかかるリソースは使わないので安心してください)

まずは aws シークレットエンジンを有効にします

  • vault secrets enable -path=aws aws

次に AWS で作成したアクセスキーとシークレットキーを登録します

  • vault write aws/config/root access_key=xxxxxxxxxxxxxxxxxxx secret_key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx region=us-east-2

Vault から IAM にユーザを作成した際のデフォルトポリシーを登録します
今回はサンプルなのでフルアクセスのポリシーになっていますがここは適宜用途に合わせて変更してください
credential_type には他に federation_token と assumed_role が指定できますが今回は使いません

vault write aws/roles/my-role \
credential_type=iam_user \
policy_document=-<<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Stmt1426528957000",
      "Effect": "Allow",
      "Action": [
        "ec2:*"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
EOF

作成したポリシーは read で確認できます

  • vault read aws/roles/my-role

ではこれでシークレットを登録してみます
read すると同時に AWS の IAM 側にユーザが作成されると思います

  • vault read aws/creds/my-role
Key                Value
---                -----
lease_id           aws/creds/my-role/LDOIOt4aBN2f1SiGUdtIl5Qj
lease_duration     768h
lease_renewable    true
access_key         AKIAQ4627A5STJ445PF7
secret_key         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
security_token     <nil>

コンソールで確認するとユーザが存在するのが確認できると思います

ロールも Vault に登録されたものが設定されています

ダイナミックシークレットを削除するには lease revoke コマンドを使います

  • vault lease revoke aws/creds/my-role/LDOIOt4aBN2f1SiGUdtIl5Qj

これで IAM からもユーザが削除されます
削除時に指定している ID は lease_id で呼ばれ Vault 内部で管理されています

パスヘルプ

aws シークレットエンジンに触れましたが他にもアクセスできるパスはあります
確認するには path-help というコマンドがあるのでこれを使うとどこにアクセスすればいいかわかります

  • vault path-help aws

認証

Vault にログインするためのセッショントークンを発行することができます
これまで Vault にアクセスできたのは vault server -dev でサーバを起動した際にトークンが発行されておりそれを使っているためです
機能確認のため別のトークンを発行してみます

  • vault token create
Key                  Value
---                  -----
token                s.M7sqaUReQRKX8fM3P0l1HceU
token_accessor       2WcqgKcNieednDvsqIhOnXOe
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

これで別のトークンが発行されました
テストなのでポリシーは「root」を使っています
このトークンを使ってログインしてみます

  • vault login s.M7sqaUReQRKX8fM3P0l1HceU
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.

Key                  Value
---                  -----
token                s.M7sqaUReQRKX8fM3P0l1HceU
token_accessor       2WcqgKcNieednDvsqIhOnXOe
token_duration       ∞
token_renewable      false
token_policies       ["root"]
identity_policies    []
policies             ["root"]

次回からのアクセスは自動的にこのトークンが使われるようになります
ではこのトークンは無効にしてみます

  • vault token revoke s.M7sqaUReQRKX8fM3P0l1HceU

=> Success! Revoked token (if it existed)

するとこれまでアクセスできたコマンドがアクセスできなくなっていると思います

  • vault token create
Error creating token: Error making API request.

URL: POST http://127.0.0.1:8200/v1/auth/token/create
Code: 403. Errors:

* permission denied

こんな感じでトークンを発行し認証をコントロールすることができます
次も作業するので環境変数 (VAULT_DEV_ROOT_TOKEN_ID) に設定したトークンを使ってログインし直しましょう

  • vault login $VAULT_DEV_ROOT_TOKEN_ID
  • vault token lookup

Vault はトークンを発行するのにサードパーティの認証を使うこともできます
例えば Github と連携し Github のアクセストークンを使うことで Vault 用のトークンを発行することもできます
※少し調べた感じだと Github Authentication は Organization への所属が必要っぽかったので試せませんでした

ポリシー

Vault でトークンを発行した際にそのトークンに割り当てるポリシーを設定することができます
ポリシーはアクセス可能なパス (シークレットエンジン) を HCL と呼ばれるフォーマットで定義します
例えば「このトークンは secret/ にはアクセスできるけど aws/ にはアクセスできない」というポリシーが作れます

ポリシーは HCL というフォーマットで記載します
簡単なポリシーを作成してみましょう

  • vim default2.hcl
path "secret/data/*" {
  capabilities = ["create", "update"]
}
path "secret/data/foo" {
  capabilities = ["read"]
}

書き換えたら新規でポリシーを登録します

  • vault policy write default2 default2.hcl

=> Success! Uploaded policy: default2

念の為確認しましょう

  • vault policy list
default
default2
root

トークンを新規で発行して動作確認してみます

  • vault token create -policy=default2
Key                  Value
---                  -----
token                s.gSmVv0dNHduln7CSmrAdKxeK
token_accessor       mXam7tDGjChxxdROT8PuP01F
token_duration       768h
token_renewable      true
token_policies       ["default" "default2"]
identity_policies    []
policies             ["default" "default2"]
  • vault login s.gSmVv0dNHduln7CSmrAdKxeK

これで使用するトークンが default2 のポリシーが割り当てたものに変わりました
ポリシー通りに動作するか確認します

  • vault kv put secret/bar key=value
Key              Value
---              -----
created_time     2019-06-10T05:36:51.687562Z
deletion_time    n/a
destroyed        false
version          1
  • vault kv get secret/bar
Error reading secret/data/bar: Error making API request.

URL: GET http://127.0.0.1:8200/v1/secret/data/bar
Code: 403. Errors:

* 1 error occurred:
        * permission denied

secret/bar に登録はできますが読み込みができないのがわかります
secret/foo は読み込みだけできます

HTTP API

これまでは CLI でシークレットエンジンにアクセスしましたが HTTP API を直接コールしてアクセスすることもできます
またアクセスする API は v2 になるので注意してください

登録

  • curl -XPOST -H "X-Vault-Token: $VAULT_DEV_ROOT_TOKEN_ID" -d '{"data":{"excited":"yes","foo":"world"}}' http://127.0.0.1:8200/v1/secret/data/hello

取得

  • curl -XGET -H "X-Vault-Token: $VAULT_DEV_ROOT_TOKEN_ID" http://127.0.0.1:8200/v1/secret/data/hello

一覧

  • curl -XLIST -H "X-Vault-Token: $VAULT_DEV_ROOT_TOKEN_ID" http://127.0.0.1:8200/v1/secret/metadata

削除

  • curl -XDELETE -H "X-Vault-Token: $VAULT_DEV_ROOT_TOKEN_ID" http://127.0.0.1:8200/v1/secret/data/hello

おまけ: 管理画面

http://127.0.0.1:8200/ui にアクセスすると Vault の管理画面が使えます
ただ Homebrew からインストールしたバイナリだと UI なし版になっているので公式からダウンロードしたバイナリと入れ替える必要があります

  • cd /usr/local/Cellar/vault/1.1.3/bin/
  • wget 'https://releases.hashicorp.com/vault/1.1.3/vault_1.1.3_darwin_amd64.zip'
  • unzip vault_1.1.3_darwin_amd64.zip

これで再度 vault を起動しましょう

  • vault server -dev

そして http://127.0.0.1:8200/ui/ にアクセスしてみましょう
トークンでログインしましょう
トークンは VAULT_DEV_ROOT_TOKEN_ID に設定したトークンで OK です
vault を再起動している場合は新しいトークンになっているので標準出力を確認してください
わからない場合は vault token lookup で確認できます

ログインするとシークレットエンジンの一覧が表示されます
ここからシークレットエンジンの有効化や認証の追加もできます
コマンドや API でオペレーションせずに UI でいろいろと操作できます

最後に

Vault を Mac 上にインストールして試してみました
シークレット情報を登録し CLI や API で簡単に取得することができます
またシークレットを登録するバックエンドには AWS や Google などを指定することも可能だということがわかりました

GettingStarted では今回紹介した内容以外に「Github を使った認証」「プロダクション向けのデプロイ方法」があります
今回は長くなりそうだったので飛ばしているので興味があればやってみてください

参考サイト

2019年6月12日水曜日

k8s の CronJob を試してみた

概要

CronJob は k8s の Pod を指定の時間に起動することができるコントローラです
定期的に実行する Pod を作りたい場合に便利です
時間の指定は crond と同じフォーマットが使えます

環境

  • macOS 10.14.5
  • minikube 1.10.0

CronJob 作成

  • vim echo_cron_job.yaml
apiVersion: batch/v1beta1
kind: CronJob
metadata:
  name: hello
spec:
  schedule: "*/1 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: hello
            image: busybox
            args:
            - /bin/sh
            - -c
            - date; echo "Hello, World!"
          restartPolicy: OnFailure

schedule で起動するスケジュールを crond 形式で指定します
restartPolicy で OnFailure を指定していますが特に必須ではありません

  • kubectl apply -f echo_cron_job.yaml

CronJob の一覧確認

  • kubectl get cj
NAME    SCHEDULE      SUSPEND   ACTIVE   LAST SCHEDULE   AGE
hello   */1 * * * *   False     0        42s             111s

CronJob の詳細確認

  • kubectl describe cj hello
Name:                       hello
Namespace:                  default
Labels:                     <none>
Annotations:                kubectl.kubernetes.io/last-applied-configuration:
                              {"apiVersion":"batch/v1beta1","kind":"CronJob","metadata":{"annotations":{},"name":"hello","namespace":"default"},"spec":{"jobTemplate":{"...
Schedule:                   */1 * * * *
Concurrency Policy:         Allow
Suspend:                    False
Starting Deadline Seconds:  <unset>
Selector:                   <unset>
Parallelism:                <unset>
Completions:                <unset>
Pod Template:
  Labels:  <none>
  Containers:
   hello:
    Image:      busybox
    Port:       <none>
    Host Port:  <none>
    Args:
      /bin/sh
      -c
      date; echo "Hello, World!"
    Environment:     <none>
    Mounts:          <none>
  Volumes:           <none>
Last Schedule Time:  Fri, 07 Jun 2019 15:45:00 +0900
Active Jobs:         <none>
Events:
  Type    Reason            Age    From                Message
  ----    ------            ----   ----                -------
  Normal  SuccessfulCreate  2m23s  cronjob-controller  Created job hello-1559889780
  Normal  SawCompletedJob   2m13s  cronjob-controller  Saw completed job: hello-1559889780
  Normal  SuccessfulCreate  83s    cronjob-controller  Created job hello-1559889840
  Normal  SawCompletedJob   73s    cronjob-controller  Saw completed job: hello-1559889840
  Normal  SuccessfulCreate  23s    cronjob-controller  Created job hello-1559889900
  Normal  SawCompletedJob   12s    cronjob-controller  Saw completed job: hello-1559889900

CronJob は指定の時間になると Job を生成して実行します

Job 確認

  • kubectl get job
NAME               COMPLETIONS   DURATION   AGE
hello-1559889840   1/1           5s         2m18s
hello-1559889900   1/1           7s         78s
hello-1559889960   1/1           6s         17s

ジョブは最新のジョブから 3 つまで保存されます
この値は successfulJobsHistoryLimit で変更できます

Job の詳細確認

  • describe job hello-1559889900
Name:           hello-1559889900
Namespace:      default
Selector:       controller-uid=c841b114-88ef-11e9-b7f1-0800273a058b
Labels:         controller-uid=c841b114-88ef-11e9-b7f1-0800273a058b
                job-name=hello-1559889900
Annotations:    <none>
Controlled By:  CronJob/hello
Parallelism:    1
Completions:    1
Start Time:     Fri, 07 Jun 2019 15:45:04 +0900
Completed At:   Fri, 07 Jun 2019 15:45:11 +0900
Duration:       7s
Pods Statuses:  0 Running / 1 Succeeded / 0 Failed
Pod Template:
  Labels:  controller-uid=c841b114-88ef-11e9-b7f1-0800273a058b
           job-name=hello-1559889900
  Containers:
   hello:
    Image:      busybox
    Port:       <none>
    Host Port:  <none>
    Args:
      /bin/sh
      -c
      date; echo "Hello, World!"
    Environment:  <none>
    Mounts:       <none>
  Volumes:        <none>
Events:
  Type    Reason            Age    From            Message
  ----    ------            ----   ----            -------
  Normal  SuccessfulCreate  2m45s  job-controller  Created pod: hello-1559889900-grgjl

Job は Pod を起動します
最終的なコマンドの結果は Pod のログを確認します

Pod でログの確認

  • kubectl logs hello-1559889900-grgjl

Job もそうだが Pod も最新 3 世代分しか残りません
削除された Pod のログは確認できないのでご注意ください
今回の場合のように 1 分ごとに実行される Job ですべてのログを残したい場合は DaemonSet + fluentd を使って外部にログを飛ばすようにしましょう

お掃除

  • kubectl delete cj hello

最後に

k8s の CronJob コントローラを使ってみました
CronJob -> Job -> Pod という流れでコンテナが作成されるのがポイントです
定義の仕方は crond と同じなので直感的に使えるのが Good です

k8s 上で定期バッチを動かす場合などに使いましょう
ロギング戦略は少し考える必要がありそうですが DaemonSet と組み合わせるのが手っ取り早いと思います

参考サイト

2019年6月11日火曜日

Ruby で gRPC 超入門

概要

gRPC はハイパフォーマンスな RPC フレームワークです
Protocol Buffers と呼ばれるインタフェース言語を使って呼び出し元の関数などを定義することができます
今回は Ruby でサンプルのコードを動かしてみました

環境

  • macOS 10.14.5
  • Ruby 2.6.2p47
    • grpc 1.21.0

準備

公式からサンプルが提供されているのでそれを元に動かしてみます

  • git clone https://github.com/grpc/grpc
  • cd grpc/examples/ruby/
  • bundle install --path vendor

'bundler', '~> 1.7' になっているので bundler のバージョンが新しい場合はバージョンの部分を削除してください

  • bundle exec ruby greeter_server.rb

でサーバを起動しておきましょう

  • bundle exec ruby greeter_client.rb

別ターミナルでクライアントを起動して "Greeting: Hello world" が返ってくれば OK です
今回のチュートリアルではこれらのサーバーとクライアントコードを修正します

helloworld.proto ファイルの修正

まず proto ファイルを修正します
proto ファイルには RPC で呼び出されるメソッドの引数と返り値を定義してあります

  • vim ../protos/helloworld.proto
syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
  rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

コメントの部分は削除しています
いろいろと書いていますが 1 行追記しているだけです
追記した箇所は rpc SayHelloAgain の行になります
message を使って引数と返り値のデータを定義することができます
あとはそれらを使った service を定義すれば OK です

grpc-tools のインストール

作成した proto ファイルからコードを生成してくれるツールです
コマンドとして使うのでグローバルにインストールしましょう

  • gem install grpc-tools

コード生成

では proto ファイルからコードを生成します
実行しているパスは examples/ruby になります

  • grpc_tools_ruby_protoc -I ../protos --ruby_out=lib --grpc_out=lib ../protos/helloworld.proto

(Mac で実行して PATH が通っていない場合は /usr/local/lib/ruby/gems/2.6.0/bin/grpc_tools_ruby_protoc あたりを探してみてください)

-I が proto ファイルを検索するディレクトリを指定します
--ruby_out と --grpc_out は生成されるコードを配置するディレクトリを指定します
あとは生成元の proto ファイルのパスを指定すれば OK です

成功すると lib ディレクトリ配下の helloworld_pb.rb と helloworld_services_pb.rb が新たに生成されています

サーバコードを修正する

新たに追加したメソッドをサーバ側で使ってみます

  • vim greeter_server.rb
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

require 'grpc'
require 'helloworld_services_pb'

class GreeterServer < Helloworld::Greeter::Service
  def say_hello(hello_req, _unused_call)
    Helloworld::HelloReply.new(message: "Hello #{hello_req.name}")
  end

  def say_hello_again(hello_req, _unused_call)
    Helloworld::HelloReply.new(message: "Hello again, #{hello_req.name}")
  end
end

def main
  s = GRPC::RpcServer.new
  s.add_http2_port('0.0.0.0:50051', :this_port_is_insecure)
  s.handle(GreeterServer)
  s.run_till_terminated_or_interrupted([1, 'int', 'SIGQUIT'])
end

main

コメントの部分は削除しています
追記したのは def say_hello_again メソッドになります
say_hello_again は proto ファイルで定義した rpc SayHelloAgain をスネークケースに変換した名前にしましょう

クライアントコード修正

追加したメソッドをクライアントコードで呼ぶようにしましょう

  • vim greeter_client.rb
this_dir = File.expand_path(File.dirname(__FILE__))
lib_dir = File.join(this_dir, 'lib')
$LOAD_PATH.unshift(lib_dir) unless $LOAD_PATH.include?(lib_dir)

require 'grpc'
require 'helloworld_services_pb'

def main
  stub = Helloworld::Greeter::Stub.new('localhost:50051', :this_channel_is_insecure)
  user = ARGV.size > 0 ?  ARGV[0] : 'world'
  message = stub.say_hello(Helloworld::HelloRequest.new(name: user)).message
  p "Greeting: #{message}"
  message = stub.say_hello_again(Helloworld::HelloRequest.new(name: user)).message
  p "Greeting: #{message}"
end

main

コメントは削除しています
main 内の最後の 2 行を追加しています
要するに追加した say_hello_again メソッドをコールしているだけです

動作確認

  • bundle exec ruby greeter_server.rb

でサーバを起動しましょう

  • bundle exec ruby greeter_client.rb

でクライアントを実行し "Greeting: Hello again, world" も新たにレスポンスに追加されれば OK です

grpcurl

クライアント側は grpcurl というコマンドでも確認することができます

  • brew install grpcurl
  • grpcurl -plaintext -d '{"name":"world"}' -proto ../protos/helloworld.proto -import-path ../protos/ localhost:50051 helloworld.Greeter/SayHello

=> { "message": "Hello world" }

こんな感じです
また定義されているサービスの一覧なども確認できます

  • grpcurl -plaintext -proto ../protos/helloworld.proto -import-path ../protos/ localhost:50051 list

=> helloworld.Greeter

  • grpcurl -plaintext -proto ../protos/helloworld.proto -import-path ../protos/ localhost:50051 describe helloworld.Greeter

でコール可能なメソッド定義の一覧を取得できます

最後に

Ruby で gRPC に入門してみました
今回は公式のサンプルとクイックスタートを元に動かしてみただけなので詳しい機能などには触れていません
流れとして

  1. proto ファイルにメソッドを定義
  2. grpc_tools で Ruby のコードを生成
  3. server ファイルでメソッドを実装

しているということが理解できれば OK かなと思います
基本的には生成されたコードには触れずサーバとクライアントコードを変更していく感じなります

参考サイト

2019年6月10日月曜日

Ruby (Sinatra) で GraphQL 入門

概要

前回 Sinatra + ActiveRecord を使った Web アプリを作成してみました
ついでなのでアプリを GraphQL 化してみました
GraphQL は REST や RPC に変わる API で JSON のようなクエリを使ってデータの CRUD が行えます
1 つのリクエストで複数のデータを取得するバッチリクエストが送れます
また RPC のように一つのパス (POST /graphql) に対してアクセスします

環境

  • macOS 10.14.5
  • sqlite3 3.24.0
  • Ruby 2.6.2p47
    • activerecord 5.2.3
    • sqlite3 1.4.1
    • 1.9.6

事前準備

必要なアプリなどは前回のものをそのまま使います
gem のインストールや app.rb の作成は済ませておいてください

graphql-ruby の追加

Ruby で GraphQL を実装するためのライブラリがあるので使わせていただきます

  • vim Gemfile
gem "graphql"
gem "rack-contrib"
  • bundle install --path vendor

上記 2 つを追記したらインストールしましょう

タイプの追加

まずはタイプを追加します
GraphQL の重要な要素として「タイプ」「クエリ」「スキーマ」があります
タイプはデータの構造を定義します

  • mkdir -p graphql/types
  • vim graphql/types/base_object.rb
require 'graphql'

module Types
  class BaseObject < GraphQL::Schema::Object
  end
end
  • vim graphql/types/user.rb
require './graphql/types/base_object.rb'

class Types::User < Types::BaseObject
  description 'Resembles a User Object Type'

  field :id, ID, null: false
  field :name, String, null: false
end

BaseObject を使って新しいタイプを追加します
今回は name と id という field を持つ User タイプを追加しました
前回 sqlite3 に同じようなテーブルを作成しましたが基本的にはそれに合わせましょう

クエリの追加

クエリはリクエストに含まれるフィールドの情報を定義します
リクエストに users というフィールドが含まれている場合に sqlite3 からデータを取得して先程のタイプにバインドしてデータを返却します

  • vim graphql/query.rb
require 'graphql'
require './graphql/types/user.rb'
require './models/user.rb'

class QueryType < GraphQL::Schema::Object
  description "The query root of this schema"

  field :users, [Types::User], null: false do
    description 'Get all users'
  end

  def users
    User.all
  end
end

スキーマの追加

スキーマでは定義したクエリを登録します
今回は 1 つしかクエリを定義してないのでそれだけ登録します

  • vim graphql/schema.rb
require 'graphql'
require './graphql/query.rb'

class TestDBSchema < GraphQL::Schema
  query QueryType
end

アプリ修正

ではアプリを修正しましょう
アプリではスキーマを使います

  • vim app.rb
require 'sinatra'
require 'rack/contrib'
require 'sinatra/json'
require "sinatra/activerecord"
require './graphql/schema.rb'

class DBTestApp < Sinatra::Base
  register Sinatra::ActiveRecordExtension
  set :database, {:adapter => "sqlite3", :database => "usersdb.sqlite3"}
  use Rack::PostBodyContentTypeParser

  post '/graphql' do
    ret = TestDBSchema.execute(
      params['query'],
      variables: params['variables'],
      context: { current_user: nil },
    )
    json ret
  end
end

スキーマには execute というメソッドが実装されておりこれに query の内容を投げます
それ以外に variables と context が投げれますが今回は特に使っていません

動作確認

アプリを起動して動作確認しましょう

  • bundle exec rackup config.ru

curl でリクエストを投げみます

  • curl -X POST -H 'Content-Type: application/json' -d '{"query":"{users{name}}"}' localhost:9292/graphql

=> {"data":{"users":[{"name":"hawk"},{"name":"snowlog"}]}}

こんな感じで users テーブルからデータが取得できると思います
GraphQL の場合 data というフィールド配下に結果が入ってきます

おまけ: Argument

例えば id:1 のユーザだけ取得したい場合には GraphQL では以下のようにリクエストします

  • curl -X POST -H 'Content-Type: application/json' -d '{"query":"{users(id:1){id name}}"}' localhost:9292/graphql

これに対応するにはクエリを以下のように修正します

  • vim graphql/query.rb
require 'graphql'
require './graphql/types/user.rb'
require './models/user.rb'

class QueryType < GraphQL::Schema::Object
  description "The query root of this schema"

  field :users, [Types::User], null: false do
    argument :id, Integer, required: false
    description 'Get all users'
  end

  def users(**arg)
    if arg.empty?
      User.all
    else
      [User.find(arg[:id])]
    end
  end
end

ポイントは argument を使って指定可能な引数を定義する点です
あとは呼び出されるメソッド (users) に可変長のハッシュ引数を受け取れるようにします
こうすることで引数なしと引数あり両方のクエリに対応できます
あとは sqlite3 に発行するとクエリを all と find に分けます
また返り値は配列である必要があるので find の場合に配列にします

最後に

Sinatra で GraphQL に入門してみました
タイプ、クエリ、スキーマの基本的な使い方は抑えておきましょう
このあとはデータを保存したり更新するための mutation などを勉強すると良いかなと思います

参考サイト