Kube-green:業務時間外にKubernetesワークロードを自動停止してDev/Stagingのクラウドコストを削減する

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

毎月のAWS請求書が届いたとき、EKS devクラスターの請求額を見て思わず手が止まった。開発者が実際に使うのは1日10時間、週5日程度なのに、月額847ドル。実際の使用時間は50時間なのに、718時間分の料金を支払っていることになる。

アイドル状態のコンピュートに1日50ドルというのは合理化しがたい。誰も触れない午後7時以降や週末でも、devとstagingのワークロードは24時間365日稼働し続けていた。ノードは起動したまま、Podは動き続け、AWSは課金し続けた。気づかないうちに積み重なるクラウドの無駄遣い——そして、たった1つのYAMLファイルでほとんど解決できる類の問題だ。

現実の問題:Kubernetesは眠らない

私たちのstagingクラスターには2つのnamespace全体で14のDeploymentと3つのStatefulSetがあった。典型的な火曜日の午後11時、全Podを通じてCPU使用率は2%未満。すべてのプロセスはアイドル状態で、翌朝8時まで来ないであろう負荷をひたすら待っていた。しかしノードにはそれが分からない。Kubernetesのノードはそういうもので、ただ動き続けるだけだ。

夜間だけでなく、週末はさらに深刻だった。48時間もの生産性ゼロのコンピュートが、毎週繰り返される。週5日、午前8時から午後8時までを稼働時間とすると、実際に役立っているのはワークロードの稼働時間のおよそ35%に過ぎない。

根本原因:Kubernetesは高可用性のために設計されており、コスト最適化のためではない

Kubernetesに何かをデプロイすると、明示的に停止するまで動き続ける。「営業時間」スケジューリングというネイティブの概念は存在しない。プラットフォームはワークロードを常時利用可能にしたいという前提で設計されている——本番環境では完全に理にかなっているが、devやstaging環境では不要な無駄を生み出す。

Horizontal Pod Autoscaler(HPA)はCPUとメモリのメトリクスに基づいてスケールする。トラフィックの急増には対応できるが、HPAは最小レプリカ数を1に強制する——ワークロードをゼロにスケールすることはまったくできない。これはアーキテクチャ上のハードな制限であり、設定の問題ではない。真のゼロを実現するには、KEDAかkube-greenのようなスケジュールベースのツールが必要だ。

devとstagingクラスターは、日曜日の午前3時に誰も見ていなくても、本番環境の常時稼働動作を引き継ぐ。プラットフォームの中にそれを抑制するものは何もない。

Kube-greenに落ち着く前に比較したソリューション

手動のkubectlスケーリング

チームは毎晩退勤前にkubectl scale deployment --replicas=0を実行することで合意した。これは約2週間機能した。しかしその後、人々は忘れ始め、あるいは「念のため」と言ってdeploymentを1レプリカのままにしておくようになった。人間のプロセスは開発者のコンテキストスイッチには耐えられない。

カスタムBash CronJob

特定のnamespace内のすべてのDeploymentを午後8時にゼロにスケールし、午前8時に戻すKubernetes CronJobを書いた。機能した——しばらくは。スクリプトはどこにも元のレプリカ数を保存していなかったため、すべてが1レプリカで起動した。ヘルスチェックに3つ以上のレプリカが必要なサービスが失敗し始め、アプリケーションのクラッシュのように見えるものをデバッグするのに午前中を費やしたが、実際はレプリカが不足しているだけだった。

KEDA(Kubernetes Event-Driven Autoscaler)

KEDAは外部イベントソースに基づいてワークロードをゼロにスケールできる。キュー駆動サービスには優れているが、夜間に単純に停止するためだけに使うのは大げさすぎる。すべてのDeploymentに独自のScaledObjectが必要になり、それをnamespace全体で管理することは、元の問題よりも多くの運用オーバーヘッドを生み出した。

Kube-green

スケジュールベースのサスペンションのために専用に構築されている。namespaceごとに1つのCRD、いつ停止していつ起動するかを定義すれば完了。ゼロにスケールする前に元のレプリカ数をアノテーションとして保存し、起動時に正確に復元する。devとstaging全体でこれを6ヶ月間稼働させた——ファントムリスタートゼロ、起動漏れゼロ、stagingが戻ってこないせいでオンコールページがかかることゼロ。結果は一貫して安定している。

Kube-greenのセットアップ:実践ガイド

Helmによるインストール

helm repo add kube-green https://kube-green.dev/helm-charts
helm repo update
helm install kube-green kube-green/kube-green \
  --namespace kube-green \
  --create-namespace

オペレーターが稼働していることを確認する:

kubectl get pods -n kube-green
# NAME                          READY   STATUS    RESTARTS   AGE
# kube-green-xxxxxxxxxx-xxxxx   1/1     Running   0          2m

SleepInfoリソース

これがkube-greenの設定の全体像です——namespaceごとに1つのYAMLファイルだけ:

apiVersion: kube-green.com/v1alpha1
kind: SleepInfo
metadata:
  name: dev-sleep-schedule
  namespace: development
