前回の Part 2 では、組み込みの autopilot ComputeClass で Standard クラスタに Autopilot 管理のノードを立て、gk3- プレフィックスや taint、Pod への mutation、DaemonSet の除外といった、「ノードの管理を Google に任せた場合」の挙動を確かめました。
では、組み込み版の ComputeClass と同じ内容を、ユーザ定義の ComputeClass で書いた場合、同じ結果が得られるでしょうか。
spec を見る限り、組み込み autopilot は podFamily: general-purpose の priority と autopilot.enabled: true を持つ、ごく普通の ComputeClass に見えます。
ということは、「同じ内容をカスタム ComputeClass で定義すれば、組み込み版と同じ結果が得られる」ことが期待できそうです。
連載の最終回となる Part 3 では、この組み込みとカスタムの ComputeClass の違いを、実機で確かめた様子をご紹介します。
検証条件
クラスタは Part 1, 2 と同じ、asia-northeast1-a の 1.36.0-gke.4447000 zonal Standard クラスタです (詳細は Part 1 の「検証環境」を参照ください)。
本記事で apply するワークロードは、いずれも 1 コンテナ / 1 Pod (Deployment は replicas: 1) で、resources は requests / limits とも cpu 500m / memory 512Mi に揃えています。
Part 2 の Job (組み込み autopilot ComputeClass、同じ requests / 1 Pod) と autoscaler 視点では同水準の compute 要求となっており、ワークロード視点ではおおよその条件を揃えました (Job / Deployment というコントローラ種別や、実行時点のクラスタ状態など、正確に揃っていない条件もあることはご承知おきください)。
Part 1, 2 と同様、計測値はあくまで参考程度とお考えください。
machineFamily を指定するカスタム Autopilot ComputeClass
最初は、autopilot.enabled: true に machineFamily: n2 を組み合わせるパターンです。
Part 1 の basic-n2 と同じマシンファミリー指定に、autopilot.enabled: true を加えた構成といえます。
apiVersion: cloud.google.com/v1 kind: ComputeClass metadata: name: ap-n2-custom spec: autopilot: enabled: true priorities: - machineFamily: n2 spot: true - machineFamily: n2 spot: false whenUnsatisfiable: ScaleUpAnyway
apply 後に立ち上がったノードを確認します。
$ kubectl get nodes -L cloud.google.com/compute-class,cloud.google.com/machine-family,cloud.google.com/gke-spot NAME ... COMPUTE-CLASS MACHINE-FAMILY GKE-SPOT gk3-gke-cc-poc-nap-n2-standard-2-spot-c0d1a47b-hldx ... ap-n2-custom n2 true
ノード VM 名は gk3- プレフィックスで、MACHINE-FAMILY は n2、priority 1 のとおり spot です。
autopilot.enabled: true を加えても、priority に書いた machineFamily: n2 はそのまま尊重されています。
ノードラベルにも gke-image-streaming: true / gke-gcfs: true / autopilot-managed-node: true など、Part 2 で見た Autopilot 共通の要素が同様に付与されていました。
「Autopilot 化の付加要素は一式付くが、マシンの選出は設定したとおりの挙動が観測できる」という結果となっています。
ところで、この spec には Part 1 で使った nodePoolAutoCreation.enabled: true を書いていません。
それでもプールが自動作成されたのは、spec に補完が入るためです。
apply 後の spec を kubectl get computeclass -o yaml で取得すると、autopilot.enabled: true の ComputeClass には nodePoolAutoCreation.enabled: true と autoscalingPolicy (組み込み版と同じ値) が自動で補完されていました (この補完は、後述のスケールダウンの考察に関わってきます)。
課金モデルの整理 (Part 2) に照らすと、machineFamily を指定した priority rule なので、node-based billing が適用される構成になるはずです。
一方、細部には Part 2 の組み込み版と違う点もありました。
Pod への mutation は、ephemeral-storage: 1Gi の追加が requests のみで、requests / limits の両方に追加された組み込み版とは異なる状態となっていました。
podFamily を指定するカスタム Autopilot ComputeClass
次は、組み込み版と同じ podFamily: general-purpose を priority に書くパターンを試してみます。
カスタム ComputeClass での podFamily は 1.35.2-gke.1485000 以降で使えるようになった、比較的新しい書き方です (Part 2 のバージョン要件を参照)。
論理的には、組み込み autopilot と同じ指定のはずです。
apiVersion: cloud.google.com/v1 kind: ComputeClass metadata: name: ap-podfamily-custom spec: autopilot: enabled: true priorities: - podFamily: general-purpose whenUnsatisfiable: ScaleUpAnyway
ワークロードは同じ基準条件 (replicas: 1 / requests / limits とも cpu 500m / memory 512Mi) です。
apply 後、Pod は 122 秒で Pending → Running になりました。
$ kubectl get nodes -L cloud.google.com/compute-class,cloud.google.com/machine-family NAME ... COMPUTE-CLASS MACHINE-FAMILY gk3-gke-cc-poc-nap-e2-medium-wsvb82l4-dfa0c313-7psd ... ap-podfamily-custom e2
調達されたノードは e2-medium です。
Part 2 で組み込み autopilot が引いたのと同じマシンタイプで、gk3- プレフィックスも、taint が 2 つであることも、gke-max-pods-per-node: 32 も、ephemeral-storage: 1Gi が requests / limits の両方に付く mutation も、組み込み版と同じでした。
機種と付加要素を見る限り、これも「組み込みと同じ内容を書けばカスタムでも同じ結果が得られる」ように見えます。
e2-medium は fallback ではないのか
Part 1 の検証では、whenUnsatisfiable: ScaleUpAnyway の fallback で default の E2 シリーズが起動しました。
今回のカスタム ComputeClass も ScaleUpAnyway で、起動したのは e2-medium です。
「podFamily を満たせず、fallback の結果としてたまたま E2 が調達されただけ」という可能性は考えられるでしょうか。
そこで、whenUnsatisfiable: DoNotScaleUp に変えた variant (便宜的に ap-podfamily-dnsup と呼びます) を用意してみました。
DoNotScaleUp なら、priority を満たせない場合 Pod は Pending のまま残り、fallback は起こらないはずです。
結果、この variant でも Pod は Pending のまま滞留することなく e2-medium のノードにスケジュールされました。
乗った先は既存ノードではなく、この variant 用に自動作成された新規プール (nap-e2-medium-y9n2qnz4) のノードで、ノードの compute-class ラベルも ap-podfamily-dnsup となっていました。
ただし、この推論は「DoNotScaleUp が仕様どおり動いている」ことを前提にしています。
こちらも確認のため、満たせない priority (存在しない machineType) だけを持つ DoNotScaleUp の ComputeClass を作り、基準条件のワークロードを deploy して 300 秒観察したところ、Pod は一貫して Pending のままでした。
(対照条件を揃えるため、この ComputeClass にも autopilot.enabled: true を入れています)
## POLL: wait until >=1 pods (-l app=pending-control-app) reach Running (timeout 300s) [t+0015s] pending-control-app-...=Pending; [t+0150s] pending-control-app-...=Pending; [t+0285s] pending-control-app-...=Pending; ## RESULT: TIMEOUT at t+300s
DoNotScaleUp は仕様どおり Pending を維持するようです。
その DoNotScaleUp の下でも e2-medium が立った以上、autoscaler は「podFamily: general-purpose という priority を e2-medium で満たせる」と判断している、つまり fallback ではないと考えられます。
podFamily はハードウェアを指名しない priority rule なので、requests に見合う小さなマシンが正規の選択肢として選ばれているようです。
マシンの調達ロジックが同じ、付加要素も同じ、そして fallback でもない、ということで、ここまでに見てきた内容では組み込み版とカスタム版に違いが見られません。
Pod の最小サイズはどうか
今度は、Part 2 の mutation 検証で使った、最小値以下の requests (cpu: 10m / memory: 16Mi、limits なし) の probe Pod をもう一度使います。
組み込み autopilot 経由では、requests は 50m / 52Mi に引き上げられるのでした。
同じ probe を、カスタムの ap-podfamily-custom を nodeSelector に指定して deploy します。
{ "limits": { "ephemeral-storage": "1Gi" }, "requests": { "cpu": "500m", "ephemeral-storage": "1Gi", "memory": "512Mi" } }
結果、request の値は 500m / 512Mi となりました。
組み込み版の 10 倍の CPU、およそ 10 倍のメモリが、最小値として強制されています。
マシンの調達ロジックもラベルも同じに見えていた両者ですが、Pod の最小サイズという点では異なる結果となりました。
この差異が生まれた要因について、公式ドキュメントに記載があります。
Resource requests in Autopilot の最小値の表は、「デフォルトとして登録されていないカスタム Autopilot ComputeClass の podFamily priority rule を使う Pod」を独立したカテゴリとして扱い、その最小値を CPU 0.5 vCPU / メモリ 0.5 GiB と定めています。
組み込み ComputeClass (bursting 対応クラスタで 50m / 52 MiB) とは別の区分です。
今回の観測値 (500m / 512Mi = 0.5 vCPU / 0.5 GiB) は、この記載と一致しています。
ちなみに、本記事の基準ワークロードをそのまま使っている限り、この差は見えませんでした。
基準の cpu 500m / memory 512Mi は、偶然ですがこのカテゴリの最小値と同じ値で、mutation が働いても値が変わらないためです (最小値以下の probe を投げてはじめて、両者の違いが表面化しました)。
なお、autopilot なしの ComputeClass (Part 1 と同じ basic-n2) に同じ probe を投げても、mutation は一切かかりませんでした (requests は 10m / 16Mi のまま、ephemeral-storage の追加もなし)。
公式ドキュメントの書きぶりから見ても、最小値の強制は Autopilot 系に固有と見て良さそうです。
デフォルト登録すると何が変わるか
GKE には、Pod が ComputeClass を明示的に選ばなかった場合に適用されるデフォルト ComputeClass を、namespace 単位またはクラスタ単位で設定する仕組みがあります (Apply ComputeClasses to Pods by default)。 さきほどはデフォルトとしての登録がないカスタム ComputeClass の挙動を観測しましたが、同じカスタム ComputeClass をデフォルトとして登録すれば、振る舞いが変わるでしょうか。
まずは、namespace 単位のデフォルト設定を試してみましょう。
namespace に cloud.google.com/default-compute-class=ap-podfamily-custom のラベルを付け、その namespace に nodeSelector を書いていない Pod を deploy します。
Pod の spec を確認すると、ComputeClass の nodeSelector が自動で注入されていました。
$ kubectl get pod -l app=probe-default-ns -o jsonpath='{.items[0].spec.nodeSelector}'
{"cloud.google.com/compute-class":"ap-podfamily-custom"}
そして、最小値以下の probe (10m / 16Mi) に対する mutation は、50m / 52Mi でした。
明示的な nodeSelector で選んだときは 500m / 512Mi に引き上げられた、同じ ap-podfamily-custom です。
ComputeClass の spec には一切手を加えていません。
それでも、デフォルトとして選ばせただけで、この試行では Pod の最小値が組み込み版と同じ扱いになりました。
公式の最小値の表に明記されている区分は「組み込み / クラスタ単位のデフォルト」と「それ以外のカスタム」で、namespace 単位のデフォルトがどちらに属するかは明記されていないようですが、挙動を見る限り、namespace 単位でも「デフォルト側」の扱いになるようです。
次に、クラスタ単位のデフォルトも試してみましょう。
こちらは gcloud container clusters update --enable-default-compute-class でフラグを有効にし、default という名前の ComputeClass (spec は ap-podfamily-custom と同一) を作る方式です。
ラベルのない namespace に nodeSelector なしの Pod を deploy したところ、基準サイズの Deployment は新規の gk3- プールに乗り、probe の mutation はやはり 50m / 52Mi 側でした。
ただし、Pod spec の書き換えられ方が namespace 単位のときと異なります。 前掲の Apply ComputeClasses to Pods by default には、次の記載があります。
If the namespace has a default ComputeClass, GKE modifies the Pod specification to select that ComputeClass.
If the namespace doesn't have a default ComputeClass, the cluster-level default class applies. GKE doesn't modify the Pod specification.
実機で見ると、確かに nodeSelector は注入されていませんでした。
一方で toleration (autopilot-managed-node と kubernetes.io/arch) は注入されており、最小値の mutation も前述のとおり働いているため、「spec を変更しない」という記載の対象は、あくまで ComputeClass の選択機構 (nodeSelector) の話で、Autopilot 系の mutation や toleration は別経路、と考えられそうです。
また、デフォルト ComputeClass 用に立ったプールのノードには compute-class の taint がなく、taint は autopilot-managed-node の 1 つだけでした。
nodeSelector で縛られない Pod が Autopilot 管理ノードに乗れていたのは、この taint 構成と注入された toleration の組み合わせによるものです。
nodeSelector がない、ということは、配置制約の緩さにつながります。
検証中の何度かの試行では、mutation を受けて 50m / 52Mi になった probe が、Autopilot 管理ノードではなく既存の default-pool (通常の Standard ノード) にスケジュールされました。
Pod を ComputeClass のノードに拘束するものが spec 上にないため、既存ノードに収まるならそちらに置かれることも仕組み上ありえます。
「デフォルト ComputeClass を設定したから、対象 Pod はすべて Autopilot 管理ノードで動くはずだ」と考えていると、想定と異なる挙動になってしまう可能性があります。
なお、デフォルト経由で立ったマシンタイプは試行によって異なりました (e2-medium のときもあれば、ek-standard-8 のときもある)。
ek は Part 2 で触れた Autopilot 専用のマシンシリーズで、今回の一連の検証では、このデフォルト登録の試行にだけ現れています。
podFamily がマシンを固定しない、という定義どおりの挙動が実機からも観測できました。
補足: 何をもって「カスタム」ComputeClass を識別しているか
ここまでの結果では、明示的な nodeSelector で選んだカスタム ComputeClass は 500m / 512Mi、デフォルト登録して選ばせた同じクラスは 50m / 52Mi と、扱いが分かれました。
GKE は何を見てこの判定を行っているのでしょうか。
spec の内容が組み込み版と一致していれば、組み込みの ComputeClass と同じ扱いになるのでしょうか。
組み込み autopilot の spec をフィールド単位で完全に複製した ComputeClass を作って確かめてみましょう。
ComputeClass 名は autopilot / gke で始められない予約プレフィックスの制約があるため、名前は ap-podfamily-clone としました。
activeMigration も description の文字列も、組み込み版と bit-exact に揃えた構成です。
結果、立ったノードは e2-medium で、機種の面では違いが出ませんでしたが、probe に対する mutation は 500m / 512Mi のままでした。
どうやら (spec を完全に写しても) 明示的な nodeSelector で選ぶ限りは、カスタム ComputeClass として扱われているようです。
よって、「spec の内容さえ組み込み版と一致していれば組み込み扱いになる」ということではないようです。
となると、先ほどの試行で変更した、登録の有無と選択経路 (デフォルト経由か、明示の nodeSelector か) のいずれか (あるいは両方) が、50m / 52Mi 側に倒した要因と考えられます。
これを確認するため、今度は「デフォルト登録済みのクラスを明示的な nodeSelector で選ぶ」probe を、クラスタを立て直して追試しました (この追試のみ、GKE 1.36.0-gke.4681000 での観測です)。
結果は、登録の単位によって割れました。
- namespace デフォルトに登録した
ap-podfamily-customを明示的なnodeSelectorで選ぶと、登録した namespace の中からでも、ラベルのない別の namespace からでも500m / 512Mi(カスタム側) になる - クラスタ単位デフォルトとして登録した
defaultクラス (spec は同一) を明示のnodeSelectorで選ぶと、50m / 52Mi(デフォルト側) になる - 対照として同時に再現した「
nodeSelectorを書かずにデフォルト経由で選ばせた Pod」は、どちらの登録でも50m / 52Miになる
とくに namespace デフォルトのパターンでは、注入された Pod と明示的に指定した Pod とで、出来上がった spec の nodeSelector は同一でした。
それでも扱いが分かれたということは、GKE は「自分が注入したかどうか」を Pod spec の形とは別のところで区別していることになります。
よって、カスタム ComputeClass は次のような法則で扱いが変わると考えられます:
- クラスタ単位のデフォルト登録は、クラスそのものをデフォルト側に移す (明示の
nodeSelectorで選んでも50m / 52Mi) - namespace 単位のデフォルト登録は、クラスの扱いを変えず、namespace デフォルトの注入で選ばれた Pod だけがデフォルト側で扱われる
公式ドキュメントの最小値の表を見ると、クラス側の区分 (「組み込み / クラスタ単位のデフォルト」と「それ以外のカスタム」) は、確かにこの実測と整合しています。
その区分に載らない namespace デフォルトだけが、クラスではなく Pod ごとの選ばれ方で挙動を変えているようです。
GKE の区別は spec の内容ではなく、クラスの区分 (組み込みまたはクラスタ単位デフォルトか、それ以外か) と、後者における Pod ごとの選ばれ方 (namespace デフォルトの注入か、明示の nodeSelector か) にある、ということが今回の検証で観測できました。
(なお、クラスタ単位デフォルトは「default という名前の ComputeClass + クラスタ側のフラグ」の組でしか構成できないため、名前とフラグのどちらが効いているのかまでは切り分けていません)
手動削除とスケールダウン
別の観点として、運用面の挙動も確かめておきます。 まず、カスタム Autopilot ComputeClass が作ったノードプールの手動削除です。 Part 2 で組み込み版のプールが HTTP 400 で拒否されることを見ましたが、カスタム版はどうでしょうか。
$ gcloud container node-pools delete nap-e2-medium-wsvb82l4 \
--cluster=gke-cc-poc --zone=asia-northeast1-a --quiet
ERROR: (gcloud.container.node-pools.delete) ResponseError:
code=400, message=Autopilot node pools cannot be accessed or modified.
ap-podfamily-custom のプール (e2-medium) も、ap-n2-custom のプール (n2-standard-2 spot) も、組み込み版と同じエラーで拒否されました。
autopilotConfig.enabled: true を持つノードプールは、由来が組み込みかカスタムかを問わず Google の管理下にあるということです。
今度は、スケールダウンの所要時間を計測しました。 workload 削除と同時に計測を始めたカスタム Autopilot のプールは、約 7.7 〜 8.7 分で一覧から消えました (組み込み版のプールは Job 削除の直後から数えて約 7.0 〜 7.2 分)。 今回の計測した限りでは、組み込みの ComputeClass であるかカスタム ComputeClass であるかによって明確な差はないように見えました。 また、サンプル数が少ないため確かなことは言えませんが、Part 1 の Standard 系 (5.6 分〜 15.3 分) よりはばらつきが少ないように見えます。
補足: リソースの削除順序
今回の検証で、ComputeClass をノードプールの削除より先に消してしまったケースが 1 つあり、そのプールの定義だけは 30 分以上一覧に残り続けました。 ノードが稼働していなければ課金対象にはならないため、さほど気にする必要はないかもしれませんが、「workload の削除 → プールの消滅確認 → ComputeClass の削除」という順序で片付けるのが無難と言えそうです。
シリーズ全体の差分整理
3 部にわたって観察した内容を、ComputeClass の系統ごとの差分として一覧にします。
| 観点 | Standard 系 (autopilot なし / Part 1) | Autopilot 系: 組み込み / クラスタ単位デフォルト | Autopilot 系: その他のカスタム |
|---|---|---|---|
| ノード VM 名のプレフィックス | gke-...-nap-... |
gk3-...-nap-... |
gk3-...-nap-... |
autopilot-managed-node のラベルと taint |
(無し) | あり (組み込みは taint 計 2 個。クラスタ単位デフォルトのプールは compute-class taint なしの計 1 個) |
あり (taint 計 2 個) |
gke-max-pods-per-node (実測) |
110 | 32 | 32 |
| マシンの選ばれ方 | priority の指定どおり (サイズは requests から決定) | podFamily は requests に応じたサイズ (今回は e2-medium、デフォルト経由の試行では ek-standard-8 も) |
同左。machineFamily 指定時は priority どおり |
| 最小値の mutation (probe 実測) | なし (manifest どおり) | 50m / 52Mi へ引き上げ (クラスタ単位デフォルトは明示の nodeSelector で選んでも同じ。追試で実測) |
500m / 512Mi へ引き上げ (podFamily 系の明示選択で実測。namespace デフォルトの注入で選ばれた Pod のみ 50m / 52Mi 。machineFamily 系は未実測) |
ephemeral-storage: 1Gi の付与 (実測) |
なし | requests / limits の両方 (組み込みで実測) | podFamily 系は両方、machineFamily 系は requests のみ |
| 課金モデル (公式ドキュメントの分類、課金明細は未実測) | Standard 課金 | podFamily → Pod-based (machineFamily 系の rule を書く場合は machineFamily → node-based。 priority rule で優先されるため) |
podFamily → Pod-based、machineFamily → node-based |
| ノードプール手動削除 | 可能 | HTTP 400 で拒否 | HTTP 400 で拒否 |
| Pod 削除後の scale-down 所要 (実測) | 5.6 〜 15.3 分 | 約 7.0 〜 7.2 分 | 約 7.7 〜 8.7 分 |
この表では、最小値の行が Autopilot 系の 2 列で分かれています。
同じ podFamily: general-purpose を明示の nodeSelector で選んだ場合で比べると、ノードの外形 (VM 名のプレフィックス、taint、max-pods ラベルなど) も、機種の選ばれ方も、手動削除の拒否も区別がなく、今回明確に観測できた差は Pod の最小サイズでした。
そしてその挙動の差異は spec の書き方ではなく、クラスの区分 (組み込みまたはクラスタ単位デフォルトか、それ以外か) と、それ以外のカスタムにおける Pod ごとの選ばれ方 (namespace デフォルトの注入か、明示の nodeSelector か) によって生まれていました。
おわりに
全 3 回の記事を通じて、ComputeClass を用いてノードプールを自動作成する複数のパターンを実機検証しました。
Part 1 では、「宣言した構成のマシンだけが立ち上がるのか」という問いを立て、(priority を満たせる状況であれば) 指定したとおりの構成でマシンが調達できることを確認しました。
満たせない priority は、試行の痕跡を残さずスキップされ、ScaleUpAnyway の下では default の E2 へ fallback する挙動を観察しました (一方、DoNotScaleUp が Pending を維持することは Part 3 で確認しました)。
Part 2 では、「ノードの管理を Google に任せた場合」の挙動を確かめました。
組み込みの autopilot ComputeClass により、Standard クラスタの中に Autopilot 管理のノードを立て、gk3- プレフィックスや taint、Pod への mutation、DaemonSet の除外といった挙動を観察しました。
Part 3 では、組み込みの ComputeClass とカスタム ComputeClass が GKE からどのように区別されているのかを調査しました。
ノードの外形 (taint, ラベルなど) もマシンファミリーの選ばれ方も両者で同じ結果となる中、Pod が強制されるリソースの最小値だけは、spec の内容ではなく、クラスの区分 (組み込みまたはクラスタ単位デフォルトか) と Pod ごとの選ばれ方 (デフォルト注入か、明示の nodeSelector か) で扱いが分かれていました。
使い分けの指針としては、(一案として) 次のように考えられそうです:
小さな Pod を小さな requests のまま (= Pod あたりの下限を低く) 動かしたいなら、組み込みの autopilot 系か、クラスタ単位のデフォルト登録を使うのが良いでしょう。
namespace 単位のデフォルトも、注入で選ばれる Pod に限れば同じ最小値になりましたが、公式ドキュメント上の位置づけを確認できていないことに加え、登録した namespace の中でも Pod が明示の nodeSelector で選ぶと最小値はカスタム側に戻る点に注意です。
カスタムの podFamily ComputeClass を明示の nodeSelector で選ぶ構成では、(クラスタ単位デフォルトに登録している場合を除き) Pod あたり最小 0.5 vCPU / 0.5 GiB を前提に容量を見積もる必要があります。
特定のマシンファミリーを Autopilot 化したいだけなら、machineFamily 指定のカスタム ComputeClass を用いるのが最も簡単です。
いずれの場合も、クラスタ単位のデフォルト登録では対象 Pod が既存の Standard ノードに配置されるケースがあるため、この点は設計時に考慮しておいたほうが良いでしょう。
今後は podFamily の更なる拡充など、ComputeClass のアップデートがあれば機会を見て検証していきたいと思います。
長々とした記事になってしまいましたが、ご覧いただきありがとうございました。

