kubernetesのetcd上のデータを読み解く

# はじめに
チーム内新規加入メンバに対してkuberntesの概要を教育している際、
「etcdの中にはreconcile loopに必要な理想状態や現在の状態が保存されてます」
と概念的な説明をしたところ、
「保存って、具体的にどこに、どういう形で保存されているんですか？」
と問われ、ものすごくゴニョゴニョとしか説明できなかったので、etcdctlを叩きながら少しだけ読み解いてみました。

## 環境

- windows10（1909）WSL2上で検証
    - ubuntu : 20.04.1 LTS
    - kind   : 0.9.0 

```bash
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"20", GitVersion:"v1.20.2", GitCommit:"faecb196815e248d3ecfb03c680a4507229c2a56", GitTreeState:"clean", BuildDate:"2021-01-13T13:28:09Z", GoVersion:"go1.15.5", Compiler:"gc", Platform:"linux/amd64"}
Server Version: version.Info{Major:"1", Minor:"19", GitVersion:"v1.19.1", GitCommit:"206bcadf021e76c27513500ca24182692aabd17e", GitTreeState:"clean", BuildDate:"2020-09-14T07:30:52Z", GoVersion:"go1.15", Compiler:"gc", Platform:"linux/amd64"} 
```

## 実施手順

手っ取り早く[kind](https://kind.sigs.k8s.io/)で確認をしていきます。

1. etcdctlコマンドでetcdの中身を取得する
1. 取得した中身を解釈する

の2stepで見ていきます。

## etcdctlコマンドでetcdの中身を取得する

このステップについては、ほぼ先駆者にて確立していただいているとおりです。
が、実行したコマンドは残しておきます。

https://qiita.com/iaoiui/items/6f851a1244ea5546517c

### kindクラスタの構築
etcdの中身が見たいだけですので、control-planeのみの1node構成で立ち上げました。

```bash
$ kind create cluster --name kind-x 
(構築の各種ログ)
$ kubectl get node
NAME                   STATUS   ROLES    AGE   VERSION
kind-x-control-plane   Ready    master   1h   v1.19.1
```
### etcdのpodでetcdctlコマンドの実行準備を行う
etcdctlコマンドを実行するための環境（etcdctlコマンドや証明書類）が整っているので、etcdctlコマンドの実行は`kubectl exec`を用いてetcdのpod内で実行していきます。

```bash
$ kubectl get pod -n kube-system |grep etcd
etcd-kind-x-control-plane                      1/1     Running   0          1h

$ kubectl exec -it -n kube-system etcd-kind-x-control-plane -- sh

# 以下、etcdのpodで作業
sh-5.0# etcdctl version
  etcdctl version: 3.4.13
  API version: 3.4

sh-5.0# ETCDCTL_API=3 etcdctl get "" --prefix --keys-only
{"level":"warn","ts":"2021-12-07T09:01:50.812Z","caller":"clientv3/retry_interceptor.go:62","msg":"retrying of unary invoker failed","target":"endpoint://client-d064f320-1d76-4b4d-8469-66ae70ce645b/127.0.0.1:2379","attempt":0,"error":"rpc error: code = DeadlineExceeded desc = latest balancer error: all SubConns are in TransientFailure, latest connection error: connection closed"}
Error: context deadline exceeded
```

上記のコマンドはkubectl execでshを開いたあと、`etcdctl version`でコマンドが使えることは確認できていますが、`etcdctl get`のコマンドに対してはerrorが発生し、etcdに接続できていません。環境変数で実行に必要な設定をしていきます。
通信に必要な証明書類は特に設定をイジっていないかぎり`/etc/kubernetes/pki/etcd/`配下にあると思います。
以下のコマンドの実行例ではlsが使えなかったため、echoを使って想定の場所に必要なファイルがあるかどうか確認しています。

```sh:etcdのpod内
sh-5.0# echo /etc/kubernetes/pki/etcd/*
/etc/kubernetes/pki/etcd/ca.crt /etc/kubernetes/pki/etcd/ca.key /etc/kubernetes/pki/etcd/healthcheck-client.crt /etc/kubernetes/pki/etcd/healthcheck-client.key /etc/kubernetes/pki/etcd/peer.crt /etc/kubernetes/pki/etcd/peer.key /etc/kubernetes/pki/etcd/server.crt /etc/kubernetes/pki/etcd/server.key

sh-5.0# export ETCDCTL_API=3
sh-5.0# export ETCDCTL_CACERT=/etc/kubernetes/pki/etcd/ca.crt
sh-5.0# export ETCDCTL_CERT=/etc/kubernetes/pki/etcd/server.crt
sh-5.0# export ETCDCTL_KEY=/etc/kubernetes/pki/etcd/server.key
```
上記設定が終わると、etcdctlで情報が取得できるようになります。
全データを取得すると、データ量が多すぎることが想定されるため、下記コマンドでは
--keys-onlyオプションを付けることでkey名のみ取得しています。
また、「prefixが””」とすることで、実質すべてのkeyを取得し一覧表示しています。

```sh:etcdのpod内
sh-5.0# etcdctl get "" --prefix --keys-only
/registry/apiregistration.k8s.io/apiservices/v1.

/registry/apiregistration.k8s.io/apiservices/v1.admissionregistration.k8s.io

/registry/apiregistration.k8s.io/apiservices/v1.apiextensions.k8s.io

/registry/apiregistration.k8s.io/apiservices/v1.apps
・
・
（以下、いっぱい）
```

ドキュメントを読み込めておらず推測にはなりますが、`/registry/`は固定値で、次の階層がリソースのkindとなり、その次の階層はcluster resource（namespace, clusterroleなど）であればリソースの名称、namespace resourceであれば`/namespace/リソース名称`、となっているようです。

- keyの例
    - cluster resource : `/registry/namespaces/kube-system`
    - namespace resource : `/registry/pods/kube-system/etcd-kind-x-control-plane`

### etcdに保存されているデータを確認する
ようやく本題。
etcdctl getコマンドで保存されているデータを確認します。
せっかくなので、自分でアプリケーションを起動した上で、そのアプリのデプロイによって作られたデータを確認してみます。

まずはkubectl execしていないターミナルから以下のように確認用のアプリを起動します。

```
# 雑にnginxで「sample-app」という名前のpodを起動。namespaceは指定していないのでdefault
$ kubectl run --image=nginx sample-app
pod/sample-app created

$ kubectl get po -w
NAME         READY   STATUS              RESTARTS   AGE
sample-app   0/1     ContainerCreating   0          14s
sample-app   1/1     Running             0          48s
```

runningになったらetcdを確認してみます。(valueが非常に長いので、視認性のために手で改行を入れています。本来改行は入っていません)

```sh:etcdのpod内
sh-5.0# etcdctl get /registry/pods/default/sample-app -w fields
"ClusterID" : 15658409494076684822
"MemberID" : 14045899000511186172
"Revision" : 271764
"RaftTerm" : 2
"Key" : "/registry/pods/default/sample-app"
"CreateRevision" : 256657
"ModRevision" : 256774
"Version" : 4
"Value" : "k8s\x00\n\t\n\x02v1\x12\x03Pod\x12\xec\x0f\n\xd3\b\n
\nsample-app\x12\x00\x1a\adefault\"\x00*$ff868501-872a-432f-93eb-e4a8bd031b902
\x008\x00B\b\b\xa2\uef0d\x06\x10\x00Z\x11\n\x03run\x12\nsample-appz\x00\x8a\x01\xb5\x03\n\v
kubectl-run\x12\x06Update\x1a\x02v1\"\b\b\xa2\uef0d\x06\x10\x002\bFieldsV1:\x85\x03\n
\x82\x03{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:run\":{}}},\"f:spec\":{\"
f:containers\":{\"k:{\\\"name\\\":\\\"sample-app\\\"}\":{\".\":{},\"f:image\":{},\"f:imagePullPolicy\":{},\"f:name\":{},
\"f:resources\":{},\"f:terminationMessagePath\":{},\"f:terminationMessagePolicy\":{}}},\"f:dnsPolicy\":{},\"f:enableServiceLinks\":{},\"f:restartPolicy\":{},\"f:schedulerName\":{},
\"f:securityContext\":{},\"f:terminationGracePeriodSeconds\":{}}}\x8a\x01\xb4\x04\n\akubelet\x12\x06Update\x1a\x02v1\"\b\b\xd2\uef0d\x06\x10\x002\bFieldsV1:\x88\x04\n\x85\x04{
\"f:status\":{\"f:conditions\":{\"k:{\\\"type\\\":\\\"ContainersReady\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}},\"k:{\\\"type\\\":\\\"Initialized\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}},
\"k:{\\\"type\\\":\\\"Ready\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}}},\"f:containerStatuses\":{},\"f:hostIP\":{},\"f:phase\":{},
\"f:podIP\":{},\"f:podIPs\":{\".\":{},\"k:{\\\"ip\\\":\\\"10.244.0.5\\\"}\":{\".\":{},\"f:ip\":{}}},\"f:startTime\":{}}}
\x12\xbd\x03\n1\n\x13default-token-c9zfx\x12\x1a2\x18\n\x13default-token-c9zfx\x18\xa4\x03\x12\x91\x01\n\nsample-app\x12\x05nginx*\x00B\x00JJ\n\x13
default-token-c9zfx\x10\x01\x1a-/var/run/secrets/kubernetes.io/serviceaccount\"\x002\x00j\x14/dev
/termination-logr\x06Always\x80\x01\x00\x88\x01\x00\x90\x01\x00\xa2\x01\x04File\x1a\x06Always \x1e2\fClusterFirstB\adefaultJ\adefaultR\x14kind-x-control-plane
X\x00`\x00h\x00r\x00\x82\x01\x00\x8a\x01\x00\x9a\x01\x11default-scheduler\xb2\x016\n\x1cnode.kubernetes.io/not-ready\x12\x06Exists\x1a\x00\"
\tNoExecute(\xac\x02\xb2\x018\n\x1enode.kubernetes.io/unreachable\x12\x06Exists\x1a\x00\"\tNoExecute(\xac\x02\xc2\x01\x00\xc8\x01\x00\xf0\x01\x01\xfa\x01\x14PreemptLowerPriority\x1a\xd3\x03\n\aRunning\x12#\n
\vInitialized\x12\x04True\x1a\x00\"\b\b\xa3\uef0d\x06\x10\x00*\x002\x00\x12\x1d\n\x05Ready\x12\x04True\x1a\x00\"\b\b\xd2\uef0d\x06\x10\x00*\x002\x00\x12'\n\x0fContainersReady\x12\x04True\x1a\x00\"\b\b\xd2\uef0d\x06
\x10\x00*\x002\x00\x12$\n\fPodScheduled\x12\x04True\x1a\x00\"\b\b\xa3\uef0d\x06\x10\x00*\x002\x00\x1a\x00\"\x00*\n172.21.0.22\n10.244.0.5:\b\b\xa3\uef0d\x06\x10\x00B\xf2\x01\n\nsample-app\x12\f\x12\n\n\b\b\xd2\uef0d
\x06\x10\x00\x1a\x00 \x01(\x002\x1edocker.io/library/nginx:latest:_docker.io/library/nginx@sha256:9522864dd661dcadfd9958f9e0de192a1fdda2c162a35668ab6ac42b465f0603BM
containerd://7bfc300dd7fca71d4338c1a3b42b9cb3461bab9bb40732c32c1ceee1b5a9ef1cH
\x01J\nBestEffortZ\x00b\f\n\n10.244.0.5\x1a\x00\"\x00"
"Lease" : 0
"More" : false
"Count" : 1

```
ClusterID、MemberID、keyなどのetcdで管理している情報はもちろんのこと、podそのものの情報と思われるvalueの中身も`f:containers\":{\"k:{\\\"name\\\":\\\"sample-app\\\"}\"`や、`docker.io/library/nginx:latest`など、人間の目から見てもpodの情報と思しきものが散見されますが、今ひとつ中身が判然としない感じがします。

ここまでは参考にさせていただいたリンクでやっている内容とほぼ同じですが、この中身にもう一歩踏み込んでみたいと思います。

もちろん、[kube-ectd-helper](https://github.com/yamamoto-febc/kube-etcd-helper)などを使えば中身が確認できますが、それを言ってしまうとkubectl getすれば情報が確認できるじゃないか、という気がしてしまいますので、あくまで基本的なコマンドを用いた結果を「目で見て理解できる」状態を目指したいと思います。

## 取得した中身を解釈する
ということで、中身を読んでいきます。
kubernetes、etcd周りでバイナリっぽい、となればprotobufではなかろうか、ということで、公式ドキュメントに情報がないか探してみると、こんな記載を発見しました。

https://kubernetes.io/docs/reference/using-api/api-concepts/#protobuf-encoding

> Kubernetes uses an envelope wrapper to encode Protobuf responses. That wrapper starts with a 4 byte magic number to help identify content in disk or in etcd as Protobuf (as opposed to JSON), and then is followed by a Protobuf encoded wrapper message, which describes the encoding and type of the underlying object and then contains the object.

私の拙い英語力では理解が浅いですが、kubernetesにおいてetcd上では、特定のenvelope wrapperでencodeされていると理解しました。
wrapperであるのでそのままで全て解決とは行かないものの上記リンク先にあるprotoをもとにすれば先程の中身が解読できるのでは、と推測しました。以下にprotoを抜粋して記載します。

以降のコマンドでは、下記protoを`k8s.proto`という名称で作業ディレクトリに保存している前提で進めます。

```proto:k8s.proto
  message Unknown {
    // typeMeta should have the string values for "kind" and "apiVersion" as set on the JSON object
    optional TypeMeta typeMeta = 1;

    // raw will hold the complete serialized object in protobuf. See the protobuf definitions in the client libraries for a given kind.
    optional bytes raw = 2;

    // contentEncoding is encoding used for the raw data. Unspecified means no encoding.
    optional string contentEncoding = 3;

    // contentType is the serialization method used to serialize 'raw'. Unspecified means application/vnd.kubernetes.protobuf and is usually
    // omitted.
    optional string contentType = 4;
  }

  message TypeMeta {
    // apiVersion is the group/version for this type
    optional string apiVersion = 1;
    // kind is the name of the object schema. A protobuf definition should exist for this object.
    optional string kind = 2;
  }

```

ここまでわかりやすさ重視で、podの中から操作する、という形式でやってきましたが、etcd podのshが貧弱過ぎてツライので、ローカルからゴリゴリ加工していきます。etcdctlを実行する際に色々とオプションが必要ですが、すべてのコマンドで記載していると書きにくい＆読みにくいのでここでaliasを設定しておきます。

先程までは人の読みやすさ重視で、etcdctl getコマンドを`-w fieds`オプションで実行しましたが、データを加工しやすいようにjsonで出力してみます。alias設定とaliasで設定したコマンドの実行結果は下記の通りです。

```bash 
$ alias etcdctl-get='kubectl exec -n kube-system etcd-kind-x-control-plane -- sh -c \
"ETCDCTL_API=3 etcdctl \
--cacert=/etc/kubernetes/pki/etcd/ca.crt \
--key=/etc/kubernetes/pki/etcd/server.key \
--cert=/etc/kubernetes/pki/etcd/server.crt get \
/registry/pods/default/sample-app --prefix -w json"'

$ etcdctl-get |jq
{
  "header": {
    "cluster_id": 15658409494076684000,
    "member_id": 14045899000511187000,
    "revision": 281129,
    "raft_term": 2
  },
  "kvs": [
    {
      "key": "L3JlZ2lzdHJ5L3BvZHMvZGVmYXVsdC9zYW1wbGUtYXBw",
      "create_revision": 256657,
      "mod_revision": 256774,
      "version": 4,
      "value": "azhzAAoJCgJ2MRIDUG9kEuwPCtMICgpzYW1wbGUtYXBwEgAaB2RlZmF1bHQiACokZmY4Njg1MDEtODcyYS00MzJmLTkzZWItZTRhOGJkMDMxYjkwMgA4AEIICKLuvI0GEABaEQoDcnVuEgpzYW1wbGUtYXBwegCKAbUDCgtrdWJlY3RsLXJ1bhIGVXBkYXRlGgJ2MSIICKLuvI0GEAAyCEZpZWxkc1YxOoUDCoIDeyJmOm1ldGFkYXRhIjp7ImY6bGFiZWxzIjp7Ii4iOnt9LCJmOnJ1biI6e319fSwiZjpzcGVjIjp7ImY6Y29udGFpbmVycyI6eyJrOntcIm5hbWVcIjpcInNhbXBsZS1hcHBcIn0iOnsiLiI6e30sImY6aW1hZ2UiOnt9LCJmOmltYWdlUHVsbFBvbGljeSI6e30sImY6bmFtZSI6e30sImY6cmVzb3VyY2VzIjp7fSwiZjp0ZXJtaW5hdGlvbk1lc3NhZ2VQYXRoIjp7fSwiZjp0ZXJtaW5hdGlvbk1lc3NhZ2VQb2xpY3kiOnt9fX0sImY6ZG5zUG9saWN5Ijp7fSwiZjplbmFibGVTZXJ2aWNlTGlua3MiOnt9LCJmOnJlc3RhcnRQb2xpY3kiOnt9LCJmOnNjaGVkdWxlck5hbWUiOnt9LCJmOnNlY3VyaXR5Q29udGV4dCI6e30sImY6dGVybWluYXRpb25HcmFjZVBlcmlvZFNlY29uZHMiOnt9fX2KAbQECgdrdWJlbGV0EgZVcGRhdGUaAnYxIggI0u68jQYQADIIRmllbGRzVjE6iAQKhQR7ImY6c3RhdHVzIjp7ImY6Y29uZGl0aW9ucyI6eyJrOntcInR5cGVcIjpcIkNvbnRhaW5lcnNSZWFkeVwifSI6eyIuIjp7fSwiZjpsYXN0UHJvYmVUaW1lIjp7fSwiZjpsYXN0VHJhbnNpdGlvblRpbWUiOnt9LCJmOnN0YXR1cyI6e30sImY6dHlwZSI6e319LCJrOntcInR5cGVcIjpcIkluaXRpYWxpemVkXCJ9Ijp7Ii4iOnt9LCJmOmxhc3RQcm9iZVRpbWUiOnt9LCJmOmxhc3RUcmFuc2l0aW9uVGltZSI6e30sImY6c3RhdHVzIjp7fSwiZjp0eXBlIjp7fX0sIms6e1widHlwZVwiOlwiUmVhZHlcIn0iOnsiLiI6e30sImY6bGFzdFByb2JlVGltZSI6e30sImY6bGFzdFRyYW5zaXRpb25UaW1lIjp7fSwiZjpzdGF0dXMiOnt9LCJmOnR5cGUiOnt9fX0sImY6Y29udGFpbmVyU3RhdHVzZXMiOnt9LCJmOmhvc3RJUCI6e30sImY6cGhhc2UiOnt9LCJmOnBvZElQIjp7fSwiZjpwb2RJUHMiOnsiLiI6e30sIms6e1wiaXBcIjpcIjEwLjI0NC4wLjVcIn0iOnsiLiI6e30sImY6aXAiOnt9fX0sImY6c3RhcnRUaW1lIjp7fX19Er0DCjEKE2RlZmF1bHQtdG9rZW4tYzl6ZngSGjIYChNkZWZhdWx0LXRva2VuLWM5emZ4GKQDEpEBCgpzYW1wbGUtYXBwEgVuZ2lueCoAQgBKSgoTZGVmYXVsdC10b2tlbi1jOXpmeBABGi0vdmFyL3J1bi9zZWNyZXRzL2t1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQiADIAahQvZGV2L3Rlcm1pbmF0aW9uLWxvZ3IGQWx3YXlzgAEAiAEAkAEAogEERmlsZRoGQWx3YXlzIB4yDENsdXN0ZXJGaXJzdEIHZGVmYXVsdEoHZGVmYXVsdFIUa2luZC14LWNvbnRyb2wtcGxhbmVYAGAAaAByAIIBAIoBAJoBEWRlZmF1bHQtc2NoZWR1bGVysgE2Chxub2RlLmt1YmVybmV0ZXMuaW8vbm90LXJlYWR5EgZFeGlzdHMaACIJTm9FeGVjdXRlKKwCsgE4Ch5ub2RlLmt1YmVybmV0ZXMuaW8vdW5yZWFjaGFibGUSBkV4aXN0cxoAIglOb0V4ZWN1dGUorALCAQDIAQDwAQH6ARRQcmVlbXB0TG93ZXJQcmlvcml0eRrTAwoHUnVubmluZxIjCgtJbml0aWFsaXplZBIEVHJ1ZRoAIggIo+68jQYQACoAMgASHQoFUmVhZHkSBFRydWUaACIICNLuvI0GEAAqADIAEicKD0NvbnRhaW5lcnNSZWFkeRIEVHJ1ZRoAIggI0u68jQYQACoAMgASJAoMUG9kU2NoZWR1bGVkEgRUcnVlGgAiCAij7ryNBhAAKgAyABoAIgAqCjE3Mi4yMS4wLjIyCjEwLjI0NC4wLjU6CAij7ryNBhAAQvIBCgpzYW1wbGUtYXBwEgwSCgoICNLuvI0GEAAaACABKAAyHmRvY2tlci5pby9saWJyYXJ5L25naW54OmxhdGVzdDpfZG9ja2VyLmlvL2xpYnJhcnkvbmdpbnhAc2hhMjU2Ojk1MjI4NjRkZDY2MWRjYWRmZDk5NThmOWUwZGUxOTJhMWZkZGEyYzE2MmEzNTY2OGFiNmFjNDJiNDY1ZjA2MDNCTWNvbnRhaW5lcmQ6Ly83YmZjMzAwZGQ3ZmNhNzFkNDMzOGMxYTNiNDJiOWNiMzQ2MWJhYjliYjQwNzMyYzMyYzFjZWVlMWI1YTllZjFjSAFKCkJlc3RFZmZvcnRaAGIMCgoxMC4yNDQuMC41GgAiAA=="
    }
  ],
  "count": 1
}
```

valueがbase64エンコードされていて、バイナリとして扱いやすそうに思えます。
であれば、公式ドキュメントから入手したprotoでdecodeできるのでは、と考えました。

以下コマンドでは、jqでvalue部だけ抜き出してprotocでデコードを試みています。
対象データは本来バイナリで扱いたいデータであるものの、base64でエンコードした「文字列」であるため、jsonの仕様上ダブルクォートがついてきてしまうので、デコード前にsedでダブルクォートを除去しています。

```bash
$ etcdctl-get |jq .kvs[].value |sed "s/\"//g" \
|base64 -d|protoc --decode=Unknown ./k8s.proto
[libprotobuf WARNING google/protobuf/compiler/parser.cc:562] No syntax specified for the proto file: k8s.proto. Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax version. (Defaulted to proto2 syntax.)
Failed to parse input.
```

あれ、、、駄目でしたね。
どこがいけなかったのか、base64デコードしたあとの値を目視で確認してみます。

```bash
$ etcdctl-get |jq .kvs[].value |sed "s/\"//g" |base64 -d|xxd|head -10
00000000: 6b38 7300 0a09 0a02 7631 1203 506f 6412  k8s.....v1..Pod.
00000010: ec0f 0ad3 080a 0a73 616d 706c 652d 6170  .......sample-ap
00000020: 7012 001a 0764 6566 6175 6c74 2200 2a24  p....default".*$
00000030: 6666 3836 3835 3031 2d38 3732 612d 3433  ff868501-872a-43
00000040: 3266 2d39 3365 622d 6534 6138 6264 3033  2f-93eb-e4a8bd03
00000050: 3162 3930 3200 3800 4208 08a2 eebc 8d06  1b902.8.B.......
00000060: 1000 5a11 0a03 7275 6e12 0a73 616d 706c  ..Z...run..sampl
00000070: 652d 6170 707a 008a 01b5 030a 0b6b 7562  e-appz.......kub
00000080: 6563 746c 2d72 756e 1206 5570 6461 7465  ectl-run..Update
00000090: 1a02 7631 2208 08a2 eebc 8d06 1000 3208  ..v1".........2.
```
頭の方の3バイト`6b38 7300(HEX) = k8s.(ASCII)`となっている部分が明らかにprotobufじゃないように見えます。
そういえば、ドキュメントに
> A four byte magic number prefix:
  Bytes 0-3: "k8s\x00" [0x6b, 0x38, 0x73, 0x00]

と書いてありました。最初の４バイトはmagic number prefixであるため、取り除かなきゃいけないようです。
その上で、シリアライズされたデータが、protobufとして正しいか目視で確認してみます。

読み方の詳細はこちらの[素晴らしい記事](https://engineering.mercari.com/blog/entry/20210921-ca19c9f371/)や[公式の仕様](https://developers.google.com/protocol-buffers/docs/encoding)を確認してください。


まず、順に見ていきましょう。以下注釈がない限り16進数表記で表現します。
最初の1バイトは`0a`です。2進数でというと`0000 1010`ですね。msbが0なのでtagとしてはこの1バイトで完結しているようです。
また、`fieldNumber << 3 | wireType`ですので、msbを取り除き、field number部とwireType部を分けて表記すると、`0001 -> fieldNumber=1(typeMeta)`であり、`010 -> wireType=2(Bytes)`であることが確認できます。

そして続く1バイトが、`09`ですが、この値はデータ長を示しています。
つまり、`0a02 7631 1203 506f 64`までの9バイトが`typeMeta`のデータであり、中身はapiVersionとkindが入っていることがわかります。

読み方がわかってきましたので、説明を少し飛ばしながら読み進めます。

続きを読むと、`0a02`とあるので、fieldNumber=1（apiVersion）でBytes型、バイト長が2バイトで有ることがわかります。
その後ろの2バイト`7631`はASCIIで”v1”ですので、"apiVersion":"v1"で有ることが読み解けます。

同様に`1203`はfieldNumber=2（kind）でBytes型、バイト長が3で有ることがわかります。
その続きの3バイトが`506f 64`ですのでASCIIで"Pod”、つまり”kind”:"Pod"ですね！

どうやら予想どおりドキュメントから入手したprotoファイルからデコードできそうです。

four byte magic number prefixを除去したらデコードできるはずです！
ddコマンドで先頭4バイトを削除した上で先程と同等のコマンドを実行します。

```bash
$ etcdctl-get |jq .kvs[].value |sed "s/\"//g" \
|base64 -d|dd bs=1 skip=4|protoc --decode=Unknown ./k8s.proto

[libprotobuf WARNING google/protobuf/compiler/parser.cc:562] No syntax specified for the proto file: k8s.proto. Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax version. (Defaulted to proto2 syntax.)
2046+0 records in
2046+0 records out
2046 bytes (2.0 kB, 2.0 KiB) copied, 0.164392 s, 12.4 kB/s
typeMeta {
  apiVersion: "v1"
  kind: "Pod"
}
raw: "\n\323\010\n\nsample-app\022\000\032\007default\"\000*$ff868501-872a-432f-93eb-e4a8bd031b902\0008\000B\010\010\242\356\274\215\006\020\000Z\021\n\003run\022\nsample-appz\000\212\001\265\003\n\013kubectl-run\022\006Update\032\002v1\"\010\010\242\356\274\215\006\020\0002\010FieldsV1:\205\003\n\202\003{\"f:metadata\":{\"f:labels\":{\".\":{},\"f:run\":{}}},\"f:spec\":{\"f:containers\":{\"k:{\\\"name\\\":\\\"sample-app\\\"}\":{\".\":{},\"f:image\":{},\"f:imagePullPolicy\":{},\"f:name\":{},\"f:resources\":{},\"f:terminationMessagePath\":{},\"f:terminationMessagePolicy\":{}}},\"f:dnsPolicy\":{},\"f:enableServiceLinks\":{},\"f:restartPolicy\":{},\"f:schedulerName\":{},\"f:securityContext\":{},\"f:terminationGracePeriodSeconds\":{}}}\212\001\264\004\n\007kubelet\022\006Update\032\002v1\"\010\010\322\356\274\215\006\020\0002\010FieldsV1:\210\004\n\205\004{\"f:status\":{\"f:conditions\":{\"k:{\\\"type\\\":\\\"ContainersReady\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}},\"k:{\\\"type\\\":\\\"Initialized\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}},\"k:{\\\"type\\\":\\\"Ready\\\"}\":{\".\":{},\"f:lastProbeTime\":{},\"f:lastTransitionTime\":{},\"f:status\":{},\"f:type\":{}}},\"f:containerStatuses\":{},\"f:hostIP\":{},\"f:phase\":{},\"f:podIP\":{},\"f:podIPs\":{\".\":{},\"k:{\\\"ip\\\":\\\"10.244.0.5\\\"}\":{\".\":{},\"f:ip\":{}}},\"f:startTime\":{}}}\022\275\003\n1\n\023default-token-c9zfx\022\0322\030\n\023default-token-c9zfx\030\244\003\022\221\001\n\nsample-app\022\005nginx*\000B\000JJ\n\023default-token-c9zfx\020\001\032-/var/run/secrets/kubernetes.io/serviceaccount\"\0002\000j\024/dev/termination-logr\006Always\200\001\000\210\001\000\220\001\000\242\001\004File\032\006Always \0362\014ClusterFirstB\007defaultJ\007defaultR\024kind-x-control-planeX\000`\000h\000r\000\202\001\000\212\001\000\232\001\021default-scheduler\262\0016\n\034node.kubernetes.io/not-ready\022\006Exists\032\000\"\tNoExecute(\254\002\262\0018\n\036node.kubernetes.io/unreachable\022\006Exists\032\000\"\tNoExecute(\254\002\302\001\000\310\001\000\360\001\001\372\001\024PreemptLowerPriority\032\323\003\n\007Running\022#\n\013Initialized\022\004True\032\000\"\010\010\243\356\274\215\006\020\000*\0002\000\022\035\n\005Ready\022\004True\032\000\"\010\010\322\356\274\215\006\020\000*\0002\000\022\'\n\017ContainersReady\022\004True\032\000\"\010\010\322\356\274\215\006\020\000*\0002\000\022$\n\014PodScheduled\022\004True\032\000\"\010\010\243\356\274\215\006\020\000*\0002\000\032\000\"\000*\n172.21.0.22\n10.244.0.5:\010\010\243\356\274\215\006\020\000B\362\001\n\nsample-app\022\014\022\n\n\010\010\322\356\274\215\006\020\000\032\000 \001(\0002\036docker.io/library/nginx:latest:_docker.io/library/nginx@sha256:9522864dd661dcadfd9958f9e0de192a1fdda2c162a35668ab6ac42b465f0603BMcontainerd://7bfc300dd7fca71d4338c1a3b42b9cb3461bab9bb40732c32c1ceee1b5a9ef1cH\001J\nBestEffortZ\000b\014\n\n10.244.0.5"
contentEncoding: ""
contentType: ""
```

デコードできました！
結局rawの中身が次なるprotobufが生のまま、ではありますが、同じ要領で読み進めれば読めそう、という感触を得たところで今回はここまでにしたいと思います。

### 参考情報
https://engineering.mercari.com/blog/entry/20210921-ca19c9f371/
https://kubernetes.io/docs/reference/using-api/api-concepts/#protobuf-encoding
https://gist.github.com/sharjeelaziz/a86b3da887f17ffdbe491146bc64edab