December 7, 2019

kubectl plugin を開発する

kubectl-plugin

本記事は Kubernetes Advent Calendar 2019 3 日目の記事です。

kubectl の plugin 機構は Kubernetes v1.14 から stable になった機能です。
今回は kubectl plugin を実践的に実装する方法について解説します。

kubectl plugin 概要

kubectl plugin とは、kubectl- prefix のついたコマンドの PATH が通っていれば、kubectl から実行できるというものです。
たとえば、kubectl-ping であれば、kubectl ping となります。

Git のサブコマンドと同じ方式なので、plugin 自体は言語を問わず開発できます。 しかし後述するように、基本的にはシェルスクリプトまたは Go で実装することになるでしょう。

既存の plugin については、プラグインマネージャ krew や直接 GitHub で “kubectl plugin” などのクエリで検索することで探すことが出来ます。

plugin を作る際、基本的に Read 操作に限る、ということは一つの指標でしょう。 kubectl の実行は imperative であり、imperative な Write 操作は Kubernetes の declarative に操作を行う思想に反するからです。
Write 操作を行うような自動化をしたいときは Custom Controller の実装を検討したほうが良いと思います。

シェルスクリプトで実装する

kubectl plugin の開発にシェルスクリプトが向いている理由の一つは、kubectl を直に実行できる点です。

kubectl の実行結果を awk や sed、jq などでフィルタ・整形、別の kubectl コマンドの引数に渡すことで複雑な操作を可能にします。

例として現在の namespace にある resource の apiVersion をリストする kubectl-current-api-versions という簡単な plugin を作りました (実際に有用かどうかはおいておきます)。

シェルスクリプトの名前が kubectl-current_api_versions.sh という、ケバブケースとスネークケース混じりになっているのは、kubectl plugin の仕様で、サブコマンドをケバブケース (e.g. current-api-versions) にする場合はサブコマンド部分をスネークケース (e.g. current_api_versions) にしなければならない、というものがあるからです。
ちなみに、実行ファイル名のサブコマンド部分をケバブケース (e.g. current-api-versions) にした場合は空白区切りのコマンド (e.g. current api versions) になります。

この plugin の実態は以下のコマンドを実行しているだけの極めて簡易的なものです。

$ kubectl get all -o json | jq '.items[].apiVersion' | uniq | sed 's/"//g'

plugin を実行するときは以下のようになります。

$ kubectl current-api-versions
v1
apps/v1

kubectl が直接使えるという観点から、シェルスクリプトで実装するのは最も簡単かつ、強力な方法でしょう。 色々なプロジェクトでよく使う kubectl ラッパースクリプトを kubectl plugin にすることを検討するのも良いでしょう。

Kubernetes を利用する際に欠かせないシェルスクリプトである kubectx や kubens も今実装されていたなら恐らく plugin として開発されていたでしょう。

シェルスクリプトで開発する際には、--namespace など kubectl 実行時に使えるオプションのサポートは自前で行い kubectl コマンドに渡す必要があります。

よって、kubectl のオプションの多くをサポートしたい場合、以下の例のように kubectl get のようにテーブル形式でフォーマットしたい場合、kubeconfig を読み取りたい場合、複雑な操作が要求される場合などでは、後述するように Go で実装するのが良いでしょう。

$ kubectl get pods
NAME                       READY   STATUS    RESTARTS   AGE
dev-app-587f4d4cb5-cz6dm   1/1     Running   1          3d10h
dev-app-587f4d4cb5-xbzqm   1/1     Running   1          3d10h

Go で実装する

kubectl plugin を Go で実装する理由は、Kubernetes なので例によって Go によるエコシステムが充実しているからです。

ヘルパーライブラリの kubernetes/cli-runtime を使うことで plugin 実装が容易になります。 cli-runtime はいくつかのサブパッケージを持っており、適宜必要なものを利用する形になります。
kubectl 本体の実装にも使われています。

  • genericclioptions: kubectl plugin のためのフラグ用のヘルパーを提供
  • kustomize: kustomize 実行のためのヘルパーを提供
  • printers: Kubernetes Object を io.Writer で Write する際のヘルパー (Go Template、フォーマット、YAML 形式など) を提供
  • resource: Kubernetes Object への CRUD のためのヘルパーを提供

公式によるサンプル kubernetes/sample-cli-plugin の実装も参考になると思いますが、genericclioptions しか用いていないため、kubectl 本体のサブコマンド実装の方が参考になると思います。

genericclioptions

genericclioptions を利用することで、以下の global option のバインディングが可能になります。

  • cluster
  • user
  • context
  • namespace
  • server
  • insecure-skip-tls-verify
  • client-certificate
  • client-key
  • certificate-authority
  • token
  • as
  • as-group
  • username
  • password
  • request-timeout
  • cache-dir

以下のようにバインディングすることで、シェルスクリプトの際に自前で行う必要のあったフラグのパースが不必要になります。

configFlags := genericclioptions.NewConfigFlags(...) // genericclioptions.ConfigFlags でオプションを管理
flags := pflag.NewFlagSet(...)
configFlags.AddFlags(flags) // pflag.FlagSet に上記オプションをバインディング

