Sansan株式会社研究開発部ArchitectグループでPlatformエンジニアをしている上田です。
ML PlatformチームではCircuitと呼ばれる研究開発部向けのKubernetes基盤を開発・運用しています。研究員が作ったMLモデルやAPIをリリースするためのKubernetes基盤で、120以上のサービスがこの基盤上で動いています。
今回は、その非本番環境(開発環境とステージング環境)で課題だった、使わないのに起動し続けているワークロードの問題に対して、Slackからオンデマンドで起動・停止できる仕組みを開発した話をします。既存のKEDA+Prometheusのスケーリング機構を生かしつつ、ArgoCDのGitOpsと衝突しない設計に焦点をあてようと思います。
背景
研究開発部のKubernetes基盤 Circuit
研究開発部ではCircuitと呼ばれるEKSベースのアプリケーション基盤を運用しています。ArgoCDによるGitOps、KEDAによるオートスケーリング、Karpenterによるノードプロビジョニングを組み合わせた構成になっています。
ステージング環境だけでバッチを含む140以上のワークロードが存在し、GPUインスタンス(g6e.xlarge、g6.2xlargeなど)を含む約120ノードが稼働しています。
課題: 全サービス一律起動の無駄
従来はKEDAのCron Scalerを使い、平日7:00〜22:00に全サービスを一律起動していました。

しかし、Istioのリクエストログ(`istio_requests_total`)を1カ月分分析したところ、実態は次の通りでした。
| カテゴリ | サービス数 | 割合 | 備考 |
| 完全未使用(月0リクエスト) | 11 | 13% | 1カ月間一度もリクエストなし |
| 月1〜6日しか使われていない | 28 | 33% | 利用日も数時間程度で終了 |
| 月の半分程度(7〜19日) | 22 | 26% | 利用日でも平均4〜5時間程度 |
| ほぼ毎営業日利用(20日以上) | 24 | 28% | それでも夜間・土日は不要 |
全体の46%が月に数日しか使われていませんでした。
特にGPUワークロード(g6e.xlarge: Spot $0.95/h、OnDemand $2.69/h)が平日日中ずっと起動していたため、コストインパクトが大きい状態でした。
解決策
従来のCron Scalerは平日日中に起動しておき、夜間に停止するというアプローチでした。デフォルトが起動状態であり、業務終了しているであろう時間帯だけ止めるという考え方です。
今回採用した思想はこの前提を逆転させています。
デフォルトは停止であり、必要な時だけ起動し、自動で停止する。
起動しっぱなしを構造的に防ぐことで、開発者自身がコストコントロールを取りやすくなります。使う人がいなければコストはほぼゼロです。
コンセプト
使いたいサービスだけ、使いたい時だけ起動する、をシンプルに実現する仕組みです。設計に当たり、次の原則を置きました。
1. 開発者体験を損なわない → GitOps(ArgoCD)を採用しているため、ワークロードの状態変更には本来PRを作成してmainにマージする必要がある。kubectlで直接変更する場合はselfHealを無効にしなければならない。今回の仕組みはこれらの手間なく、Slackで完結する操作体験を提供する。
2. 既存エコシステムに乗る → KEDA+Prometheusのスケーリング機構をそのまま活用し、新しいOperatorは作らない
3. 消し忘れのセーフティネット → 22:00 JSTに自動全停止
4. サービス登録の手間ゼロ → ScaledObjectのtrigger変更だけで自動検出
5. 強い依存をしないようにする → もし利用されなくなった時でもすぐに引き剥がせるようにする
設計
設計に当たり、いくつかの方式を検討しました。
| 選択肢 | 理由 |
| KEDA Prometheus scaler + Workload Controller | ArgoCDと完全共存。Git管理リソースの変更不要 |
| `paused-replicas` annotationを操作 | ScaledObjectをArgoCD管理外にする必要がある |
| ConfigMap + ArgoCD ignoreDifferences | ignoreDifferencesの設定が必要で運用が複雑化 |
| PRでreplicasを変更 | 遅い(マージ待ち)、Revert忘れのリスク |
| 現状維持(Cron Scalerのみ) | 利用者不在でもリソースが起動し続けコスト効率が悪い |
最大の論点はどうやって開発者体験を損なわず、ArgoCDのGitOpsと共存するかでした。
ArgoCDはGitをSingle Source of Truthとして、クラスタの状態をGitに合わせる(selfHeal)仕組みです。Controllerが直接Deploymentのreplicasを変更すると、ArgoCDが即座にGitの値に戻してしまいます。
これを回避する方法として、次の3つが考えられます。
- ignoreDifferencesで特定フィールドを除外する → 設定が煩雑、管理対象が増えるたびに追加が必要
- selfHealを無効にする → GitOpsの恩恵を失う
- Git上のmanifestを変更せずにスケーリングを制御する → 採用
3つ目の方法として、KEDAのPrometheus Scalerを起動ゲートとして活用する設計を採用しました。ControllerはPrometheusメトリクスの値を切り替えるだけで、Git上のScaledObject manifestは固定です。KEDAがメトリクスを参照してスケーリングを行うため、ArgoCDから見ると何も変わっていない状態を維持できます。
全体構成
開発環境・ステージング環境の各クラスタに同一構成でデプロイされます。
Circuitには管理クラスタが存在しないため、管理都合上、異なる環境のクラスタへのアクセスをしないようにしています。そのため環境ごとに個別デプロイしています。

