drumato.com

about
contacts
免責事項
ライセンス
記事一覧
日記一覧
ENGLISH
Kubectl Plugin Builder

Kubectl Plugin Builder

kubernetesgokubectl

この記事は IPFactory Advent Calendar 2021 の11日目です. 私がIPFactoryとして活動するのは今年度が最後なので,何かしら技術的知見が残せればと思って執筆しています.

ご存知の通り,Kubernetesはたくさんの拡張性をuserに提供しています. これは 公式document でも紹介されています.

ExtensibilityDescription
Custom Controller独自にresource reconcilerを記述できる仕組み
CRDOpenAPI Schemaをもとに,新たなresourceを定義できるような機能で,CRD自体が組み込みresource
Admission WebhookAPI request時にValidation/Mutationを行えるようなWebhook Serverを建てられる仕組み
Kubernetes Scheduler PluginNodeのScoring/Filtering algorithmに影響を与え,Pod Schedulingの挙動を変更する機能
CNI PluginflannelやCalicoに代表される,Container Networkingを実現するためのPluggable機構

これと同じように,Kubernetes運用者のほとんどが使用する kubectl でも拡張性が提供されています. それを kubectl plugin といい,それを開発/利用することで運用を効率化できます.

本記事ではこのkubectl pluginについて紹介しつつ, kubectl plugin開発に関連する話題を取り上げて, 最終的に私が開発しているcode generatorを解説します.


Background

kubectl pluginとは

ここではkubectl pluginについて復習します. kubectl pluginとはその実ただの実行形式です. kubectl本体が認識できるpathに置かれ, kubectl-* という命名がされていればkubectl pluginとして扱われます. 公式documentではShell Scriptで実装する例が紹介されています. kubectl pluginの利点はいくつかありますが,Kubernetes運用者にとって,kubectl本体のcommandと自作のoperation toolを統一的に扱えるのは非常に便利です. kubectl plugin list でどのようなpluginがinstallされているか確認することもできます.

著名なkubectl pluginの一つに,postfinance/kubectl-ns があります. kubernetes/sample-cli-pluginの題材でもありますし, awesome-kubectl-pluginsでも紹介されています. kubeconfigにはcontextを埋め込めるfieldが存在しますが, そのうちnamespaceの情報を簡単に扱うためのpluginです.

kubectl-nsは多くのことを成し遂げないtoolに見えますが, 個人的には, 小さな仕事を実現するpluginを組み合わせる という作り方がとても良いと思っています. この理由は後述します.

kubectl pluginの作り方

先程述べたように,kubectl plugin自体はただのexecutableであるため, shell scriptやPythonにGoなど,特定の言語に限らず実装することができます. よってここでは,私が考える kubectl pluginをうまく実装する方法 にfocusしたいと思います. 私はGoで,かつ spf13/cobra などのCLI application builderを使用して開発するのを強くおすすめします.

第一に,Kubernetesの運用者にとっての最も大きな関心は Kubernetesの運用を簡単に便利にする というものであり, それをどのように構築するかについてはあんまりcostを割きたくないからです.これは 本当に小さなpluginはshell script等でサクッと作るべき という主張にも見えますが,どちらかというと 小さくても,scaleしても管理しやすい言語でやったほうが良い ということを意味しています. この発想から, 小さなpluginを組み合わせる 方法の利点も見えてくると思います.

第二に,GoはKubernetes Ecosystemのほとんどすべてが採用している言語であり, Kubernetes Engineerにとって親しい言語だと言えるからです. kube-apiserverやkube-scheduler, kubectl本体などのcore componentなどもGoで書かれています. operation toolであると考えたとき,新しくteamに入ってきたmemberがすぐに使えるほうが便利です. これは,その分野でmainstreamとなっている言語で開発する利点を活かした形です.

最後に,kubectl pluginのほとんどが実際にGoで開発されており, 更にそれら殆どがcobraを使用している,という点です. kubectl pluginは case by caseで必要なものが異なる という点から実例を起点とした文献がほとんどですが, 多くの実装は公開されているため,それらを参照して書くということがやりやすくなります.

Goでkubectl pluginを作ることで見えてくる問題