spec:
  weekdays: "1-5"          # 月曜日から金曜日
  sleepAt: "20:00"         # 午後8時にスリープ
  wakeUpAt: "08:00"        # 午前8時に起動
  timeZone: "Asia/Tokyo"
  suspendDeployments: true
  suspendStatefulSets: true
  suspendCronJobs: true
kubectl apply -f dev-sleep-schedule.yaml

kube-greenはゼロにスケールする前に、各リソースのアノテーションに元のレプリカ数を書き込む。起動時にはそのアノテーションを読み込み、以前と同じ状態を正確に復元する。設定ドリフトも推測もない。

異なる時間と除外設定のあるStaging

Stagingは通常、QA実行のためにより遅くまで稼働している。別のスケジュールを設定し、特定のワークロードをサスペンションから除外することができる:

apiVersion: kube-green.com/v1alpha1
kind: SleepInfo
metadata:
  name: staging-sleep-schedule
  namespace: staging
spec:
  weekdays: "1-5"
  sleepAt: "23:00"           # QAテストのために遅めに設定
  wakeUpAt: "07:30"          # 朝のスモークテストのために早めに
  timeZone: "Asia/Tokyo"
  suspendDeployments: true
  suspendStatefulSets: false  # データベースは稼働し続ける
  excludeRef:
    - apiVersion: apps/v1
      kind: Deployment
      name: monitoring-agent  # 常に稼働させておく

監視エージェントや外部ヘルスチェックが依存するサービスにはexcludeRefを使用する。kube-greenはサスペンションサイクル中、それらを完全にスキップする。

ステータスの確認とデバッグ

# 現在のスリープ状態を確認
kubectl get sleepinfo -n development
# NAME                 SLEEP AT   WAKE UP AT   WEEKDAYS   TIMEZONE      STATUS
# dev-sleep-schedule   20:00      08:00        1-5        Asia/Tokyo    Sleeping

# サスペンド中のDeploymentに保存されたレプリカ数を確認
kubectl get deployments -n development -o jsonpath=\
  '{range .items[*]}{.metadata.name}: replicas={.metadata.annotations.sleepinfo\.kube-green\.dev/replicas}{"\n"}{end}'

必要なときの手動オーバーライド

土曜日に開発者が環境を起動させる必要があるときは、deploymentを直接スケールするだけだ:

kubectl scale deployment my-app --replicas=2 -n development

kube-greenは邪魔しない。スリープ時に現在のレプリカ数を読み込んで保存する。次のスケジュールされた起動時には、手動クリーンアップなしで通常の操作が再開される。

Kube-greenがカバーしないこと

kube-greenはワークロードをサスペンドする——インフラを直接管理するわけではない。マネージドKubernetes(EKS、GKE、AKS)では、コントロールプレーンは常時稼働する。ノードコストの削減は、Podがなくなったときにクラスターオートスケーラーが実際にノードを削除する場合にのみ実現する。ノードグループがスケールトゥゼロをサポートしていることを確認しよう:

# EKSノードグループの最小サイズを確認
aws eks describe-nodegroup \
  --cluster-name my-cluster \
  --nodegroup-name dev-workers \
  --query 'nodegroup.scalingConfig'
# minSize: 0 または 1 と表示されるはず

また、PersistentVolumeのコストはPodがスリープしても止まらない。EBSボリュームは何かがアタッチされているかどうかに関わらず時間単位で課金される。本当に低コストなdev環境を目指すなら、重要でないデータにはエフェメラルストレージを検討するか、ストレージコストは放置する価値のある小さな項目として受け入れよう。

6ヶ月後の実際の数字

kube-green導入前、devクラスターは平均8ノードが24時間365日稼働していた。導入後——Podがなくなるとクラスターオートスケーラーがノードを削除する——業務時間外は通常1〜2ノードに減少した。月額請求は約847ドルから約290ドルに下がった。Stagingも同様の結果で、両環境全体でおよそ65%の削減となった。

運用コストはほぼゼロだった。最初の30分のセットアップと1週間の起動時間の監視を経て、システムは一切の介入なしに稼働し続けている。開発者がスケジュールに気づくことはほとんどない——朝の起動タイミングが適切で、人々が最初のコーヒーを飲み終える前に環境が準備できているからだ。

より興味深い副作用は文化的なものだった。以前は、devリソースが特定の規模で必要かどうかを誰も考えていなかった。今では、コストの可視化がライトサイジングについての議論を呼んでいる:なぜdevのDeploymentに3つのレプリカが必要なのか?stagingテストが負荷をかけることもないのに、なぜそのStatefulSetは4Giのメモリに設定されているのか?kube-greenは、課金ダッシュボードだけでは決して実現できなかった形でリソースの所有権を可視化した。

devまたはstagingクラスターがサスペンション戦略なしで24時間稼働しているなら、これが意味のある節約への最速の道だ——30分のセットアップで、月額557ドルが毎月戻ってくる。CRDはシンプルで、オペレーターは軽量で、その節約は毎週末また積み重なっていく。

Share: