Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

Knative ServingをKustomizeでPatchStrategicMergeしたい!

こんにちは、技術本部研究開発部 Architect グループ ML Platform チームの宮地です。 本記事は Sansan Advent Calendar 2023 の22日目、および【R&D DevOps通信】の連載記事のひとつです。 adventar.org

普段の業務では Circuit という Kubernetes を利用したアプリケーション基盤を作っています。 Circuitは研究開発部の研究成果を最速でリリースすることを目的に開発運用しています。 研究員はアプリケーションの開発と、テンプレートを元にしたマニフェストの記述をするだけで、誰でも簡単にアプリケーションのデプロイできる仕組みを実現しています。

speakerdeck.com

先日、Circuitに Knative Serving *1を利用してサーバーレス(以下、kservice)ワークロードを導入しました。 本ブログではkserviceでKustomize *2をフル活用させるための工夫を紹介します。

発生した問題

既存の運用では、アプリケーションはDeployment,Serviceなどを用いてデプロイしています。また、環境差分を吸収するために Kustomize の PatchStrategicMerge*3を利用しています。Baseに共通の設定を記述し、Overlay に環境差分を記述しています。 新たなkserviceでも、マニフェストを作成する際に、Kustomize の PatchStrategicMerge の利用を考えました。しかし、配列の値がすべて Overlay 側に置き換えられてしまいました。

特に、研究員がアプリケーションのコンテナや環境変数を設定する際に必要となるspec.template.spec.containers.env は、Kustomizeを使用が必要な項目であり、これは大きな問題です。

マニフェストの例(クリックすると展開されます) Baseマニフェスト

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: blog-test
spec:
  template:
    spec:
      containers:
        - name: "blog-test"
          env:
            - name: HOGE_ENV
              value: "hogehoge"
            - name: FUGA_ENV
              value: "fugafuga"

Overlayマニフェスト

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: blog-test
spec:
  template:
    spec:
      containers:
        - name: "blog-test"
          env:
            - name: PIYO_ENV
              value: "piyopiyo"

意図するマニフェスト

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: blog-test
spec:
  template:
    spec:
      containers:
        - name: "blog-test"
          env:
            - name: HOGE_ENV
              value: "hogehoge"
            - name: FUGA_ENV
              value: "fugafuga"
            - name: PIYO_ENV
              value: "piyopiyo"

意図しないマニフェスト

apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: blog-test
spec:
  template:
    spec:
      containers:
        - name: "blog-test"
          env:
            - name: PIYO_ENV
              value: "piyopiyo"

解決方法の検討

この問題を解決する方法は3つ存在します。

  • JsonPatchを利用する方法
  • カスタムリソースの開発元が配布しているカスタムスキーマを利用する方法
  • PatchStrategicMergeの挙動を定義したカスタムスキーマを用意する方法

1つ目は、研究員の認知負荷を上げてしまうため、採用していません。Circuitは、アプリケーションのデプロイのために研究員の方々が操作する既存マニフェストはすべて PatchStrategicMerge 対応です。ここで kservice のみ JsonPatchでは利用体験を損ねると判断したからです。 2つ目は、Knative Servingのカスタムスキーマが配布されていないため採用できませんでした。この手法が利用可能な例として、ArgoCD*4やArgo Workflows*5があります。 よって、基盤側で用意することで研究員全体の利用体験向上に繋がると考え、3つ目を採用しました。次章以降、解決の手順を説明します。

解決手順

KustomizeではKubernetesの標準リソース(Deployment,Service etc)のスキーマ定義をデフォルトで参照しています。そのためDploymentのspec.template.spec.containers[].env[]はnameをkeyに辞書型配列のようにマージしてくれます。 これと同じ定義をカスタムリソースに対して行うことで解決できます。 解決方法を4つのステップに分けて説明します。

1.Kustomizeよりクラスタのスキーマ情報を取得

クラスタのスキーマ情報はkustomizeコマンドで取得できます。

$ kustomize openapi fetch > schema.json

2.該当のスキーマ情報を探す

今回の課題はkserviceのspec.template.spec.containers[].env[]の値がすべてOverlay側に置き換えられてしまうことでした。そのため、dev.knative.serving.v1.Serviceのスキーマを探します。次の記述が該当のスキーマの一部です。該当のスキーマ以外は、今回の目的では不要のため削除してしまいます。

