なになれ

IT系のことを記録していきます

Gitに秘匿情報をcommitする必要がなくなるKustomize Pluginを作った

AWS Systems Manager Parameter Storeを使って、SecretsリソースをGenerateするKustomize Pluginを作りました。

作ったもの github.com

この記事では、なぜこのPluginが必要だったのか、どのようにPluginを作ったかといったことを紹介します。
このPlugin自体はAWS環境のみとなりますが、同様のPluginを作ることでその他の環境でも似たようなことができると思います。
また、GitOpsにおける秘匿情報管理の参考例になるのではと思います。

なお、今回の内容はKustomize v3.8.4を前提としたものになります。

モチベーション

以下の条件を満たすために、独自の仕組みを用意することにしました。

  • GitOpsにおいて秘匿情報をGitにcommitしたくない
  • Secretsリソースの環境変数が変更されたらPodに変更が反映される
  • PodがCustom Resourceで作成されていても問題なく動作する

以下、細かい説明です。

GitOpsでは、KubernetesのManifestファイルをGitに登録する必要があります。
これは秘匿情報を保持するSecretsリソースも例外ではありません。
Secretsリソースのデータはbase64エンコードされているだけなので、Gitのリポジトリを見ることができれば簡単に秘匿情報を取得できてしまいます。
これではマズいので、Kubernetes関連のOSSでは、秘匿情報を扱うためのプログラムがいくつかあります。有名どころでは、以下のものです。

GitHub - bitnami-labs/sealed-secrets: A Kubernetes controller and tool for one-way encrypted Secrets

GitHub - godaddy/kubernetes-external-secrets: Integrate external secret management systems with Kubernetes

これらはCustom Resourceという形で独自のリソースをKubernetes内で動作させて、代わりにSecretsリソースを作るプログラムです。
これによって、Gitに秘匿情報をcommitせずに済みます。
ただこのやり方だと、Secretsリソースが作り直された時にSecretsリソースを参照するPodを作り直すといったことができません。
Podを作り直す処理はSecretsで定義された環境変数をPodに反映させるために必要な処理になります。
実はSecretsリソースとの差分を検出して、Podを作り直すプログラムもあります。

GitHub - stakater/Reloader: A Kubernetes controller to watch changes in ConfigMap and Secrets and do rolling upgrades on Pods with their associated Deployment, StatefulSet, DaemonSet and DeploymentConfig – [✩Star] if you're using it!

ただDeploymentsリソースといったKubernetes標準のリソースしか対応しておらず、例えばArgo RolloutsといったPodをCustom Resourceで作成するケースの場合には対応できませんでした。

Kustomize Pluginについて

ここからはgeneratorに分類されるPluginを作成する前提での説明になります。
generatorはManifestの内容をプログラムで生成する機能です。
このgeneratorを独自に作ることでSecretsリソースのデータをファイル上では隠蔽しつつ、Secretsリソースを作ることができます。

Pluginの利用方法

まずは、KustomizeでPluginを利用方法です。

利用するgeneratorをkustomization.yamlで指定します。

kustomization.yaml

...
generators:
- aws-ssm-secret.yaml
...

Pluginで利用するパラメータを定義するためのyamlファイルを用意します。

aws-ssm-secret.yaml

apiVersion: hi1280.com/v1
kind: AwsSsmSecret
metadata:
  name: secret
  version: 1
envs:
- PASSWORD=/hello-service/password
region: ap-northeast-1

apiVersionおよびkindの値がPluginを配置するPathと関連しています。
例えば、上記のyamlの場合、以下にPluginを配置します。

$XDG_CONFIG_HOME/kustomize/plugin/hi1280.com/v1/awsssmsecret/

$XDG_CONFIG_HOMEはデフォルトでは$HOME/.configになります。
$XDG_CONFIG_HOME/kustomize/pluginフォルダ以下にapiVersionの値、小文字にしたkindの値という形でPathが決められます。

Pluginの実装

Pluginの実装については、Builtinで用意されているsecret generatorの実装が参考になります。

kustomize/SecretGenerator.go at master · kubernetes-sigs/kustomize · GitHub

SecretGenerator.go

package main

import (
    "sigs.k8s.io/kustomize/api/kv"
    "sigs.k8s.io/kustomize/api/resmap"
    "sigs.k8s.io/kustomize/api/types"
    "sigs.k8s.io/yaml"
)

type plugin struct {
    h                *resmap.PluginHelpers
    types.ObjectMeta `json:"metadata,omitempty" yaml:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
    types.SecretArgs
}

//noinspection GoUnusedGlobalVariable
var KustomizePlugin plugin

func (p *plugin) Config(h *resmap.PluginHelpers, config []byte) (err error) {
    p.SecretArgs = types.SecretArgs{}
    err = yaml.Unmarshal(config, p)
    if p.SecretArgs.Name == "" {
        p.SecretArgs.Name = p.Name
    }
    if p.SecretArgs.Namespace == "" {
        p.SecretArgs.Namespace = p.Namespace
    }
    p.h = h
    return
}

func (p *plugin) Generate() (resmap.ResMap, error) {
    return p.h.ResmapFactory().FromSecretArgs(
        kv.NewLoader(p.h.Loader(), p.h.Validator()), p.SecretArgs)
}

plugin型でPluginが受け取るパラメータを定義します。
Configメソッドはパラメータを実際にセットする処理を実行します。
Generateメソッドは値を生成する処理を実行します。

今回のPluginではGenerateメソッドで、AWS SSMから秘匿情報を取得し、値をセットしています。

...
func (p *plugin) Generate() (resmap.ResMap, error) {
...
    for _, e := range p.Envs {
        env := strings.Split(e, "=")
        getParamsInput := &ssm.GetParameterInput{
            Name:           aws.String(env[1]),
            WithDecryption: aws.Bool(true),
        }
        resp, err := svc.GetParameter(getParamsInput)
        if err != nil {
            return nil, err
        }
        args.LiteralSources = append(args.LiteralSources, env[0]+"="+*resp.Parameter.Value)
    }

    return p.h.ResmapFactory().FromSecretArgs(
        kv.NewLoader(p.h.Loader(), p.h.Validator()), args)
}
...

Goのプログラムが完成したら、ビルドを行います。
Pluginとして動作するようにビルドを行い、適切なPathにPluginを配置します。
例えば、以下のコマンドになります。

$ go build -buildmode plugin -o $XDG_CONFIG_HOME/kustomize/plugin/hi1280.com/v1/awsssmsecret/AwsSsmSecret.so .

Pluginの実行

Go Pluginを利用する場合、PluginをコンパイルしたGoでkustomizeもコンパイルする必要があります。

これについての詳細は下記にあります。
Go plugin Caveats | Kustomize

kustomizeをコンパイルするには以下を実行します。

$ go get sigs.k8s.io/kustomize/kustomize/v3

既にkustomizeをインストール済みの場合、コンパイルしたkustomizeを利用するようにPathを通す必要があります。

kustomize buildの実行には、--enable_alpha_pluginsの指定が必要になります。

$ kustomize build --enable_alpha_plugins .

まとめ

GitOpsで秘匿情報を扱うにはどうすれば良いかと試行錯誤した結果、Kustomize Pluginを作ることに行き着きました。
今回の取り組みを通して、GitOpsで秘匿情報を扱うことについて、まだデファクトスタンダードが存在しないということが分かりました。
今回紹介したPluginの使い方はGitHubのREADMEを見てもらえればと思います。

参考

Kustomize Plugins | Kustomize