📙

【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とはどんな役割をするのでしょうか?

https://knative.dev/docs/serving/

それは主にアプリケーションを「サーバーレスに動かすことにあります。

アプリケーションのコード(コンテナ)を用意するだけで、
それ以外の面倒な部分(インフラ管理、スケール調整など)を自動的にやってくれるということです。

  • ゼロへのオートスケール(自動的な拡大・縮小)
    アクセスが無い時は、 アプリケーションを実行するサーバー(Pod)をゼロに減らします。
    これにより、コストを節約できます。
    アクセスが来ると自動的にサーバーを起動(スケーリング)してリクエストを処理します。もちろんアクセスが多いときにはアクセス量に応じて、自動でサーバーの数を増やし(スケールアウト)、負荷に対応します。

  • トラフィック管理とデプロイの自動化
    アプリケーションの新しいバージョンを公開する際の複雑な作業を自動化します。
    アプリケーションのコードや設定を変更するたびに、
    その時点の状態が「リビジョン」として記録されます。
    新しいリビジョンをデプロイする際、すぐに全ユーザーに公開するのではなく、
    「新しいリビジョンに10%だけアクセスを振り分けて試す」といった、
    安全なリリース戦略(トラフィック分割)を簡単に設定できます。
    ちなみに本記事ではこの機能をほぼ無効化状態にして、単純にゼロスケールの実現にのみ利用しています。

では、早速Knative ServingをEKSに導入していきます。

Knative Servingの導入手順

今回Knative Servingのinstallするバージョンはv1.19.0です。

公式ドキュメントはこちらが参考になります。
https://knative.dev/docs/install/yaml-install/serving/install-serving-with-yaml/

はじめに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アプリケーションを用意します。

https://knative.dev/docs/getting-started/first-service/#__tabbed_1_2

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-servingconfigmap/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-servingconfigmap/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だとどう書くかが記載されているので、そちらをご確認ください。

https://knative.dev/docs/serving/convert-deployment-to-knative-service/#example-conversion

大きな特徴は、Knativeのマニフェストは基本kind: Serviceひとつにまとめられるという点です。

apiVersion: serving.knative.dev/v1
kind: Service

あとはゼロスケールに関する設定をannotationsで指定することで、細かいスケーリングの挙動を制御できます。

https://knative.dev/docs/serving/autoscaling/scale-bounds/

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-servingconfigmap/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