All Articles

kustomize pluginでmanifestを動的に操作する

2019/07/04に kustomize の v3.0.0 がリリースされ、それまで暗黙の仕組みっぽかったプラグイン機構がオフィシャルになった。 …と言っても大きくアナウンスされているような節はなく、ドキュメントについても、

https://github.com/kubernetes-sigs/kustomize/tree/master/docs/plugins

このページのみで、また他に自作する方法のエントリが見当たらなかったので書く。なおこのエントリはやや社内向けでもある。

宣言的なmanifest管理

kubernetesのmanifestを管理する機構として、代表的なものは helmkustomize だと思う。以前はプロジェクトで helm を使っていたけど、 base に記述した設定を overlay で差分パッチを当てつつmanifestを生成する手法はいいな、ということでいつしか kustomize を使うようになった。kustomize というかkubernetesのmanifestsは宣言的に記述できる点が運用しやすく、構成をYAMLで記述することでkubernetes APIを通じてリソースが適切に配置される。なので如何にして効率よくこれらのYAMLを管理・生成するかが運用観点からは大事で、kustomize はその用途にして必要十分な条件を揃えていると思っている(多少足りない機能などもあるけど)。

動的な部分はどうするか

とはいえ、どうしても実行コンテキストに依存する動的な部分が出てくる。例えばCIからビルドしたてホヤホヤのコンテナイメージでpodを立てるまでを自動化したい場合、CIでビルドしてからイメージハッシュを書き換えて kustomize build -> kubectl apply のような手順を取る必要が出てくる。kustomize build で生成されたmanifestに対して sed で置き換えるみたいなこともできなくはないが、手順として美しくない。かといって overlays でどうにかするのか?というのも微妙で、動的に変わる部分をうまくやる方法を模索していた中、冒頭に書いた kustomize plugin が公式にサポートされたのでこれを使うことにした。

kustomize pluginを自作する

作成手順は https://github.com/kubernetes-sigs/kustomize/tree/master/docs/plugins にある通りだが、もう少しだけ詳しく書く。pluginは大きく GeneratorTransform のフェーズに分けられ、それぞれ、

  • Generator: 動的にmanifestを生成して組み込む
  • Transform: 生成物のmanifestを変形する

用途として用いられる。また、これらはplugin実行用の manifest をロードし、かつコンパイルしたpluginコマンドを実行することで実現される。このコマンドは実行形式ならなんでも良いらしく、Shell Scriptで書いても良いし、Goでも書ける。ここではちょっとクセのあるGoプラグインを作成する方法について書く。

実装体系

上述のdocsに記載のあるフォーマットで記載する。大まかには以下の決まりを守れば良い:

  • main パッケージとして記述する
  • Generator または Transformer インターフェースのいずれか、または両方を満たす実装をする
  • その実装を KustomizePlugin という変数でexportする

具体的な実装は以下。docsそのままだけど、ちょっと処理内容のコメントを足してある:

package main

import (
    // 必要なパッケージをimport
    "sigs.k8s.io/kustomize/v3/pkg/ifc"
    "sigs.k8s.io/kustomize/v3/pkg/resmap"
    "sigs.k8s.io/yaml"
)

// プラグイン定義
type myPlugin struct {
  loader ifc.Loader
  factory *resmap.Factory

  // plugin設定ファイルの値にmappingするフィールド
  Message string `json:"message" yaml:"message"`
}

// プラグイン識別用にこの変数名でexportする
var KustomizePlugin myPlugin

// プラグイン設定ファイルのロード。
func (p *myPlugin) Config(
    ldr ifc.Loader,     // pluginローダ。実行ルートなどの情報が取れる
    rf *resmap.Factory, // manifest生成Factory。このポインタを経由して追加manifestを生成したりする
    config []byte,      // plugin設定ファイルの[]byte配列。これをUnmarshalすれば設定が読み込める
) error {
    p.loader = ldr
    p.factory = rf
    // configをUnmarshalしてプラグインの構造体フィールドにmappingする
    return yaml.Unmarshal(config, p)
}

