etcdに格納されたデータを見る

kubernetesがリソースを作成するにあたり、大きく二つのステップがある。

  1. apiを経由してetcdにデータが格納される
  2. controllerがapiを経由しetcdのデータを突合し、差分があれば更新(Reconciliation Loop)

f:id:wadason:20220223235045p:plain https://kubernetes.io/ja/docs/concepts/overview/components/

リソースを変更・追加するときはetcdにデータが格納される。 コアコンセプトであるReconciliation Loopでetcdと実際のリソースとの差分を検知しリソースを更新することで実現できる。

この記事では前半のkubectlを実行し、etcdにデータが格納されるまでを確認することにした。 Reconciliation Loopの挙動は別途確認することにする。

今回は以下を確認する。

  • etcdにデータが格納されるまで
  • etcdのデータの中身

前提

  • kubernetes v1.22.3
  • minikube v1.24.0
  • etcdctl 3.3.12

etcdにデータが格納されるまで

kubectl applyで実行する。

まずはマニフェストを準備する。 最近検証で使ってたので、dnsutilsを使っているが特に意味はない。

apiVersion: v1
kind: Pod
metadata:
  name: dnsutils
spec:
  containers:
  - name: dnsutils
    image: k8s.gcr.io/e2e-test-images/jessie-dnsutils:1.3
    command:
      - sleep
      - "3600"
    imagePullPolicy: IfNotPresent
  restartPolicy: Always

podを立ち上げる。

# k apply -f dnsutils.yaml
# k get po

NAME       READY   STATUS    RESTARTS   AGE
dnsutils   1/1     Running   0          8m32s

余談

kubectlは最終的にapiを実行するので、 クラスタに接続できるようにして、直接apiを実行すると同じことが実現できる。

kubectl proxy
Starting to serve on 127.0.0.1:8001

curl -X GET http://127.0.0.1:8001/api/v1/namespaces/default/pods
# イメージだけ伝わればいいので、所々省略してる
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "13267"
  },
  "items": [
    {
      "metadata": {
        "name": "dnsutils",
        "namespace": "default",
        "uid": "c6c5612f-adbe-43b6-8ff6-9dc63381d6b0",
        "resourceVersion": "12346",
        "creationTimestamp": "2022-02-23T05:56:42Z",
        # 省略
      },
      "spec": {
        # 省略
      },
      "status": {
        # 省略
      }
    }
  ]
}%

こんな感じで、RESTでkubectlに備わってる機能を実行できる。 無数に存在するapiの中で開発に必要な機能を抜粋し、kubectlで叩けるようになっていることがわかる。

api-serverの挙動も今回はソースコード追ったりはしない。 が、etcdにデータが格納される流れとしては、以下のようになる。

  • リクエストを受信
  • ServerChainにより前処理を実行
  • マニフェストをパースしてバリデーションを実行(エラーならレスポンスを返却)
  • バリデーションが通ればetcdに保存
  • レスポンスを返却

etcdのデータを見てみる

今回はetcdctlを用いて検証したが、curlなどのhttpリクエストでも同じことが実現できる。

その前にetcdについて

  • etcdとは、キーバリューストアのデータベースである。
  • kubernetesのデータ(状態や構成、メタデータ)を保持している。
  • Raftという合意アルゴリズムを用いて、可用性と一貫性を担保している。

Raftの合意アルゴリズムの特徴としては、可用性を高めるために必ずリーダが選出され、フォロワにログをレプリケートする。 例えばリーダが正常に稼働しなくなった場合は次のリーダが選出される(Leader Electionという)。

難しくてあんまり理解できてないが、 高い可用性と一貫性を担保している仕組みという理解に止めておく。

参考

https://raft.github.io/

kubernetesの場合は、マスターノードの数だけetcdを用意する「stacked etcd」という構成が一般的。 f:id:wadason:20220224103319p:plain

※ etcdをマスターノードではなく外部で構築し運用する場合は「external etcd」という構成になる。 参考

kubernetes.io

minikubeに接続しetcdctlで検証

$ minikube ssh
$ docker run -it --net host gcr.io/etcd-development/etcd /bin/sh
$ etcdctl version

etcdctl version: 3.3.12
API version: 3.3

やり方は下記を参考にした。 - qiita.com

まずは、必要な環境変数を設定(今回はminikubeの場合)。

