自動ベストプラクティス追従アプリケーション基盤のManifests生成システム設計

目次

はじめに

自己紹介

Sansan株式会社の研究開発部ArchitectグループTakowasaチームというPlatform EngineerだったりSREだったりMLOps Engineerだったりを駆使し、研究員の開発生産性を向上させるためあらゆることをするチームに所属する加藤 (@discord_tech) と申します。
最近ドハマリしているゴルフの練習でプルプルになった手で頑張ってタイピングをして参りますので、是非、最後まで読んでいっていただければと思います🏌️

今回書くこと

本記事では、最近開発を行った自動ベストプラクティス追従アプリケーション基盤ChassisのPull Request環境、自動E2Eテスト実行、マニフェスト生成のうち、よりCDに近いマニフェスト生成の際に行った工夫とそれに至った思想について話します。

ポジションと取り巻く環境

最初に、弊社研究開発部における開発について説明します。弊社の研究開発部での開発は大きく2つに分けられます。

1つ目がデータ化です。
弊社のサービス、Sansanでは名刺を、Bill Oneでは請求書を、Contract Oneでは契約書をデータ化します。その際にデータ化に伴い行われる様々な工程を細分化し、自動、手動の混在したGeesと呼ばれるシステムを構築しており、その中でエンジニアだけでは解けない問題を伴う自動化システムの開発を行っています。 (Geesについて気になった方はこちらの記事をご参照ください)

2つ目がデータ利活用です。
Sansanの1機能であるSansan Labsでは、Sansanに登録されている様々なデータを活用し、営業のアプローチ先の選定を支援したりするような先進的な機能を提供しています。その中で利用される技術は古典的な統計学から機械学習、LLMなど目的に合わせて様々な手段を駆使しており、それらのシステムの開発を行っています。 (Sansan Labsについて気になった方はこちらの記事をご参照ください)

特に、Sansan Labsの開発チームでは年間100アプリケーションのリリースを目標としており、このハイペースなリリースをサポートするため、研究開発部ArchitectグループではCircuitと呼ばれるKubernetes基盤を運用し、プロダクトとしてのプラットフォームの提供を念頭に開発生産性の向上を目指して日々改善を加えています。 (Circuitでのプロダクトとしてのプラットフォーム提供について気になった方はこちらの資料こちらの資料もご覧いただくと面白いと思います)

現状のManifests生成

次に現状のCircuitでの開発者体験と3年間運用してきた中で見えてきた課題について説明します。開発者は下記のフローで開発を行います。

graph TD
  A[(リポジトリを作成)] --> B[cookiecutterを使用してAppsのテンプレを生成]
  B --> C[Appsを開発]
  C --> D[cookiecutterを使用してManifestsのテンプレを生成]
  D --> E[Manifestsの中身を編集]
  E --> F[ManifestsをCircuitのリポジトリにPR]
  F --> G[AppsのリポジトリのActionsでECR Push]
  G --> H[(内製Image UpdaterがPRを作成)]
  H --> I[PRをマージでリリース]

上記のフローにより、当初は年間1リリース程度だったところから年間10リリース程度と10xのスケールを達成することが出来ました。しかし、それと同時に管理すべきアプリケーションの数もスケールし、プラットフォームチームによって追加された新機能やKubernetes自体のアップデートに伴う進化についていけないアプリケーションが出てきてしまいました。

Chassisの紹介

前述の基盤の進化に伴うベストプラクティスの更新についていけないアプリケーションを救うべく私達はアプリケーション基盤Chassisを開発しました。以下、概要について説明しますが、詳細については現在プロポーザル募集中のSRE Kaigi 2025に提出しているので、気になりましたら、投票いただければと思います!

Chassisには大きく3つの機能があります。

1つ目がPull Request環境です。
これはよく、Ephemeral Environmentと呼ばれるもので、開発途中のシステムの挙動をStaging環境や本番環境にて確認をすることの出来る機能です。ただし、開発途中におけるチェックには様々なパターン (例えば、APIは現行のシステムを使用し、フロントはPRのシステムを使用したい場合もあれば、両方PRのシステムを使用したい場合もある) があり、各設定に対応できるような工夫がされています。

2つ目が自動E2Eテストになります。
この機能ではアプリケーション開発者があらかじめ作成したE2Eテストを前述のPull Request環境に適用する機能です。この際、テスト実行の際に必要な下準備 (例えば、経路を通すためのネットワークの認可等) を当機能によって肩代わりし、E2Eテストを行う際のハードルを下げ、その上でCIにE2Eテストの成功を組み込むことによって次項で解説するベストプラクティスの拡散の工程で設定に変更が入っても動作することを保証します。

Manifests生成機能

3つ目がManifests生成機能です。
今日の主役なので別項目でじっくり説明します。

Chassisにて開発する際、chassis_config.tomlという設定ファイルを開発者は書きます。設定ファイルは下記のような内容となっており、アプリケーション開発において必要な設定を書き込むことができます。開発者がPythonでの開発を主としているため、pyproject.tomlで慣れ親しんでいるtomlを採用しています。

