APC 技術ブログ

株式会社エーピーコミュニケーションズの技術ブログです。

株式会社 エーピーコミュニケーションズの技術ブログです。

AKSでもKarpenterで爆速オートスケールしよう(Node Autoprovision)

はじめに

こんにちは!ACS事業部の谷合です。
去年12月にパブリックプレビューで、Node autoprovision(以降NAP)がAKSでサポートされたのは皆さんご存じでしょうか?

azure.microsoft.com

NAPはAWS社製のOSSであるKarpenterをベースにしたAKSの拡張機能で、ノードのリソースひっ迫などで、スケジューリングできないPodがある場合、自動で事前定義済みのノードをプロビジョニングして、水平スケールさせます。

karpenter.sh

また、以下の利点があります。

  1. AKSのオートスケール機能を経由しないので、ノードの払い出しが早い
  2. ノードのスケールインが早い(オートスケール機能のスケールイン評価には10分かかります
  3. NodePoolリソースや、AKSNodeClassリソースなどで、払い出しノードが事前定義できる
  4. NodePoolリソースを複数作成することで、Kubernetesの機能であるnodeSelector や Node Affinity によって払い出しノードを選択できる。
  5. 払い出しノードを定義するNodePoolリソースに重みづけをし、優先度を設定できる

パブリックプレビューのアナウンスから1か月以上、触れていなかったので、検証してみます。
なお本ブログは以下のドキュメントを元にしています。

learn.microsoft.com

まずはクラスタ作成の準備

ます、aks-preview CLI拡張機能を有効化します。

$ az extension add --name aks-preview
Extension 'aks-preview' 0.5.163 is already installed.

次に、NodeAutoProvisioningPreview 機能フラグを登録します。

$ az feature register --namespace "Microsoft.ContainerService" --name "NodeAutoProvisioningPreview"

NodeAutoProvisioningPreview 機能フラグが登録されたか確認するには、以下のコマンドを実行します。
"state": "Registed"と表示されるまで数分かかる場合があります。

$ az feature show --namespace "Microsoft.ContainerService" --name "NodeAutoProvisioningPreview"

"state": "Registed"と表示されたら、以下コマンドでMicrosoft.ContainerService リソース プロバイダーの登録を最新の情報に更新します。

az provider register --namespace Microsoft.ContainerService

AKSで有効化しよう

現在は以下の制限があります。

  • Windows および Azure Linux ノード プールはまだサポートされていません(Ubuntuノードのみのサポートです)
  • ノード プール構成による Kubelet 構成はサポートされていません
  • NAP は現在、新規クラスター作成でのみ有効にできます
  • オーバーレイ ネットワークと cilium ネットワーク ポリシーを使用する必要があります。

それでは、az aks create コマンドを使用して、NAPを有効化したAKSを作成してみましょう。
作成時には、--node-provisioning-mode を "Auto" に設定します。
なお、今回は --nodepool-taints CriticalAddonsOnly=true:NoSchedule オプションを有効にして、ノードプールに CriticalAddonsOnly=true:NoSchedule taintを付与してシステムノードプールのみのクラスタとします。

$ az aks create \
  --name aks-karp-test \
  --resource-group <your resource group> \
  --node-provisioning-mode Auto \
  --network-plugin azure \
  --network-plugin-mode overlay \
  --network-dataplane cilium \
  --nodepool-taints CriticalAddonsOnly=true:NoSchedule

作成されたAKSで以下コマンドを実行してみてください。Karpenter関連のAPIが入っているはずです。
今回は、nodepoolsリソースを検証対象しています。

$ kubectl api-resources | awk 'NR==1' ; k api-resources | grep karpenter
NAME                               SHORTNAMES          APIVERSION                             NAMESPACED   KIND
aksnodeclasses                     aksnc,aksncs        karpenter.azure.com/v1alpha2           false        AKSNodeClass
machines                                               karpenter.sh/v1alpha5                  false        Machine
nodeclaims                                             karpenter.sh/v1beta1                   false        NodeClaim
nodepools                                              karpenter.sh/v1beta1                   false        NodePool
provisioners                                           karpenter.sh/v1alpha5                  false        Provisioner

以上で、検証の準備完了です。

スケールさせてみようぜ

NAPは、通常ノードのリソース不足の際に、スケジューリングされていないPending状態のPodを検出して、自動でノードを水平スケールさせる機能です。
今回は、AKSクラスタを作成した際に、CriticalAddonsOnly=true:NoSchedule taintを付与してシステムノードプールのみのクラスタとしたことで、「ノードのリソース不足でPodがスケジューリングできない状態」を疑似的に発生させてみます。

それでは行ってみましょー

まず、適当なPodをデプロイします。
すると、Podはスケジューリングできずに Pending となるはずです。
この時、ノードは3台のシステムノードのみです。

$ kubectl run test --image=nginx

$ kubectl get po -owide
NAME   READY   STATUS    RESTARTS   AGE   IP       NODE     NOMINATED NODE   READINESS GATES
test   0/1     Pending   0          13s   <none>   <none>   <none>           <none>

$ kubectl get nodes -o wide
NAME                                STATUS   ROLES   AGE     VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION      CONTAINER-RUNTIME
aks-nodepool1-36650083-vmss000000   Ready    agent   5m52s   v1.27.7   10.224.0.4    <none>        Ubuntu 22.04.3 LTS   5.15.0-1053-azure   containerd://1.7.5-1
aks-nodepool1-36650083-vmss000001   Ready    agent   5m49s   v1.27.7   10.224.0.6    <none>        Ubuntu 22.04.3 LTS   5.15.0-1053-azure   containerd://1.7.5-1
aks-nodepool1-36650083-vmss000002   Ready    agent   5m43s   v1.27.7   10.224.0.5    <none>        Ubuntu 22.04.3 LTS   5.15.0-1053-azure   containerd://1.7.5-1

同時に以下のコマンドも流しておくことで、新規ノードの払い出しの様子も見ることができるようになります。

$ kubectl get events -A --field-selector source=karpenter -w
NAMESPACE   LAST SEEN   TYPE     REASON             OBJECT                    MESSAGE
default     2s          Normal   Unconsolidatable   node/aks-default-65f84    Can't replace with a cheaper node
default     2s          Normal   Unconsolidatable   nodeclaim/default-65f84   Can't replace with a cheaper node
default     93s         Normal   Nominated          pod/test                  Pod should schedule on: nodeclaim/default-65f84

すると、Podが新規払い出しされたノード(aks-default-65f84)にスケジューリングされます。

$ kubectl get po -owide
NAME   READY   STATUS    RESTARTS   AGE     IP             NODE                NOMINATED NODE   READINESS GATES
test   1/1     Running   0          2m17s   10.244.3.245   aks-default-65f84   <none>           <none>

また、新規ノードにはtaintがないことも分かります。

$ kubectl describe node aks-default-65f84 | grep Taint
Taints:             <none>

$ kubectl describe node aks-nodepool1-36650083-vmss000000 | grep Taint
Taints:             CriticalAddonsOnly=true:NoSchedule

さて、この新規ノードはどういう定義のノードプールとなっているのでしょう?
正解は、以下のdefault NodePoolリソースと、AKSNodeClassリソースで定義されています。
NodePoolにはノード プロビジョナの要件や、Weightなど定義可能となっています。
また、AKSNodeClassにはノードのイメージやストレージサイズなどを定義します。
NodePoolを複数デプロイしておくことで、スケジューリングされていないPodに合うノードがデプロイされるわけです。

$ kubectl get nodepool default -oyaml
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  annotations:
    karpenter.sh/nodepool-hash: "12393960163388511505"
    kubernetes.io/description: General purpose NodePool for generic workloads
    meta.helm.sh/release-name: aks-managed-karpenter-overlay
    meta.helm.sh/release-namespace: kube-system
  creationTimestamp: "2024-01-18T10:28:20Z"
  generation: 1
  labels:
    app.kubernetes.io/managed-by: Helm
    helm.toolkit.fluxcd.io/name: karpenter-overlay-main-adapter-helmrelease
    helm.toolkit.fluxcd.io/namespace: 65a8fc4f884470000177418e
  name: default
  resourceVersion: "410732"
  uid: 6e9dcd87-dd1d-4167-aa5e-9171926d616c
spec:
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: Never
  template:
    spec:
      nodeClassRef:
        name: default
      requirements:
      - key: kubernetes.io/arch
        operator: In
        values:
        - amd64
      - key: kubernetes.io/os
        operator: In
        values:
        - linux
      - key: karpenter.sh/capacity-type
        operator: In
        values:
        - on-demand
      - key: karpenter.azure.com/sku-family
        operator: In
        values:
        - D

$ kubectl get aksnodeclasses default -oyaml
apiVersion: karpenter.azure.com/v1alpha2
kind: AKSNodeClass
metadata:
  annotations:
    kubernetes.io/description: General purpose AKSNodeClass for running Ubuntu2204
      nodes
    meta.helm.sh/release-name: aks-managed-karpenter-overlay
    meta.helm.sh/release-namespace: kube-system
  creationTimestamp: "2024-01-18T10:28:20Z"
  generation: 1
  labels:
    app.kubernetes.io/managed-by: Helm
    helm.toolkit.fluxcd.io/name: karpenter-overlay-main-adapter-helmrelease
    helm.toolkit.fluxcd.io/namespace: 65a8fc4f884470000177418e
  name: default
  resourceVersion: "1731"
  uid: e8baaa03-40b9-43b9-8b35-c90cf6452b53
spec:
  imageFamily: Ubuntu2204
  osDiskSizeGB: 128

上記のノード定義は、ノードのラベルで確認可能となっています。

$ kubectl get node aks-default-65f84  -ojson | jq '.metadata.labels'
{
  beta.kubernetes.io/arch=amd64
  beta.kubernetes.io/instance-type=Standard_D2_v4
  beta.kubernetes.io/os=linux
  failure-domain.beta.kubernetes.io/region=japaneast
  failure-domain.beta.kubernetes.io/zone=japaneast-2
  karpenter.azure.com/sku-cpu=2
  karpenter.azure.com/sku-family=D
  karpenter.azure.com/sku-gpu-count=0
  karpenter.azure.com/sku-memory=8192
  karpenter.azure.com/sku-name=Standard_D2_v4
  karpenter.azure.com/sku-networking-accelerated=true
  karpenter.azure.com/sku-version=4
  karpenter.azure.com/zone=japaneast-2
  karpenter.sh/capacity-type=on-demand
  karpenter.sh/initialized=true
  karpenter.sh/nodepool=default
  karpenter.sh/registered=true
  kubernetes.azure.com/cluster=MC_rg-taniai_aks-karp-test_japaneast
  kubernetes.azure.com/ebpf-dataplane=cilium
  kubernetes.azure.com/mode=user
  kubernetes.azure.com/network-name=aks-vnet-25367643
  kubernetes.azure.com/network-subnet=aks-subnet
  kubernetes.azure.com/network-subscription=d79e0410-8e3c-4207-8d0a-1f7885d35859
  kubernetes.azure.com/nodenetwork-vnetguid=72f5c0f4-5f2b-413a-8a9c-d2aa0d3d27ce
  kubernetes.azure.com/podnetwork-type=overlay
  kubernetes.azure.com/role=agent
  kubernetes.io/arch=amd64
  kubernetes.io/hostname=aks-default-65f84
  kubernetes.io/os=linux
  kubernetes.io/role=agent                    
}

AKSでサポートされているNodePoolリソースのサポートされているノード プロビジョナの要件は以下ドキュメントを参照してください。 learn.microsoft.com また、NodePoolの他の設定は、以下のKarpenterの公式ドキュメントで確認可能です。 karpenter.sh

では、次はPodを削除した際の動きを確認しましょう。

Podを削除して、不要になったノードは、NAPによって自動で削除されます。

$ kubectl delete po test 

$ kubectl get events -A --field-selector source=karpenter -w
NAMESPACE   LAST SEEN   TYPE      REASON                       OBJECT                    MESSAGE
:
default     0s          Normal    DisruptionTerminating        node/aks-default-65f84    Disrupting Node: Consolidation/Delete
default     0s          Normal    DisruptionTerminating        nodeclaim/default-65f84   Disrupting NodeClaim: Consolidation/Delete
default     0s          Normal    DisruptionWaitingDeletion    nodeclaim/default-65f84   Waiting on deletion to continue disruption

以下のコマンドでも削除されている様子が確認できるはずです。

$ kubectl get node
NAME                                STATUS     ROLES   AGE     VERSION
aks-default-65f84                   NotReady   agent   7m46s   v1.27.7
aks-nodepool1-36650083-vmss000000   Ready      agent   15m     v1.27.7
aks-nodepool1-36650083-vmss000001   Ready      agent   15m     v1.27.7
aks-nodepool1-36650083-vmss000002   Ready      agent   15m     v1.27.7

$ kubectl get node
NAME                                STATUS     ROLES   AGE     VERSION
aks-nodepool1-36650083-vmss000000   Ready      agent   15m     v1.27.7
aks-nodepool1-36650083-vmss000001   Ready      agent   15m     v1.27.7
aks-nodepool1-36650083-vmss000002   Ready      agent   15m     v1.27.7

NoodPoolリソースを追加してみよう

NodePoolリソースを追加した際の挙動も見てみましょう。

重みづけで優先度設定をする

まずは以下のNodePoolリソースを追加します。
この時、karpenter.azure.com/sku-cpuに8を指定します。
また、Weightを指定することで優先度をつけることもできます。
今回はをWeightを10にすることで、default NodePoolよりも優先させます。

$ cat << EOT | k apply -f -
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: my-node
spec:
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: Never
  template:
    spec:
      nodeClassRef:
        name: default
      requirements:
      - key: karpenter.azure.com/sku-cpu
        operator: In
        values:
        - "8"
  weight: 10
EOT

まず、Podをデプロイします。
すると、my-node NodePoolリソースを使用して、ノードが払い出されたことが確認できます。

$ kubectl run test --image=nginx

$ kubectl get events -A --field-selector source=karpenter -w
NAMESPACE   LAST SEEN   TYPE      REASON                       OBJECT                    MESSAGE
:
default     0s          Normal    Nominated                    pod/test                  Pod should schedule on: nodeclaim/my-node-lzf4f
default     0s          Normal    Unconsolidatable             node/aks-my-node-lzf4f    Can't replace with a cheaper node
default     0s          Normal    Unconsolidatable             nodeclaim/my-node-lzf4f   Can't replace with a cheaper node

$ kubectl get po -owide
NAME   READY   STATUS    RESTARTS   AGE     IP             NODE                NOMINATED NODE   READINESS GATES
test   1/1     Running   0          9m58s   10.244.3.107   aks-my-node-lzf4f   <none>           <none>

ノードのスペックを確認してみます。
"karpenter.azure.com/sku-cpu": "8" ラベルや、'.status.capacity'からmy-node NodePoolの設定が効いていることが確認できますね!

$ kubectl get node aks-my-node-lzf4f -ojson | jq '.metadata.labels'
{
  "beta.kubernetes.io/arch": "amd64",
  "beta.kubernetes.io/instance-type": "Standard_D8_v4",
  "beta.kubernetes.io/os": "linux",
  "failure-domain.beta.kubernetes.io/region": "japaneast",
  "failure-domain.beta.kubernetes.io/zone": "japaneast-2",
  "karpenter.azure.com/sku-cpu": "8",
  "karpenter.azure.com/sku-family": "D",
  "karpenter.azure.com/sku-gpu-count": "0",
  "karpenter.azure.com/sku-memory": "32768",
  "karpenter.azure.com/sku-name": "Standard_D8_v4",
  "karpenter.azure.com/sku-networking-accelerated": "true",
  "karpenter.azure.com/sku-version": "4",
  "karpenter.azure.com/zone": "japaneast-2",
  "karpenter.sh/capacity-type": "on-demand",
  "karpenter.sh/initialized": "true",
  "karpenter.sh/nodepool": "my-node",
  "karpenter.sh/registered": "true",
  "kubernetes.azure.com/cluster": "MC_rg-taniai_aks-karp-test_japaneast",
  "kubernetes.azure.com/ebpf-dataplane": "cilium",
  "kubernetes.azure.com/mode": "user",
  "kubernetes.azure.com/network-name": "aks-vnet-25367643",
  "kubernetes.azure.com/network-subnet": "aks-subnet",
  "kubernetes.azure.com/network-subscription": "d79e0410-8e3c-4207-8d0a-1f7885d35859",
  "kubernetes.azure.com/nodenetwork-vnetguid": "72f5c0f4-5f2b-413a-8a9c-d2aa0d3d27ce",
  "kubernetes.azure.com/podnetwork-type": "overlay",
  "kubernetes.azure.com/role": "agent",
  "kubernetes.io/arch": "amd64",
  "kubernetes.io/hostname": "aks-my-node-lzf4f",
  "kubernetes.io/os": "linux",
  "kubernetes.io/role": "agent",
  "node-role.kubernetes.io/agent": "",
  "node.kubernetes.io/instance-type": "Standard_D8_v4",
  "topology.disk.csi.azure.com/zone": "japaneast-2",
  "topology.kubernetes.io/region": "japaneast",
  "topology.kubernetes.io/zone": "japaneast-2"
}

$ kubectl get node aks-my-node-lzf4f -ojson | jq '.status.capacity'
{
  "cpu": "8",
  "ephemeral-storage": "129886128Ki",
  "hugepages-1Gi": "0",
  "hugepages-2Mi": "0",
  "memory": "32864128Ki",
  "pods": "110"
}

nodeSelectorでNoodPoolリソースを選択する

次に、以下のNoodPoolリソースを作成します。
この時karpenter.azure.com/sku-memoryを"131072"にします。

$ cat << EOT | k apply -f -
apiVersion: karpenter.sh/v1beta1
kind: NodePool
metadata:
  name: my-node-comp
spec:
  disruption:
    consolidationPolicy: WhenUnderutilized
    expireAfter: Never
  template:
    spec:
      nodeClassRef:
        name: default
      requirements:
      - key: karpenter.azure.com/sku-memory     
        operator: In
        values:
        - "131072"
EOT

$ kubectl get nodepools.karpenter.sh -owide
NAME           NODECLASS      WEIGHT
default        default        
my-node        default        10
my-node-comp   default        
system-surge   system-surge 

次にnodeSelectorで上記NodePoolリソースを選択してみましょう。
nodeSelectorか、Node Affinityが使用できますが、今回はnodeSelectorを使用します。
nodeSelectorに、上記NodePoolリソースの karpenter.azure.com/sku-memory: "131072" を指定します。
すると、指定したラベルがあるNodePoolリソースで、ノードが払い出されるはずです。

$ cat << EOT | k apply -f -
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: null
  labels:
    run: test-comp
  name: test-comp
spec:
  containers:
  - image: nginx
    name: test-comp
    resources: 
  nodeSelector:
    karpenter.azure.com/sku-memory: "131072"
EOT
pod/test-comp created

$ kubectl get events -A --field-selector source=karpenter -w
:
default     0s          Normal    Nominated                    pod/test-comp             Pod should schedule on: nodeclaim/my-node-comp-v57hv

$ kubectl get node
NAME                                STATUS   ROLES   AGE    VERSION
aks-my-node-comp-v57hv              Ready    agent   42s    v1.27.7
aks-nodepool1-36650083-vmss000000   Ready    agent   4d1h   v1.27.7
aks-nodepool1-36650083-vmss000001   Ready    agent   4d1h   v1.27.7
aks-nodepool1-36650083-vmss000002   Ready    agent   4d1h   v1.27.7

重みづけの時と同じく、"karpenter.azure.com/sku-memory": "131072" ラベルや、'.status.capacity'からmy-node-comp NodePoolの設定が効いていることが確認できます。

$ kubectl get node aks-my-node-comp-v57hv -ojson | jq '.metadata.labels'
{
  "beta.kubernetes.io/arch": "amd64",
  "beta.kubernetes.io/instance-type": "Standard_E16as_v5",
  "beta.kubernetes.io/os": "linux",
  "failure-domain.beta.kubernetes.io/region": "japaneast",
  "failure-domain.beta.kubernetes.io/zone": "japaneast-2",
  "karpenter.azure.com/sku-cpu": "16",
  "karpenter.azure.com/sku-encryptionathost-capable": "true",
  "karpenter.azure.com/sku-family": "E",
  "karpenter.azure.com/sku-gpu-count": "0",
  "karpenter.azure.com/sku-memory": "131072",
  "karpenter.azure.com/sku-name": "Standard_E16as_v5",
  "karpenter.azure.com/sku-networking-accelerated": "true",
  "karpenter.azure.com/sku-storage-premium-capable": "true",
  "karpenter.azure.com/sku-version": "5",
  "karpenter.azure.com/zone": "japaneast-2",
  "karpenter.sh/capacity-type": "spot",
  "karpenter.sh/initialized": "true",
  "karpenter.sh/nodepool": "my-node-comp",
  "karpenter.sh/registered": "true",
  "kubernetes.azure.com/cluster": "MC_rg-taniai_aks-karp-test_japaneast",
  "kubernetes.azure.com/ebpf-dataplane": "cilium",
  "kubernetes.azure.com/mode": "user",
  "kubernetes.azure.com/network-name": "aks-vnet-25367643",
  "kubernetes.azure.com/network-subnet": "aks-subnet",
  "kubernetes.azure.com/network-subscription": "d79e0410-8e3c-4207-8d0a-1f7885d35859",
  "kubernetes.azure.com/nodenetwork-vnetguid": "72f5c0f4-5f2b-413a-8a9c-d2aa0d3d27ce",
  "kubernetes.azure.com/podnetwork-type": "overlay",
  "kubernetes.azure.com/role": "agent",
  "kubernetes.io/arch": "amd64",
  "kubernetes.io/hostname": "aks-my-node-comp-v57hv",
  "kubernetes.io/os": "linux",
  "kubernetes.io/role": "agent",
  "node-role.kubernetes.io/agent": "",
  "node.kubernetes.io/instance-type": "Standard_E16as_v5",
  "topology.disk.csi.azure.com/zone": "japaneast-2",
  "topology.kubernetes.io/region": "japaneast",
  "topology.kubernetes.io/zone": "japaneast-2"
}

$ kubectl get node aks-my-node-comp-v57hv -ojson | jq '.status.capacity'
{
  "cpu": "16",
  "ephemeral-storage": "129886128Ki",
  "hugepages-1Gi": "0",
  "hugepages-2Mi": "0",
  "memory": "131903372Ki",
  "pods": "110"
}

重みづけでの優先度設定と、ラベルでのNodePool選択方法をご紹介しました。
NodePool選択時に、指定したラベルを持つNodePoolが複数ある場合は、重みづけをするなどユースケースが考えられると思います。

さいごに

今回はKarpeneterをベースにしたNAPをご紹介しました。
まだパブリックプレビューですが、GAが待ち遠しい機能ですよね。
さらなるAKSのノードスケール機能の進化を心して待ちましょう!

ACS事業部のご紹介

私達ACS事業部はAzure・AKSなどのクラウドネイティブ技術を活用した内製化やGitHub Enterpriseの導入のご支援をしております。
www.ap-com.co.jp www.ap-com.co.jp また、一緒に働いていただける仲間も募集中です!
我々の事業部のCultureDeckはコチラ。

www.ap-com.co.jp 今年もまだまだ組織規模拡大中なので、ご興味持っていただけましたらぜひお声がけください。 www.ap-com.co.jp

本記事の投稿者: 谷合純也
AKS/ACA、GitHubをメインにインフラ系のご支援を担当しています。
junya0530さんの記事一覧 | Zenn