内部的には spf13/pflag を利用しているので、kubectl plugin は基本的に spf13/pflag に依存することになります。

ConfigFlagsRESTClientGetter interface を実装しており、k8s.io/client-go/rest.Config を返す ToRESTConfig() メソッドを利用して使いたいオプションを取得します。

resource

resource パッケージは genericclioptions パッケージと組み合わせることで各種オプションを有効化しつつ Kubernetes Object への CRUD 操作を行うことが出来ます。

kubernetes/client-go パッケージでも当然 CRUD 操作は行えるのですが、genericclioptions.ConfigFlags の設定を inject しやすいことやコマンドライン引数処理の観点から plugin 開発では基本的にこちらを使うべきだと思います。

Buildergenericclioptions.RESTClientGetter (genericclioptions.ConfigFlags ) の設定を読み、Kubernetes Object への bulk 操作を行います。
以下にサンプルコードを載せます。

result := resource.
    NewBuilder(configFlags).
    Unstructured().
    NamespaceParam(namespace).
    DefaultNamespace().
    ResourceTypeOrNameArgs(true, args...).
    Latest().
    Flatten().
    Do()
if err := result.Err(); err != nil {
    return err
}

if err := result.Visit(func(info *resource.Info, e error) error {
    // 
}); err != nil {
    return err
}

各種メソッドの説明は省きますが、最終的に各レスポンスに対して Visitor 関数を実行することがポイントです。
たとえば Visitor 関数に前述の ResourcePrinter で Object の情報を出力することなどが出来ます。

printers

kubectl plugin は CLI であるため、出力は当然標準出力になります。 そうなると、kubectl get のようにテーブルでフォーマットして出力したかったり、何かしらのテンプレートを利用して出力したかったりします。

しかし、自前でテーブル形式で出力しようと思うと、そもそもフォーマットを実装 (あるいはサードパーティのパッケージを使う) したり、ヘッダーを表示したり、Wide 形式にしたり、Object の情報の他に namespace を表示したり、など考えることが多いです。

そこで printers パッケージが有用です。
printers パッケージは Kubernetes Object を human readable に出力するヘルパーを提供します。 実態としては、ResourcePrinter interface を実装した各種 struct の PrintObj メソッドを用います。 interface の定義によりモックや差し替えが可能になっています。

type ResourcePrinter interface {
    // Print receives a runtime object, formats it and prints it to a writer.
    PrintObj(runtime.Object, io.Writer) error
}

例として、テーブル形式で出力する NewTablePrinter での出力を考えます。

func NewTablePrinter(options PrintOptions) ResourcePrinter

引数の PrintOptions は以下のようになります。

type PrintOptions struct {
    NoHeaders     bool
    WithNamespace bool
    WithKind      bool
    Wide          bool
    ShowLabels    bool
    Kind          schema.GroupKind
    ColumnLabels  []string

    SortBy string

    // indicates if it is OK to ignore missing keys for rendering an output template.
    AllowMissingKeys bool
}

この PrintOptions を利用することで、ヘッダの設定や namespace、kind の表示有無などを設定できるため、human readable かつ Kubernetes way な出力が簡単に実現できます。

NAMESPACE   NAME                       AGE     LABELS
default     dev-app-587f4d4cb5-9cwqt   3d11h   app=app,pod-template-hash=587f4d4cb5
default     dev-app-587f4d4cb5-cz6dm   3d11h   app=app,pod-template-hash=587f4d4cb5
default     dev-app-587f4d4cb5-xbzqm   3d11h   app=app,pod-template-hash=587f4d4cb5

実装例

以上を踏まえてサンプルの plugin を作りました。
micnncim/kubectl-lab で見れます。

実装例としてシンプルにするために kubectl get とほぼ同じ動作をするだけのものにしました。

krew

kubectl plugin 用のパッケージマネージャ krew を使うことで plugin の簡単なインストール・管理が可能になり、配布が簡単になります。

以下は README の引用ですが、典型的なプラグインマネージャのコマンドをサポートしています。

$ kubectl krew search                 # show all plugins
$ kubectl krew install view-secret    # install a plugin named "view-secret"
$ kubectl view-secret                 # use the plugin
$ kubectl krew upgrade                # upgrade installed plugins
$ kubectl krew uninstall view-secret  # uninstall a plugin

自分の開発した plugin を krew でインストールできるようにするには、krew-index に登録する必要があり、自分の plugin の情報を記載した Plugin CRD のPR を出して approve されれば無事 krew でも利用可能になります。
maintainer にはレビュー時に plugin の有用性などについて問われることがあり、必ずしも approve されるわけではないことに注意です (自分はまだ PR を出していません)。

まとめ

kubectl plugin を開発するときは、シンプルに実装するときはシェルスクリプト、色々な機能やオプションをサポートしたいときは Go で実装する、という方針で良いと思います。

個人的にももっと便利な plugin を開発していきたいと思っています。

参考

© micnncim 2019

Powered by Hugo & Kiss.