func (p *myPlugin) Generate() (respmap.Respmap, error) {
  // Generatorフェーズにコールされる
  // manifest定義のresmap.Resmapを作って返却することで kustomize build の結果に含めることができる
}

func (p *myPlugin) Transform(m resmap.ResMap) error {
  // Transformフェーズにコールされる。
  // 引数mにはbuild結果のmanifestが全て入っているので、この中身を改変することができる
}

ifc.Loaderresmap.Factory の他に resourcetypes というパッケージも必要になってくる(はず)。godocではリンクがつながってなかったりするので下記にリンクを貼っておく。

それぞれの構造体とメソッドを読めばやりたいことが実現できると思う。また、プラグインの実行フェーズでは当然ながらGoの機能がフルに使える。例えば os.Getenv() で環境変数を使うこともできるし、net/http で外部リソースを取ってきて使う、なんてこともでき、実行コンテキストに依存する処理、例えばCIビルド中の環境変数を埋め込んだりが可能になる。

例: sopsをデコードしてSecretとして追加する

実装例として、Secretsをsopsでデコードしてmanifestに追加するプラグインを書く。なお、この実装は https://github.com/Agilicus/kustomize-sops にて参考実装があるのでそれをkustomize pluginとして書き直したものとする。これはkustomize pluginがオフィシャルになる前からあり、プラグイン作成に際してとても参考にさせていただいた。わかりやすいようにコメントも入れておく。

// SopsSecret.go
package main

import (
    "log"
    "path/filepath"

    "go.mozilla.org/sops/decrypt"
    "sigs.k8s.io/kustomize/v3/pkg/ifc"
    "sigs.k8s.io/kustomize/v3/pkg/resmap"
    "sigs.k8s.io/kustomize/v3/pkg/types"
    "sigs.k8s.io/yaml"
    )

type plugin struct {
    rf        *resmap.Factory
    ldr       ifc.Loader

    // 以下のフィールドは設定ファイルからロードされる
    Name      string   `json:"name,omitempty" yaml:"name,omitempty"`
    Namespace string   `json:"namespace,omitempty" yaml:"namespace,omitempty"`
    File      string   `json:"file,omitempty" yaml:"file,omitempty"`
    Keys      []string `json:"keys,omitempty" yaml:"keys,omitempty"`
}

var KustomizePlugin plugin

func (p *plugin) Config(
    ldr ifc.Loader,
    rf *resmap.Factory,
    c []byte,
) error {
    p.rf = rf
    p.ldr = ldr
    // 設定値を構造体にマッピングする
    return yaml.Unmarshal(c, p)
}

// Generate()が実装されているので、このプラグインは Generatorフェーズで実行される
func (p *plugin) Generate() (resmap.ResMap, error) {
    secret := make(map[string]string)
    // ifcLoader.Root()で実行ルートを取得して、sopsでエンコードされたファイルを取得する
    secret_file := filepath.Join(p.ldr.Root(), p.File)
    v, err := decrypt.File(secret_file, "yaml")
    if err != nil {
      log.Fatal(err)
    }
    err = yaml.Unmarshal([]byte(v), &secret)
    if err != nil {
      log.Fatal(err)
    }

    // types.SecretArgs{}で Secretのmanifestフォーマットを生成
    args := types.SecretArgs{}
    args.Name = p.Name
    args.Namespace = p.Namespace
    for _, k := range p.Keys {
      if v, ok := secret[k]; ok {
        args.LiteralSources = append(
            args.LiteralSources, k+"="+v)
      }
    }
    // resmap.Factoryのメソッドからmanifestリソースを生成する
    return p.rf.FromSecretArgs(p.ldr, nil, args)
}

あとはこのファイルを plugin mode でコンパイルする。バイナリの置き場所も決まっており、

${XDG_CONFIG_HOME}/kustomize/plugin/${apiVersion}/LOWERCASE(${kind})/${kind}

に配置する。apiVersionは任意のFQDN形式(他と被らない)などで良くて、例えば kustomize.wnotes.net とする場合、配置先は

${XDG_CONFIG_HOME}/kustomize/plugin/kustomize.wnotes.net/v1/sopssecret

