【k8s】 EKSにKnativeを導入したゼロスケールの実現
はじめに
Kubernetes運用において、利用状況に応じたスケールイン・アウトは様々な方法で実現できますが、
ゼロスケール(つまり使っていないPodは0個にする)を実現するためには、
標準のHorizontal Pod Autoscaler (HPA) だけではいくつかの課題があり、追加の仕組みが必要です。
# minReplicas: 0にした場合
The HorizontalPodAutoscaler "app-hpa" is invalid:
* spec.minReplicas: Invalid value: 0: must be greater than or equal to 1
* spec.metrics: Forbidden: must specify at least one Object or External metric to support scaling to zero replicas
この「ゼロスケール」は、リクエスト駆動で動作するAPIサーバーや、
開発環境など、常時リクエストがあるわけではないワークロードにおいて、
コンピューティングリソースとコストを劇的に削減を期待できます。
ゼロスケールを実現する代表的なオープンソースのソリューションとして、KEDAが有名ですが、
今回はサーバーレスワークロードの実行基盤であるKnativeを使ってみました。
本記事では、Knative Servingコンポーネントに焦点を当て、AWSのマネージドKubernetesサービスであるEKS上に導入してみた手順や試行錯誤について記載いたします。
前提条件
今回は、以下の状況で導入した内容を記載します。
- AWS EKSクラスターが作成済の環境に導入
- 導入したEKSクラスターのバージョンはv1.33
- 今回外部アドレスを利用していないため、ネットワーク周りの詳細は割愛してます。
Knative Servingとは?
まずはKnative ServingをEKSに導入していくのですが、
そもそもKnative Servingとはどんな役割をするのでしょうか?
それは主にアプリケーションを「サーバーレス」的に動かすことにあります。
アプリケーションのコード(コンテナ)を用意するだけで、
それ以外の面倒な部分(インフラ管理、スケール調整など)を自動的にやってくれるということです。
-
ゼロへのオートスケール(自動的な拡大・縮小)
アクセスが無い時は、 アプリケーションを実行するサーバー(Pod)をゼロに減らします。
これにより、コストを節約できます。
アクセスが来ると自動的にサーバーを起動(スケーリング)してリクエストを処理します。もちろんアクセスが多いときにはアクセス量に応じて、自動でサーバーの数を増やし(スケールアウト)、負荷に対応します。 -
トラフィック管理とデプロイの自動化
アプリケーションの新しいバージョンを公開する際の複雑な作業を自動化します。
アプリケーションのコードや設定を変更するたびに、
その時点の状態が「リビジョン」として記録されます。
新しいリビジョンをデプロイする際、すぐに全ユーザーに公開するのではなく、
「新しいリビジョンに10%だけアクセスを振り分けて試す」といった、
安全なリリース戦略(トラフィック分割)を簡単に設定できます。
ちなみに本記事ではこの機能をほぼ無効化状態にして、単純にゼロスケールの実現にのみ利用しています。
では、早速Knative ServingをEKSに導入していきます。
Knative Servingの導入手順
今回Knative Servingのinstallするバージョンはv1.19.0です。
公式ドキュメントはこちらが参考になります。
はじめにCRDをインストールします。
# install CRDs
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.19.0/serving-crds.yaml
# applyされたcomponentの確認
% kubectl get customresourcedefinitions.apiextensions.k8s.io -A | grep knative
certificates.networking.internal.knative.dev 2025-11-04T07:58:46Z
clusterdomainclaims.networking.internal.knative.dev 2025-11-04T07:58:47Z
configurations.serving.knative.dev 2025-11-04T07:58:46Z
domainmappings.serving.knative.dev 2025-11-04T07:58:47Z
images.caching.internal.knative.dev 2025-11-04T07:58:48Z
ingresses.networking.internal.knative.dev 2025-11-04T07:58:47Z
metrics.autoscaling.internal.knative.dev 2025-11-04T07:58:47Z
podautoscalers.autoscaling.internal.knative.dev 2025-11-04T07:58:47Z
revisions.serving.knative.dev 2025-11-04T07:58:47Z
routes.serving.knative.dev 2025-11-04T07:58:47Z
serverlessservices.networking.internal.knative.dev 2025-11-04T07:58:47Z
services.serving.knative.dev 2025-11-04T07:58:47Z
続いてコアコンポーネントをインストールします。
# install core components
kubectl apply -f https://github.com/knative/serving/releases/download/knative-v1.19.0/serving-core.yaml
namespace/knative-serving created
...
# applyされたリソースの確認
% kubectl get pods -n knative-serving
NAME READY STATUS RESTARTS AGE
activator-76fc47ccbc-xdmwn 1/1 Running 0 45s
autoscaler-fd75468cb-7vgct 1/1 Running 0 45s
controller-86cf6454bb-5vbn4 1/1 Running 0 45s
webhook-6688f4d78d-xs4q9 1/1 Running 0 45s
そして最後にネットワークレイヤー(Kourier)のインストールを行います。
# install Kourier networking layer
kubectl apply -f https://github.com/knative/net-kourier/releases/download/knative-v1.19.0/kourier.yaml
namespace/kourier-system created
...
# applyされたリソースの確認
% kubectl get pods -n knative-serving | grep kourier
NAME READY STATUS RESTARTS AGE
net-kourier-controller-57777c4589-d297z 1/1 Running 0 13s
% kubectl get all -n kourier-system
NAME READY STATUS RESTARTS AGE
pod/3scale-kourier-gateway-78966c4b94-26tqd 1/1 Running 0 68s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/kourier LoadBalancer 172.20.71.66 <pending> 80:30184/TCP,443:31489/TCP 68s
service/kourier-internal ClusterIP 172.20.218.71 <none> 80/TCP,443/TCP 68s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/3scale-kourier-gateway 1/1 1 1 68s
NAME DESIRED CURRENT READY AGE
replicaset.apps/3scale-kourier-gateway-78966c4b94 1 1 1 69s
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
horizontalpodautoscaler.autoscaling/3scale-kourier-gateway Deployment/3scale-kourier-gateway cpu: <unknown>/100% 1 10 1 68s
Knative用のサンプルアプリケーションのデプロイ
Knative Servingの導入が完了したら、次にKnative用のマニフェストを用意します。
ここではまずチュートリアルにある簡単なHello Worldアプリケーションを用意します。
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
name: hello
namespace: test
annotations:
networking.knative.dev/ingress-class: kourier.ingress.networking.knative.dev
spec:
template:
spec:
containers:
- image: ghcr.io/knative/helloworld-go:latest
ports:
- containerPort: 8080
env:
- name: TARGET
value: "World"
こちらをデプロイすると、以下のようなPodのログが確認出来ました。
2025/11/12 05:32:53 helloworld: starting server...
2025/11/12 05:32:53 helloworld: listening on port 8080
今度はクラスタ内部から以下のcurlを実行してみましょう。
curl hello.test.svc.cluster.local
もしクラスタ内部にcurlを実行できるPodを用意していない場合は、以下のように一時的なPodを起動して試すことができます。
実行すると、Hello World!が返ってきました。
% kubectl run -it --rm debug --image=curlimages/curl --restart=Never -n test -- sh
If you don't see a command prompt, try pressing enter.
~ $ curl http://hello.test.svc.cluster.local
Hello World!
~ $ exit
pod "debug" deleted
ここでデプロイされたリビジョンを確認してみます。
以下のように、hello-00001というリビジョンが作成されていることがわかります。
% kubectl get revision -n test
NAME CONFIG NAME GENERATION READY REASON ACTUAL REPLICAS DESIRED REPLICAS
hello-00001 hello 1 True 1 1
ここで例えば、環境変数TARGETの値を変更してみます。
env:
- name: TARGET
value: "Zenn"
同様にクラスタ内部からcurlを実行してみると、以下のように変更が反映されていることが確認できます。
# curl hello.test.svc.cluster.local
Hello Zenn!
さらに2分程度放置すると、Podがゼロスケールされていることが確認できます。(REPLICASが0になっている)
% k get revision -n test
NAME CONFIG NAME GENERATION READY REASON ACTUAL REPLICAS DESIRED REPLICAS
hello-00002 hello 2 True 0 0
hello-00001 hello 1 False 0 0
Knativeの設定を変更する
さて、ここまで基本的なKnativeの動作を確認したところで、
個人的にちょっと気になった点、はまった点とその対処方法について記載します。
古いrevisionのCrashしたPodが残る
Knativeは先述した通り、複数のrevisionをリリース戦略としてトラフィックを分割できる機能がありますが、古いrevisionのPodがCrashした場合、そのPodが残り続けることがあります。
この時、新しい正常なrevisionのPodがゼロスケールしてしまうと、kubectl get podしたときにCrashしたPodしか表示されなくなり、正常に動作しているのか分かりづらくなります。
Knativeを使わないDeploymentの更新なら、
新しいPodが正常に起動すれば古いPodは削除されますよね。
これと同じ感覚で利用するために、あえてrevisionは1個だけにする、という設定変更が必要です。
ではその設定はどこにあるかというと、namespace/knative-servingのconfigmap/config-gcに定義されています。
デフォルトでは最低20個は保持されるようになってましたので、これをpatchコマンドで変更します。
公式Githubのドキュメントにその設定項目が記載されていました。
kubectl patch configmap/config-gc \
--namespace knative-serving \
--type merge \
--patch '{"data":{
"retain-since-create-time": "disabled",
"retain-since-last-active-time": "disabled",
"min-non-active-revisions": "0",
"max-non-active-revisions": "0"
}}'
-
retain-since-create-time: リビジョンがガベージコレクションの対象となる前に、リビジョンが作成されてから経過する必要がある時間 -
retain-since-last-active-time: リビジョンがガベージコレクションの対象となる前に、リビジョンが最後にアクティブになってから経過する必要がある時間 -
min-non-active-revisions: 保持する非アクティブなリビジョンの最小数 -
max-non-active-revisions: 保持する非アクティブなリビジョンの最大数
このpatchを設定直後に古いrevisionが消えるわけではありません。
次回何かしらのrevisionが作成されたタイミングで、同名の古いrevisionがクリーンアップされます。
nodeSelectorがデフォルトで無効
使用しているEKSクラスターではKarpenterによるノード管理を行っているのですが、
何も知らずにnodeSelectorを指定してKnativeのリソースを上げようと思った所、
バリデーションエラーで弾かれてしまいました。
また、nodeSelectorがダメなら...とAffinityを指定しても同様に弾かれてしまいました。
「おかしいな?」と思って調べてみたところ、
こちらもKnativeではデフォルトで無効になっていることが分かり、設定を変更する必要がありました。
この設定はどこにあるかというと、namespace/knative-servingのconfigmap/config-featuresで定義されていました。
kubectl patch configmap/config-features \
--namespace knative-serving \
--type merge \
--patch '{"data":{"kubernetes.podspec-affinity":"enabled","kubernetes.podspec-nodeselector":"enabled"}}'
これでnodeSelectorやAffinityを指定したKnativeリソースをデプロイできるようになります。
既存のDeploymentをKnative Serviceに変換する
では最後に、Knativeのマニフェストはどのように作成すれば良いのかを記載します。
まずは以下の公式ドキュメントに簡単なnginxのDeployment、およびServiceがKnativeだとどう書くかが記載されているので、そちらをご確認ください。
大きな特徴は、Knativeのマニフェストは基本kind: Serviceひとつにまとめられるという点です。
apiVersion: serving.knative.dev/v1
kind: Service
あとはゼロスケールに関する設定をannotationsで指定することで、細かいスケーリングの挙動を制御できます。
spec:
template:
metadata:
annotations:
autoscaling.knative.dev/min-scale: "0"
autoscaling.knative.dev/max-scale: "1"
autoscaling.knative.dev/window: "60s"
autoscaling.knative.dev/scale-down-delay: "10s"
autoscaling.knative.dev/scale-to-zero-pod-retention-period: "60s"
-
autoscaling.knative.dev/min-scale: リビジョンが持つべきレプリカの最小数。デフォルトはenable-scale-to-zeroが有効なら0、そうでなければ1。
※namespace/knative-servingのconfigmap/config-autoscalerにあるenable-scale-to-zeroの設定が確認出来ます。 -
autoscaling.knative.dev/max-scale: リビジョンが持つべきレプリカの最大数。デフォルトは0(無制限)。 -
autoscaling.knative.dev/window: スケールダウンするか判断するためのメトリクス(リクエスト数、同時接続数など)を収集・平均化する期間。設定は6sから1hの間。デフォルトは60s。 -
autoscaling.knative.dev/scale-down-delay: スケールダウンの決定を適用する前に、同時実行数を減らした状態で経過しなければならない時間枠を指定する。設定は0sから1hの間。デフォルトは0s。 -
autoscaling.knative.dev/scale-to-zero-pod-retention-period: オートスケーラーがポッドをゼロにスケーリングすることを決定した後、最後のポッドがアクティブなままになる最小時間。負でない期間文字列が指定できる("1m5s"など)。デフォルトは0s。
例えば、上記のアノテーションの設定なら、トラフィックが無いと以下の計算の時間くらい経過したタイミングでPodがゼロスケールされます。
10秒 (scale-down-delay)
+ 60秒 (stable-window のデフォルト)
+ 60秒 (scale-to-zero-pod-retention-period)
= 約130秒 (約2分10秒)
Podがすぐ起動するimageなら良いですが、起動に時間がかかるimageの場合はすぐにゼロスケールされても困る、といったケースもきっとあるでしょう。
これらのアノテーションを活用して、適切な設定を模索するのが良さそうです。
おわりに
いかがでしたでしょうか?
意外に執筆時点でKnativeに関する記事が少なかったので、今回記載に至りました。
少しでも誰かの参考になれば幸いです。
ここまで読んでいただきありがとうございました。
Discussion