# 固定したいバージョン default = "latest"
fixed_version = "latest"
# 設定ファイルを配置しているリポジトリ required
repository = "eightcard/randd_chassisctl"
# 設定ファイルのターゲットするNamespace required
namespace = "samples"

# 全application用のe2e設定を書く
[[e2e]]
# testの名前、e2e/{test-name}
name = "onboarding-test"
exclude_targets = ["target_application_name"]

[[applications]]
# アプリケーションの名前 required
name = "ApplicationName"
# アプリケーションのディレクトリ、e2e, manifestsのフォルダが置かれる default = "/"
directory = "/"
# サービスのタイプ default = kservice
# kservice or deployment
# kservice: Kservice
# deployment: Deployment, Service, HPA, VirtualService, Gateway
service_type = "deployment"
# URLの設計を示す host or path default = "host
# この設定を行うと
# host: https://application_name.rd.ds.sansan.com/
# path: https://base_url.rd.ds.sansan.com/application_name/
url_mode = "host"
# url_mode = "path"のときにbase_urlを定める
base_url = "application_name"

# secretを環境変数に渡す
# Secretの登録は依頼をする
# 将来的には1passwordに対応する予定です
[[applications.secrets]]
name = "secret_name"
env_name = "ENV_NAME"

# Pull Request Environmentの設定を書く
[applications.pr]
# PR EnvのON/OFF default = false
enabled = true
# PR Envのターゲット環境 default = ["development", "staging", "production"]
environments = ["development", "staging", "production"]

# このセクションではPR環境のリソースの上書きについての設定を書く
# ベースのパッチ以外が必要ない場合は空で作成する ([applications.pr.used_resources.deployment]のみ)
# 手書きのmanifestが無くなると必要が無くなる
# 現状、Deployment, Ingress, Service, Service(Knative Serving), VirtualService, Gatewayが対応している
[applications.pr.used_resources.deployment]
# deploymentへのPatchStrategicMergeを書く
manual = """
test
"""

[[applicastions.pr.used_resources.ingress.patches]]
# manifestへのPatchを書く
patch = """
are
"""

# Auto E2E Executorの設定を書く
[applications.e2e]
enabled = true

[[applications.e2e.directories]]
directory = "base"
actions = true
deploy = true

[applications.e2e.directories.cron]
cron_config = "0 0 * * *"

...

この設定ファイルより、Chassisの根幹を担うchassisctlを使用してchassisctl buildというコマンドを実行すると、例えばDeploymentモードの場合、下記のファイルを生成します。

  • authorization-policy.yaml
  • deployment.yaml
  • gateway.yaml
  • horizontal-pod-autoscaler.yaml
  • pod-disruption-budget.yaml
  • service.yaml
  • virtual-service.yaml

ここまでは通常のテンプレートエンジンと似た挙動をします。しかし、ここからCircuitが進化する際にChassisの本領が発揮されます。

例えば、今までEKSのIngress Controllerを使用してアプリケーションをデプロイしていた基盤にIstioを導入し、今までALBを複数作成していた環境からロードバランサーの機能をKubernetes側で管理するように変更をしたとします。 (実際にあったシチュエーションです)

そうなった場合、Takowasaチームではchassisctl buildによって作成されるManifestsがIstioを使用したものになるように変更の開発を行います。そして、chassisctlのリポジトリでPRを作成すると、chassisctlのリポジトリのCIにてChassisに乗っかっている全てのアプリケーションのリポジトリへ新しい設定のPRが作成されます。そしてそのPRの中のCIにて前述の2機能、Pull Request環境、自動E2Eテストが実行され、新しい設定の動作が保証され、安全に全アプリケーションへベストプラクティスを拡散することが出来ます。私達はこの挙動を連鎖CIと呼んでいます。

設計思想

ここからが当記事の本題になります。

プラットフォームの更新には攻めの更新と守りの更新があります。 前者は開発者が必要性に気づかないような改善を行う更新を指していて、それに対して後者は開発者が必要としている改善を行う更新を指します。共通プラットフォーム化の良い点は守りの更新をする毎にプラットフォームの機能が増え、工数に余裕ができ、攻めの更新の比重を多く置くことができるようになるという点にあります。 そこで、Chassisでは守りの更新を常にプラットフォームへの機能吸収を前提として行うことを大切にしています。

設計において考えたこと

前述したテンプレートエンジンを使用した現在のManifests生成システムには3段階の問題点があります。

1つ目が一度生成すると再生成が出来ないという点です。 テンプレートエンジンを使用する場合、テンプレートを用いた初回の生成がされた後、テンプレートが更新されても既に生成されたManifestsを変更することは出来ないため、既存のアプリケーションの更新のコストがアプリケーション数のスケールと共に線形に増加してしまいます。 そこで、既存のアプリケーションを更新し続けるには、一度生成されたManifestsを新しいバージョンのテンプレートで再生成し、最新の設定を反映する仕組みを用意する必要があります。