いざGoでkubectl pluginを書こうとしたとき,いくつかのboilerplateが必要であることがわかります.

  • client-goの初期化処理
  • cli-runtimeの初期化処理
    • -n/--namespace などに代表される汎用的なcli flagの利用
  • Complete/Validate/Run という,kubectl plugin implsで頻出するpractice

これらは一度書くだけなら特に難しくないですが, やはり何度も書くと退屈な部分になってきますし, この書き方が微妙に異なることで素早く理解/改修できないと困ります. また,kubectl pluginも一般的にAPI clientを初期化して使用しますが, maintainabilityの高いpluginを開発するためにはいい感じにinterfaceを整備して, testableに開発する,みたいなことが必要になってきます. しかし,これをきれいに設計して,というのも一種のcostとして考えられます.


kubectl-plugin-builder

そこで,私はkubebuilder(本記事では解説しません)の思想や実績を参考にして, kubectl pluginの開発をサクッと始められるものを作り始めました. kubebuilderほどKubernetes communityで認められるものにできるかはわかりませんが, 少なくともidea自体はだいぶ便利な自負があるので,これからも開発は継続していきます. 実装は GitHub においてあります. また,かんたんな使い方についてはDocumentを書いています. 主な機能は次のとおりです.

  • project初期化機能
  • cli application architectureをyamlから宣言的に生成する機能
    • flag
    • command alias
  • yamlに新しいcommand definitionを追加する機能
  • pluginの出力formatを制御する機構

高々数k行の実装なのですぐ理解できると思いますし, 実装を読まなくても適当にcommand打って生成されたfile眺めてたらわかります.

簡単な使い方

まずは適当なdirectoryでprojectを初期化します.

1$ mkdir kubectl-demo && cd kubectl-demo 2$ kubectl-plugin-builder new github.com/Drumato/kubectl-demo 3Initialization Complete! 4Run `go mod tidy` to install third-party modules.

するといくつかのfileが生成されます. kubectl-plugin-builder new 実行直後のprojectは以下のような構成になっています.

1$ tree 2. 3├── cli.yaml 4├── cmd 5│ └── kubectl-demo 6│ └── main.go 7├── go.mod 8├── internal 9│ └── cmd 10│ ├── demo 11│ │ ├── command.go 12│ │ └── handler.go 13│ └── node.go 14├── LICENSE 15└── Makefile 16 175 directories, 8 file
  • cli.yaml ... pluginのCLI app architectureを定義するspec
    • make generate(kubectl-plugin-builder generate) で使用される
  • LICENSE ... 現在はMITのみ対応している
  • Makefile ... 開発に便利なtaskを持つtask runner
    • format ... すべてのGo packageのformat
    • test ... すべてのGo packageのtest
    • build ... plugin build
    • generate ... 宣言的にGo filesを生成する
    • install ... pluginを INSTALL_DIR にinstallする(defaultだと /usr/bin)
  • internal/cmd/node.go ... CLINodeOptions interfaceを定義するfile
    • plugin内のすべてのcommandがこのinterfaceを実装していることを仮定する
  • internal/cmd/demo ... root commandの定義
  • cmd/kubectl-demo/main.go ... the plugin's entrypoint

もちろんこの段階でbuildすることができます.

1$ go mod tidy 2$ make > /dev/null 3$ ./kubectl-demo -h 4Usage: 5 demo [flags] 6 7Flags: 8 -h, --help help for demo 9 -o, --output string the command's output mode (default "normal")

さて,それぞれのfileについて紹介します. まず cmd/kubectl-demo/main.go からです.

1// Code generated by kubectl-plugin-builder. 2package main 3 4import ( 5 "fmt" 6 "github.com/Drumato/kubectl-demo/internal/cmd/demo" 7 "os" 8 9 "k8s.io/cli-runtime/pkg/genericclioptions" 10) 11 12func main() { 13 streams := genericclioptions.IOStreams{ 14 In: os.Stdin, 15 Out: os.Stdout, 16 ErrOut: os.Stderr, 17 } 18 19 if err := demo.NewCommand(&streams).Execute(); err != nil { 20 fmt.Fprintf(os.Stderr, "ERROR: %+v\n", err) 21 os.Exit(1) 22 } 23}