export ETCDCTL_API=3
export ETCDCTL_CACERT=/var/lib/minikube/certs/etcd/ca.crt
export ETCDCTL_CERT=/var/lib/minikube/certs/apiserver-etcd-client.crt
export ETCDCTL_KEY=/var/lib/minikube/certs/apiserver-etcd-client.key

試しにetcdクラスタの一覧を取得。 今回はマスターノードが1台のため一つだけ表示されている。

/ # etcdctl member list
aec36adc501070cc, started, minikube, https://192.168.49.2:2380, https://192.168.49.2:2379

keyvalueストアなので、keyを全て出力してみる。

etcdctl get --keys-only  --prefix ""

長すぎるので省略するが、例えばnamespaceの場合。

/registry/namespaces/default
/registry/namespaces/kube-node-lease
/registry/namespaces/kube-public
/registry/namespaces/kube-system

kubectlで試しに取得すると同じ結果が出力されている。

k get namespaces
NAME              STATUS   AGE
default           Active   9h
kube-node-lease   Active   9h
kube-public       Active   9h
kube-system       Active   9h

適当にkeyを指定して、valueを参照する。

# etcdctl get "/registry/namespaces/default" -w=json
{
   "header":{
      "cluster_id":18038207397139142846,
      "member_id":12593026477526642892,
      "revision":15809,
      "raft_term":3
   },
   "kvs":[
      {
         "key":"Zm9v",
         "create_revision":203,
         "mod_revision":203,
         "version":1,
         "value":"Zm9v"
      }
   ],
   "count":1
}

さっき作った、podはどんな感じかというと、

etcdctl get --keys-only  --prefix "" | grep dnsutils
/registry/events/default/dnsutils.16d654aa24560992
/registry/events/default/dnsutils.16d654aa27a5029a
/registry/events/default/dnsutils.16d654aa2e24e48b
/registry/events/default/dnsutils.16d66d970a929b22
/registry/events/default/dnsutils.16d66d9748aec6fc
/registry/events/default/dnsutils.16d66d974aa74a95
/registry/events/default/dnsutils.16d66d97548d2181
/registry/pods/default/dnsutils

eventと、podのリソースが存在する。 適当に見てみると、

etcdctl get "/registry/events/default/dnsutils.16d654aa24560992" -w=json
{
   "header":{
      "cluster_id":18038207397139142846,
      "member_id":12593026477526642892,
      "revision":15809,
      "raft_term":3
   },
   "kvs":[
      {
         "key":"Zm9v",
         "create_revision":203,
         "mod_revision":203,
         "version":1,
         "value":"Zm9v"
      }
   ],
   "count":1
}

etcdctl get "/registry/pods/default/dnsutils" -w=json
{
   "header":{
      "cluster_id":18038207397139142846,
      "member_id":12593026477526642892,
      "revision":15972,
      "raft_term":3
   },
   "kvs":[
      {
         "key":"Zm9v",
         "create_revision":4742,
         "mod_revision":15215,
         "version":14,
         "value":"Zm9v"
      }
   ],
   "count":1
}

ちゃんとデータが格納されとる。

試しに、新しいkeyを作成してデータを入れてみる。

/ # etcdctl put /test/hoge "hoge"
OK
/ # etcdctl get /test/hoge
/test/hoge

さらに、バージョン管理も確認してみる。

/ # etcdctl put /test/piyo piyo
OK
/ # etcdctl put /test/piyo2 piyo2
OK
/ # etcdctl put /test/piyo3 piyo3
OK
/ # etcdctl get /test/piyo --prefix
/test/piyo
piyo
/test/piyo2
piyo2
/test/piyo3
piyo3
/ # etcdctl get /test/piyo --prefix --sort-by=CREATE
/test/piyo
piyo
/test/piyo2
piyo2
/test/piyo3
piyo3

etcdctl get /test/piyo --rev=0
/test/piyo
piyo

まとめ

etcdの中身を確認すると、kubectlやapiがなにをしているのかイメージがつきやすい。 keyはetcdctl putからもわかるように自由に作成できる。

データ格納後は、各種コントローラーが、apiを経由してetcdの変更差分を検知し、schedulerにより実際の変更を行う流れになる。 この拡張性が、カスタムコントローラを開発しやすくすることにつながってる、と現時点では感じた。

etcdが更新される場合に、コントローラーがどのように差分を検知し、処理を行うのかも気になるので今後検証してみる。

参考