次に、テンプレートの再生成ができるようになった後の世界における、開発者による変更差分とテンプレート更新に伴う変更差分の見分けがつかないという問題が発生します。 例えば、前述したIngressを使用したルーティングからIstioを使用したルーティングへの変更をしようとしているシチュエーションを想定します。あるアプリケーションではUDPを使用しており、Ingressの設定をNLBを作成するように手動で変更していたとします。その場合、テンプレートの更新によってIngressが削除されてしまうため、アプリケーションが動作しなくなってしまいます。

このようなシチュエーションを解決するにはIngressの生成の項目でhelmでよくされるcreate = trueのような設定を追加し、Ingress → Istioのテンプレートエンジン更新の際に前述の連鎖CIを行い、UDP通信の遮断による動作不良を検知。テンプレートエンジン更新者がCIが失敗しているアプリケーションをチェックし、テンプレートエンジンの更新と共に変更が必要なアプリケーションの設定を同時に更新するということをする必要があります。この作業はアプリケーション数のスケールとともにコストが重くなってしまいます。

実際の設計

上記の3段階の問題点を解決するため、Chassisでの実装の工夫とTakowasaチームの立ち回り方に工夫をしました。

まず、Chassis側での工夫から説明します。 現状のManifestsは下記の様なディレクトリ構成になっており、各環境のoverlaysのkustomization.yamlがbuildされデプロイされます。

├── base/
│   ├── authorization-policy.yaml
│   ├── deployment.yaml
│   ├── gateway.yaml
│   ├── horizontal-pod-autoscaler.yaml
│   ├── kustomization.yaml
│   ├── pod-disruption-budget.yaml
│   ├── service.yaml
│   └── virtual-service.yaml
└── overlays/
    └── environment-name/
        ├── authorization-policy.yaml
        ├── deployment.yaml
        ├── gateway.yaml
        ├── horizontal-pod-autoscaler.yaml
        ├── kustomization.yaml
        └── virtual-service.yaml

しかし、このままでは生成されたManifestsと開発者が変更したManifestsが見分けられなかったため、下記の様なディレクトリ構成としました。

├── auto/
│   ├── base/
│   │   ├── authorization-policy.yaml
│   │   ├── deployment.yaml
│   │   ├── gateway.yaml
│   │   ├── horizontal-pod-autoscaler.yaml
│   │   ├── kustomization.yaml
│   │   ├── pod-disruption-budget.yaml
│   │   ├── service.yaml
│   │   └── virtual-service.yaml
│   └── overlays/
│       └── environment-name/
│           ├── authorization-policy.yaml
│           ├── deployment.yaml
│           ├── gateway.yaml
│           ├── horizontal-pod-autoscaler.yaml
│           ├── kustomization.yaml
│           ├── pod-disruption-budget.yaml
│           ├── service.yaml
│           └── virtual-service.yaml
└── manual/
    ├── base/
    │   └── kustomization.yaml
    └── overlays/
        └── environment-name/
            └── kustomization.yaml

このようにし、manualフォルダ以下の該当環境のkustomization.yamlをbuildすることによって同様のことを開発者による変更を明示した上で行えます。

次にTakowasaチームの運用における立ち回りの工夫です。 守りの更新がmanualディレクトリ以下に隔離されるようになったため、このディレクトリを見れば共通化されていない設定が明確に見えるようになりました。そこでTakowasaチームではCircuit側の機能不足等の実現不可能である場合を除いて基本的にmanualフォルダに設定を書かなくて済むようにchassisctlを更新し続けるように運用を行っています。 これにより、同じ守りの更新を繰り返す必要が無くなり、攻めの更新へ割く時間を多く取ることが出来るようになります。

開発者の体験

開発者目線では下記のフローで開発することになります。

graph TD
  A[(リポジトリを作成)] --> B[(chassisctl initでリポジトリの初期設定を行う)]
  B --> C[chassis_config.tomlを編集]
  C --> D[chassisctl build]
  D --> E[PRを作成]
  E --> F[リリースPRが作成される]
  F --> G[mergeでリリース]

おわりに

Chassisによってアプリケーションの更新を自動化したことで、プラットフォームの進化による成果をアプリケーションに簡単に反映することが出来るようになりました。 Takowasaチームはアプリケーションの開発を効率化するため、攻めの更新を積極的にしていく方針で、下記の機能の開発をロードマップとして持っています。

  • batchのe2eテスト(結果が変わらないことを確認)
  • e2eテストの結果をリッチに表示
  • Metrics, Logs, Tracesの収集自動化
  • リソースの自動設定
  • LLMによるエラー分析
  • カナリアリリース設定の自動化
  • リリース承認フローの自動化

また、研究開発部ではKubernetesプラットフォーム側、アプリケーション側共に更新をしていく仲間を募集しています。 まずはカジュアル面談も可能ですので、気軽にお問い合わせください。よろしくお願いいたします!