ここで genericclioptions.IOStreams のinstanceを渡します. これは各commandがtestを書く場合を想定して,I/O Captureのために渡している感じです. testの際には IOSTreams.Outbytes.Buffer などを渡せば,出力結果をtestすることができます. 次に internal/cmd/node.go です.

1// Code generated by kubectl-plugin-builder. 2 3/* MIT License 4 * 5 * Copyright (c) 2021 you 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a copy 8 * of this software and associated documentation files (the "Software"), to deal 9 * in the Software without restriction, including without limitation the rights 10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 * copies of the Software, and to permit persons to whom the Software is 12 * furnished to do so, subject to the following conditions: 13 * 14 * The above copyright notice and this permission notice shall be included in all 15 * copies or substantial portions of the Software. 16 * 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 * SOFTWARE. 24 */ 25package cmd 26 27import ( 28 "github.com/spf13/cobra" 29) 30 31type CLINodeOptions interface { 32 Complete(cmd *cobra.Command, args []string) error 33 Validate() error 34 Run() error 35} 36 37type OutputMode = string 38 39const ( 40 OutputModeNormal OutputMode = "normal" 41 // OutputModeJSON 42 // OutputModeYAML 43)

ここでは CLINodeOptions interfaceの定義と, OutputMode と呼ばれる,各commandの出力結果を制御するための型が出力されます. すべてのcommandがこのinterfaceを実装するようになっているので, 自動的に Complete/Validate/Run modelを踏襲することができる,というわけです. Code generated by kubectl-plugin-builder.// Code generated by kubectl-plugin-builder; DO NOT EDIT. の区別があり, 前者の場合はuserによる更新を許容していて,後者は宣言的にreplaceされ続けます. 実際のcommand定義である internal/cmd/demo/command.go を見てみましょう.

1$ cat internal/cmd/demo/command.go 2// Code generated by kubectl-plugin-builder; DO NOT EDIT. 3 4/* MIT License 5 * 6 * Copyright (c) 2021 you 7 * 8 * Permission is hereby granted, free of charge, to any person obtaining a copy 9 * of this software and associated documentation files (the "Software"), to deal 10 * in the Software without restriction, including without limitation the rights 11 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 * copies of the Software, and to permit persons to whom the Software is 13 * furnished to do so, subject to the following conditions: 14 * 15 * The above copyright notice and this permission notice shall be included in all 16 * copies or substantial portions of the Software. 17 * 18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 * SOFTWARE. 25 */ 26package demo 27 28import ( 29 "github.com/spf13/cobra" 30 31 "github.com/Drumato/kubectl-demo/internal/cmd" 32 "k8s.io/cli-runtime/pkg/genericclioptions" 33) 34 35var ( 36 // demoOutputModeFlag provides 37 // user-passed option to options. 38 demoOutputModeFlag string 39) 40 41// WARNING: don't rename this function. 42func NewCommand(streams *genericclioptions.IOStreams) *cobra.Command { 43 c := &cobra.Command{ 44 Use: "demo", 45 46 Aliases: []string{}, 47 48 RunE: func(cmd *cobra.Command, args []string) error { 49 o := &options{streams: streams} 50 if err := o.Complete(cmd, args); err != nil { 51 return err 52 } 53 54 if err := o.Validate(); err != nil { 55 return err 56 } 57 58 return o.Run() 59 }, 60 } 61 62 hangChildrenOnCommand(c, streams) 63 defineCommandFlags(c) 64 65 return c 66} 67 68// hangChildrenOnCommand enumerates command's children and attach them into it. 69func hangChildrenOnCommand(c *cobra.Command, streams *genericclioptions.IOStreams) { 70} 71 72// defineCommandFlags declares primitive flags. 73func defineCommandFlags(c *cobra.Command) { 74 c.Flags().StringVarP( 75 &demoOutputModeFlag, 76 "output", 77 "o", 78 cmd.OutputModeNormal, 79 "the command's output mode", 80 ) 81}

*cobra.Command を返す関数を定義しています. cmd/kubectl-demo/main.go で呼び出されるものです. このGo fileの内容は cli.yaml によって決まります.