実装の詳細
1. ScaledObjectの自動検出
起動時に全namespaceのScaledObjectをlistし、稼働中はwatchでイベントを監視します。

`scaleTargetRef`から対象リソースのnamespace、名前、種別(DeploymentまたはRollout)を抽出し、インメモリのレジストリに登録します。ScaledObjectが削除された場合はレジストリから除去し、メトリクスも削除します。
2. Prometheusメトリクスによるスケーリング制御
`circuit_workload_activation`はPrometheus Gaugeメトリクスで、ラベルに`env`、`namespace`、`workload`を持ちます。

- ワークロード起動時: 値を`1`に設定 → KEDAがreplicasを1以上にスケール
- ワークロード停止時: 値を`0`に設定 → KEDAがreplicasを0にスケールダウン
KEDAは複数triggerの中で最大のdesired replicasを採用するため、負荷ベースのスケーリング(例: HTTPリクエスト数ベース)と併用できます。
| `circuit_workload_activation` | リクエスト数 | 結果 |
| 0 | — | replica: 0(全トリガーinactive) |
| 1 | 50 req/s | replica: 1 |
| 1 | 300 req/s | replica: 3(負荷ベースが採用される) |
3. Slack BotのModal UI
Slack Appの呼び出しは`/command`にて行います。

実行後、Modalから起動対象のNamespaceを選択し、起動したいワークロードにチェックを入れてPlanします。

変更のプレビューとともに起動するワークロードが確認できるため、問題なければApplyを実行することで対象ワークロードを起動できます。

4. 夜間自動停止
CronJobが毎日22:00 JSTに全停止APIを呼び出します。
ユーザーが起動したものの停止を忘れるケースも想定されます。
特に金曜や長期休暇前などでも確実に停止させることで、余計なコストを削減しています。
導入後の削減効果
コスト削減効果
移行済みサービスの稼働率とコストを計測し、3つの方式で比較しました。
| 方式 | 月間稼働時間/サービス | 稼働率 | コスト比 |
| 常時稼働 | 720h | 100% | 100% |
| 夜間停止Cron(平日7:00〜22:00) | 330h | 44% | 約46% |
| オンデマンド起動(実測) | ~24h | 3.3% | 約3% |
従来330時間/月起動していたワークロードが、実際に必要とされていたのは月24時間程度でした。残りの306時間は誰も使っていないのに起動していた時間だったということになります。
特にGPUワークロード(g6e.xlarge、g6.2xlarge、g4dn.xlargeなど)はPodがノードを専有するため、稼働時間の削減がそのままノードコストの削減に直結します。移行済みのGPU/アクセラレータ系ワークロードではCron時代と比較して約92%の削減を確認しました。
まとめ
非本番環境の、使わないのに起動している問題に対して、KEDAのPrometheus Scalerを起動ゲートとして活用するアプローチで解決しました。
ControllerはPrometheusメトリクスの値を0/1で切り替えるだけで、Git上のmanifestには一切触れません。ArgoCDのGitOpsと衝突せず、サービスオーナーはScaledObjectのtriggerを差し替えるだけで移行が完了します。
移行済みの16サービスではCron Scaler時代と比較して約93%のコスト削減を確認しており、全サービスに展開すればさらに高い削減が見込めます。それ以上に、開発者がSlackから数クリックで環境を制御できるようになったことで、自分たちでコストをコントロールしているという意識が自然に生まれているのを実行ログから観測でき、非常に良い取り組みになったと感じました。
Sansan技術本部ではカジュアル面談を実施しています
Sansan技術本部では中途の方向けにカジュアル面談を実施しています。Sansan技術本部での働き方、仕事の魅力について、現役エンジニアの視点からお話しします。「実際に働く人の話を直接聞きたい」「どんな人が働いているのかを事前に知っておきたい」とお考えの方は、ぜひエントリーをご検討ください。