"dev.knative.serving.v1.Service": {
    "description": "Service acts as a top-level container that ...省略..."
    "properties": {
        ~省略~
    }
}

3.動作定義したカスタムスキーマの作成

該当スキーマの中には、spec.template.spec.containers[].env[]の動作定義が記述されています。 PatchStrategicMergeの動作定義を追加していきます。 追加点は"type": "array"と同階層に次の記述を追加します。

 "x-kubernetes-patch-merge-key": "name", # マージのkeyを指定
 "x-kubernetes-patch-strategy": "merge" # パッチの動作を定義

これにより、spec.template.spec.containers[].env[]のnameをkeyに辞書型配列のようにマージしてくれます。*6

以下、変更前と変更後の比較です。(動作定義に不要な記述は削除しています。)

変更前(クリックすると展開されます)

{
  "definitions": {
    "dev.knative.serving.v1.Service": {
      "properties": {
        "spec": {
          "properties": {
            "template": {
              "description": "Template holds the latest specification for the Revision to be stamped out.",
              "properties": {
                "metadata": {
                  "x-kubernetes-preserve-unknown-fields": true
                },
                "spec": {
                  "properties": {
                    "containers": {
                      "items": {
                        "properties": {
                          "env": {
                            "items": {
                              "default": {}
                            },
                            "type": "array"
                          }
                        },
                        "type": "object"
                      },
                      "type": "array"
                    }
                  },
                  "required": [
                    "containers"
                  ],
                  "type": "object"
                }
              },
              "type": "object"
            }
          },
          "type": "object"
        }
      },
      "type": "object",
      "x-kubernetes-group-version-kind": [
        {
          "group": "serving.knative.dev",
          "kind": "Service",
          "version": "v1"
        }
      ]
    }
  }
}

変更後(クリックすると展開されます)

{
  "definitions": {
    "dev.knative.serving.v1.Service": {
      "properties": {
        "spec": {
          "properties": {
            "template": {
              "description": "Template holds the latest specification for the Revision to be stamped out.",
              "properties": {
                "metadata": {
                  "x-kubernetes-preserve-unknown-fields": true
                },
                "spec": {
                  "properties": {
                    "containers": {
                      "items": {
                        "properties": {
                          "env": {
                            "items": {
                              "default": {}
                            },
                            "type": "array",
                            "x-kubernetes-patch-merge-key": "name", # マージのkeyを指定
                            "x-kubernetes-patch-strategy": "merge" # パッチの動作を定義
                          }
                        },
                        "type": "object"
                      },
                      "type": "array",
                      "x-kubernetes-patch-merge-key": "name", # マージのkeyを指定
                      "x-kubernetes-patch-strategy": "merge" # パッチの動作を定義
                    }
                  },
                  "required": [
                    "containers"
                  ],
                  "type": "object"
                }
              },
              "type": "object"
            }
          },
          "type": "object"
        }
      },
      "type": "object",
      "x-kubernetes-group-version-kind": [
        {
          "group": "serving.knative.dev",
          "kind": "Service",
          "version": "v1"
        }
      ]
    }
  }
}

4.kustomizeに読み込ませる

最後に次のようにkustomization.yamlに読み込む設定を書きます。*7

openapi:
    - path: schema.json

これで完了です。

注意点

この手法はさまざまなマニフェストで応用可能です。しかし、注意点もあります。

管理コストが増える

カスタムコントローラーのアップデートに伴い、スキーマの管理が必要となります。 意図したマージを行いたいだけであれば、JsonPatch をするだけで事足りる場合が多くあると考えます。

標準スキーマが読み込まれない

標準スキーマとカスタムスキーマは同時に読み込まれません。つまりカスタムスキーマを読み込ませた場合、標準リソースでKustomizeを適切に扱うことができなくなります。 対策としてカスタムスキーマに標準スキーマも記述は可能ですが、サイズが大きくなるため注意が必要です。管理コストの増加にも繋がります。

おわりに

今回はKustomizeのPatchStrategicMergeの挙動を定義したカスタムを用意し、kserviceのspec.template.spec.containers[].env[]の値がすべてoverlay側に置き換えられてしまう問題を解決しました。 また、この手法はさまざまなマニフェストで応用可能ですが、管理コストが増えるというデメリットがあります。

研究開発部では、MLOps/DevOpsエンジニアを募集しています。

open.talentio.com

© Sansan, Inc.