1license: MIT 2packageName: github.com/Drumato/kubectl-demo 3root: 4 name: demo 5 year: 2021 6 author: you 7 defPath: internal/cmd/demo 8 children:

最後に internal/cmd/demo/handler.go を紹介します. これは internal/cmd/demo/command.go で呼び出される Complete/Validate/Run の実装がおいてあり, userが好きに変更することを想定しています.inplaceに書き換わってしまうことはありません.

1// Code generated by kubectl-plugin-builder. 2 3/* MIT License 4 * 5 * Copyright (c) 2021 you 6 * 7 * Permission is hereby granted, free of charge, to any person obtaining a copy 8 * of this software and associated documentation files (the "Software"), to deal 9 * in the Software without restriction, including without limitation the rights 10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 * copies of the Software, and to permit persons to whom the Software is 12 * furnished to do so, subject to the following conditions: 13 * 14 * The above copyright notice and this permission notice shall be included in all 15 * copies or substantial portions of the Software. 16 * 17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 * SOFTWARE. 24 */ 25package demo 26 27import ( 28 "fmt" 29 30 "github.com/Drumato/kubectl-demo/internal/cmd" 31 "github.com/spf13/cobra" 32 "k8s.io/cli-runtime/pkg/genericclioptions" 33) 34 35// this assignment ensures 36// options struct must implement CLINodeOptions interface. 37var _ cmd.CLINodeOptions = &options{} 38 39type options struct { 40 cmd *cobra.Command 41 args []string 42 streams *genericclioptions.IOStreams 43 outputMode cmd.OutputMode 44} 45 46// Complete implements CLINodeOptions interface. 47func (o *options) Complete(cmd *cobra.Command, args []string) error { 48 o.cmd = cmd 49 o.args = args 50 o.outputMode = demoOutputModeFlag 51 return nil 52} 53 54// Validate implements CLINodeOptions interface. 55func (o *options) Validate() error { 56 return nil 57} 58 59// Run implements CLINodeOptions interface. 60func (o *options) Run() error { 61 switch o.outputMode { 62 // case cmd.OutputModeJSON: 63 // case cmd.OutputModeYAML: 64 case cmd.OutputModeNormal: 65 _, err := fmt.Fprintf(o.streams.Out, "%s\n", o.cmd.Use) 66 return err 67 } 68 69 return fmt.Errorf("unsupported output format '%s' found", o.outputMode) 70}

次に cli.yaml を書き換えて宣言的に生成してみます.

1license: MIT 2packageName: github.com/Drumato/kubectl-demo 3root: 4 name: demo 5 year: 2021 6 author: you 7 defPath: internal/cmd/demo 8 flags: 9 - name: flag1 # added 10 type: string 11 description: controls root command behavior 12 - name: flag2 # added 13 type: string 14 description: controls root command behavior 15 children: 16 - name: subcmd1 17 year: 2021 18 author: you 19 defPath: internal/cmd/demo/subcmd1 20 - name: subcmd2 21 year: 2021 22 author: you 23 defPath: internal/cmd/demo/subcmd2
1$ make > /dev/null 2$ ./kubectl-demo -h 3Usage: 4 demo [flags] 5 demo [command] 6 7Available Commands: 8 completion generate the autocompletion script for the specified shell 9 help Help about any command 10 subcmd1 11 subcmd2 12 13Flags: 14 --flag1 string controls root command behavior 15 --flag2 string controls root command behavior 16 -h, --help help for demo 17 -o, --output string the command's output mode (default "normal") 18 19Use "demo [command] --help" for more information about a command.

このように,cobra CLI applicationのconstruction,つまりcommand同士の親子関係もうまく扱ってくれます.

今後の展望

ここまでで基盤となるbuilder部分は作れたと思うので, あとはcmd argを自動でparseしてくれるようにしたり,client-go/pkg/clientset の初期化をしてくれたりという, 開発する上で便利な細々としたcode生成, そして tests/spec.yaml に書いた期待出力からそれをtestする internal/cmd/<CMD_NAME>/handler_test.go を自動生成するといった機能を作ろうと思っています.


Conclusion

本日はkubectl pluginについての関心事から紹介しつつ, 私が開発しているkubectl-plugin-builderを紹介しました. よろしかったらこれを使って遊んでみてください!

References