Kubernetes で高度な開発を進めていると、以下のようなシナリオに遭遇することがあります。

  • 特定の Namespace 内にある全てのリソースを表示したい
  • クラスタ内の Pod にどういったラベルが付いているのか知りたい

このような場合に、jqyqreducefrom/to/with_entries が使えます。

基本的な使い方

reduce

reduce を使うと、配列を縮約して一つの値を計算できます。 reduce の最初の引数には入力となる配列を指定します。

$ # jq
$ echo '[1, 2, 3]' | jq 'reduce .[] as $i (0; . + $i)'
6

yq の場合は ireduce を使います。また、キーワードの並びが jq と異なります。

$ # yq
$ echo '[1, 2, 3]' | yq '.[] as $i ireduce(0; . + $i)'
6

from/to/with_entries

to_entries はオブジェクトを keyvalue の配列に分解します。

$ # jq
$ # -c は compact
$ echo '{"a": 1, "b": 2}' | jq -c 'to_entries'
[{"key":"a","value":1},{"key":"b","value":2}]

$ # yq
$ # -P は pretty print
$ echo '{"a": 1, "b": 2}' | yq -P 'to_entries' 
- key: a
  value: 1
- key: b
  value: 2

逆に、from_entrieskeyvalue の配列からオブジェクトを組み立てます。

$ # jq
$ echo '{"a": 1, "b": 2}' | jq -c 'to_entries | from_entries'
{"a":1,"b":2}

$ # yq
$ echo '{"a": 1, "b": 2}' | yq -P 'to_entries | from_entries'
a: 1
b: 2

with_entries は、オブジェクトを keyvalue の配列に分解し、各要素を加工してオブジェクトを組み立て直します。 yq ではフィールド名 .value と演算子 * の間にスペースが必要です。

$ # jq
$ echo '{"a": 1, "b": 2}' | jq -c 'with_entries(.value=.value*10)' 
{"a":10,"b":20}

$ # yq
$ echo '{"a": 1, "b": 2}' | yq -P 'with_entries(.value = .value * 10)'
a: 10
b: 20

Namespace 内のリソースを全て表示する

kubectl get では、リソースの種類を複数指定してオブジェクトを取得できます。

$ kubectl get deploy,daemonset -n kube-system
NAME                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/coredns   2/2     2            2           79s

NAME                        DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR            AGE
daemonset.apps/kindnet      4         4         4       4            4           <none>                   76s
daemonset.apps/kube-proxy   4         4         4       4            4           kubernetes.io/os=linux   79s

そこで、kubectl api-resources --namespaced -o name で Namespaced リソースの種類の一覧を取得し、paste コマンドで連結すると全ての種類のリソースを取得できます。 macOS をお使いの場合は paste -sd, - としてください。

$ # Ubuntu
$ kubectl api-resources --namespaced -o name | paste -sd,
bindings,componentstatuses,configmaps,endpoints,events,...

GET できないリソースに関するエラーを無視するため、末尾に 2>/dev/null を追加します。

$ NAMESPACE=kube-system
$ kubectl get -n ${NAMESPACE} $(kubectl api-resources --namespaced -o name | paste -sd,) -o json 2>/dev/null

これで Namespace 内の全てのリソースのマニフェストが手に入りました。 kubectl get のデフォルトの出力(上記参照)だと情報量が多いため、ここでは jqyq で加工します。 reduce を使って map に出力をマージします。

jq の場合、{<kind>:{<name>:1}} という形のオブジェクトを合成し、最後に name の辞書を with_entries(.value=(.value | keys)) でキーの配列に変換します。jq で map のキーを計算する際は式を () に入れる必要があります。

yq*+ で配列のマージができるので、{<kind>:[<name>]} という形のオブジェクトを *+ で合成します。

$ # jq
$ # -S は sort
$ NAMESPACE=kube-system
$ kubectl get -n ${NAMESPACE} $(kubectl api-resources --namespaced -o name | paste -sd,) -o json 2>/dev/null | jq -S 'reduce .items[] as $i ({}; . * {($i.kind): {($i.metadata.name): 1}}) | with_entries(.value=(.value | keys))'

$ # yq
$ NAMESPACE=kube-system
$ kubectl get -n ${NAMESPACE} $(kubectl api-resources --namespaced -o name | paste -sd,) -o yaml 2>/dev/null | yq '.items[] as $i ireduce({}; . *+ {$i.kind: [$i.metadata.name]}) | sortKeys(.)'

出力例

