KyvernoでSecretに文字列処理を行う方法

Kubernetesクラスタ運用中、Secretの値に特定の処理を施した上で保存・再利用したいケースはしばしば発生します。たとえば、外部システムが投入するSecretに含まれる文字列を大文字化、もしくは一部のパターンを置換して管理したい場合などです。本記事では、KubernetesネイティブなポリシーエンジンKyvernoを用いて、Secret適用時に文字列処理を行う2つのアプローチについて解説します。

![Kyverno Secret String Processing.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7565/49ce3839-0c86-db57-ef66-9091a37a47ff.png)


## Kyvernoとは

Kyvernoは、KubernetesリソースをYAMLベースで記述し、ポリシーとして適用できる強力なポリシーエンジンです。KyvernoはAdmissionコントローラーとして動作し、以下のような機能を提供します：

* Validate: リソース適用前の条件検証
* Mutate: リソース適用時にパッチを当ててリソースを変更
* Generate: あるリソースをトリガーに、別のリソースを自動生成

## 文字列処理をしたいシナリオ

たとえば、source-secretというSecretが作成された時、その中のdata.usernameを大文字化してtarget-secretという新たなSecretに反映したいと考えます。最初に考えられるアプローチは、Kyvernoのgenerateを使ってsource-secretが作成されたタイミングでtarget-secretを自動生成する、というものです。

しかしここで、Kyvernoのgenerateには「同一Kindのリソースを同時に生成できない場合がある」という制約があります。特に同一のSecretを生成する際など、generateの仕組み上想定しづらいパターンが発生する可能性があります。

### generateルールの制約

Kyvernoのgenerateは、たとえばConfigMapが作成されたタイミングで別のConfigMapを生成する、といった使い方に適しています。しかし、generateで「Secretが来たらSecretを生成する」という場合、"generation kind and match resource kind should not be the same"というエラーが発生します。これは同一Kindを生成すると、その生成されたリソースからまたリソースが増えてしまうこと抑制しているものだとおもわれます。

このような状況では、別のアプローチとしてmutateを使用する方法が考えられます。

### mutateで同一Secret内のデータ変換を実現

mutateルールを使えば、source-secretが適用される直前にSecretリソース自体を書き換えることが可能です。つまり、同一のSecretリソースのdata.usernameを事前に大文字化してからクラスタに保存できます。これなら、同一Kind生成の問題を回避できます。

## Kyvernoで使える文字列処理関数

KyvernoはJMESPath拡張関数を提供しており、文字列操作に便利な関数が多数あります：

* to_upper(string): 文字列を大文字に変換
* to_lower(string): 文字列を小文字に変換
* replace(old, new, string), replaceAll(old, new, string): 文字列置換
* regex_replaceAll(pattern, replace, string): 正規表現置換
* base64_decode(string) / base64_encode(string): Base64エンコード/デコード
* トリム系関数 (trim, trim_prefix, trim_suffix) など

Secretのdata値はBase64エンコードされています。そのため、data.usernameを加工する場合はbase64_decode()で文字列を元に戻し、その後to_upper()等で大文字化。最後にbase64_encode()で再度エンコードし直す流れとなります。

## mutateルールの例

以下はdefaultネームスペースのsource-secretが作成される際に、data.usernameを大文字化して上書きするポリシー例です：

```yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: mutate-secret-data
spec:
  rules:
    - name: mutate-secret-username
      match:
        resources:
          kinds:
            - Secret
          namespaces:
            - default
          names:
            - source-secret
      mutate:
        patchStrategicMerge:
          apiVersion: v1
          kind: Secret
          metadata:
            name: "{{ request.object.metadata.name }}"
            namespace: "{{ request.object.metadata.namespace }}"
          data:
            username: "{{ base64_encode(to_upper(base64_decode(request.object.data.username))) }}"
```

## ポリシーの動作確認

1. 上記ポリシーをkubectl apply -fで適用します。
2. 以下のSecretをkubectl apply -fで投入します：

```yaml
apiVersion: v1
kind: Secret
metadata:
  name: source-secret
  namespace: default
type: Opaque
data:
  username: dXNlck5hbWU=  # "userName"をBase64エンコードした値
```

3. 適用後にkubectl get secret source-secret -o yamlで中身を確認すると、usernameの値はUSERNAMEに大文字化された文字列のBase64エンコードに置き換わっています。

これで、Admission時に同一Secret内で文字列変換が行われたことが確認できます。

## まとめ

当初はgenerate機能を用いて、「あるSecretをトリガーに別のSecretを文字列処理して生成する」シナリオを考えましたが、Kyvernoのgenerateには同一Kind生成が難しい状況があります。そこで、同一リソース上でのデータ変換が必要な場合はmutateを使うことで、Admission時にリソース自体を変換できます。

mutateなら、Secretが保存される直前にdataを任意の文字列処理で加工でき、base64_decode(), to_upper(), base64_encode()などのKyverno拡張関数を組み合わせることで、複雑な文字列変換も実現可能です。

KyvernoはポリシーベースでKubernetesリソースを制御・拡張できる有用なツールです。generateとmutateの使い分けを意識しつつ、SecretやConfigMap、他のKubernetesリソースの管理に役立ててみてください。