となる。シェルによっては $XDG_CONFIG_HOME は定義されていないので、その場合は $HOME/.config とすれば良い。ビルドコマンドは、

go build -buildmode plugin -o ${XDG_CONFIG_HOME}/kustomize/plugin/kustomize.wnotes.net/v1/sopssecret/SopsSecret SopsSecret.go

となる。この時注意点があり、 kustomizeのコマンドがビルドされている各種ライブラリのバージョンと一致させることが必要 という点に気をつける必要がある。実際に遭遇した例で言うと、プラグイン中で kubernetes/client-go を使って現在のpodの状態をAPIから取得しようとしてプラグインに組み込んだが、 kustomize が内部で使っているバージョンと一致せずコンパイルできない、というケースに遭遇した(どうしようもなかったのでプラグイン中では kubectl get pods コマンドを実行して取得して対応した)。使用するライブラリ、特にkubernetes関連のライブラリのバージョンには気をつける必要がある。 go modules 配下でビルドすればバージョンを揃えるのは比較的簡単になると思う。

プラグインの実行

ここまでできればあとは実行するだけである。プラグインの起動設定はboostrapとなる kustomization.yaml に書く。通常は overlays に書くことになるはず:

// app/overlays/production/kustomization.yaml
bases:
  - xxx
patchesStrategicMerge:
  - yyy
  - zzz

# Generator pluginの設定
generators:
  - sopssecret.yaml

# Transform pluginの設定
transformers:
  - ...

sopssecret.yaml プラグインの設定ファイルは下記:

apiVersion: kustomize.wnotes.net/v1
kind: SopsSecret
metadata:
  name: global
name: global
file: secret.yaml
keys:
  - ...

apiVersion の指定とプラグインファイルの配置先の対応を確認して欲しい。 ${yaml.apiVersion}/LOWERCASE(${yaml.kind})/${yaml.kind} でプラグインを探すので、上記の設定の場合は、

${XDG_CONFIG_HOME}/kustomize/plugin/kustomize.wnotes.net/v1/sopssecret/SopsSecret

とビルドした実行バイナリが呼ばれることになり、先にビルドした実行バイナリの配置先と一致する。そしてそのプラグインコードの Config() メソッドにこのYAMLファイルを []byte にmarshalしたものが第三引数として渡され、このプラグイン設定ファイルの中身によってプラグインの挙動を制御することが可能になる。多少省略した部分もあるが、これが kustomize プラグインの基本形となる。

実際の使い所

現在のプロジェクトでは istio を導入しており、 CI上で最新イメージでDeploymentを構成、 VirtualService / DestinationRule を切り替える処理までをプラグインを併用して自動化している。

  1. Generator フェーズで VirtualService 及び DestinationRule を動的に生成してDefault Ruleを切り替える、または指定ヘッダでリクエストを転送する設定を注入する
  2. Transform フェーズで docker build したイメージハッシュで Deployment のコンテナを差し替えてデプロイ

なお、即座にSWAPせず動作確認が必要なケースではDefault Routeまでは切り替えず、指定ヘッダ指定でルーティングを転送する部分までを行っている(プラグインの設定ファイルで制御できるようにしている)。これらの一連の処理を独自プラグインで実現している。実際はFastlyを組み合わせてより詳細な組み合わせでルーティングを制御できるアーキテクチャ構成を取っているが、これはどこかの機会でメンバーが公開してくれると思うのでそちらに譲ることにする。

ご利用は計画的に

プラグインを導入することで kustomize 管理下のmanifestsを制御下におくことができ、極端に言うと全てを動的に生成したりすることも可能となるが、 kustomize 自体は宣言的なmanifestを管理するものであり、冪等を保つには動的な部分は最小限にとどめておくべきであり、アーキテクチャ・運用上でどうしても動的に制御すべき部分においてのみプラグインで操作するのが良い使い方だと思う。またプラグインバイナリは内部の挙動がパッと見てわからないので、プロジェクトで導入する際にはプラグインの挙動についてメンバーの合意を取っておくことも重要である。ご利用は計画的に。

現場からは以上です。