{
  "ConfigMap": [
    "coredns",
    "extension-apiserver-authentication",
    "kube-proxy",
    "kube-root-ca.crt",
    "kubeadm-config",
    "kubelet-config"
  ],
  "ControllerRevision": [
    "kindnet-6f5f87b9f5",
    "kube-proxy-b9c5d5dc4"
  ],
  "DaemonSet": [
    "kindnet",
    "kube-proxy"
  ],
  ...
}

Pod に付けられたラベルの統計情報を取得する

同じような方法で、クラスタ内の Pod に付いているラベルの統計情報を取得できます。 まず、クラスタ内の全ての Pod のマニフェストを取得し、ラベルの key と value の配列を取得します。

jq, yq いずれの場合も、パイプライン全体を [] で囲うと出力を一つの配列にまとめられます。 こうすると後段の処理が書きやすくなります。

jqto_entries は入力が null の場合(ここでは、Pod にラベルが付いていない場合)にエラーとなるので、エラーを無視するため ? を付けています。yqto_entriesnull を自動的に無視します。

$ # jq
$ kubectl get po -A -o json | jq '[.items[].metadata.labels | to_entries? | .[]]'

$ # yq
$ kubectl get po -A -o yaml | yq '[.items[].metadata.labels | to_entries | .[]]'

出力例

[
  {
    "key": "k8s-app",
    "value": "kube-dns"
  },
  {
    "key": "pod-template-hash",
    "value": "565d847f94"
  },
  ...
]

ラベルのキーと値が全て取得できたので、reduce で結果を整形します。

$ # jq
$ kubectl get po -A -o json | jq -S '[.items[].metadata.labels | to_entries? | .[]] | reduce .[] as $i ({}; . * {($i.key): {($i.value): 1}}) | with_entries(.value=(.value | keys))'

$ # yq
$ kubectl get po -A -o yaml | yq '[.items[].metadata.labels | to_entries | .[]] | .[] as $i ireduce({}; . * {$i.key: {$i.value: 1}}) | with_entries(.value = (.value | keys)) | sortKeys(.)'

出力例

{
  "app": [
    "kindnet",
    "local-path-provisioner"
  ],
  "component": [
    "etcd",
    "kube-apiserver",
    "kube-controller-manager",
    "kube-scheduler"
  ],
  ...
}

クエリを少し変えると、各ラベルに何種類の値が存在するかを調べられます。

$ # jq
$ kubectl get po -A -o json | jq -S '[.items[].metadata.labels | to_entries? | .[]] | reduce .[] as $i ({}; . * {($i.key): {($i.value): 1}}) | with_entries(.value=(.value | length))'

$ # yq
$ kubectl get po -A -o yaml | yq '[.items[].metadata.labels | to_entries | .[]] | .[] as $i ireduce({}; . * {$i.key: {$i.value: 1}}) | with_entries(.value=(.value | length))'

出力例

{
  "app": 2,
  "component": 4,
  "controller-revision-hash": 2,
  "k8s-app": 3,
  "pod-template-generation": 1,
  "pod-template-hash": 2,
  "tier": 2
}

@tsv を使うと、結果をテーブル形式で出力できます。

$ # jq
$ kubectl get po -A -o json | jq -r '[.items[].metadata.labels | to_entries? | .[]] | reduce .[] as $i ({}; . * {($i.key): {($i.value): 1}}) | with_entries(.value=(.value | keys | join(","))) | to_entries | .[] | [.key,.value] | @tsv' | column -t | sort

$ # yq
$ kubectl get po -A -o json | yq '[.items[].metadata.labels | to_entries | .[]] | .[] as $i ireduce({}; . * {$i.key: {$i.value: 1}}) | with_entries(.value=(.value | keys | join(","))) | to_entries | .[] | [.key,.value] | @tsv' | column -t | sort

出力例

app                       kindnet,local-path-provisioner
component                 etcd,kube-apiserver,kube-controller-manager,kube-scheduler
controller-revision-hash  5b547684d9,6bc6858f58
k8s-app                   kindnet,kube-dns,kube-proxy
pod-template-generation   1
pod-template-hash         547f784dff,558bd4d5db
tier                      control-plane,node

まとめ

ここでは reduce, from/to/with_entries の基本的な使い方と、応用例を 2 つ紹介しました。 この組み合わせは、リソースの種類やインスタンスを横断してクラスタの内容を掴みたいときに絶大な威力を発揮します! ぜひぜひ日々の運用にご活用ください。

参考資料