Amazon Web Services ブログ

Amazon EKS のスケジューリングをカスタマイズする

この記事は Customizing scheduling on Amazon EKS (記事公開日 : 2022 年 6 月 1 日) の翻訳記事です。

Google Trends によると、Kubernetes への関心は 2019 年の秋に急上昇しました。F-16 に Kubernetes をデプロイしたという米国国防総省の発表を受けて、関心が急上昇したのでしょうか。今日、ブロックチェーンネットワークの構築から 5G ネットワークの構築まで、ほぼすべての業界で Kubernetes が利用されています。お客様は Kubernetes を使用してイノベーションを加速し、明日のインターネット基盤を構築しています。

Kubernetes がこの変化し続ける景色の中で成長を続けているのは、柔軟で、様々なユースケースに対応できるからです。その拡張性により、ビジネスニーズに合わせて Kubernetes をチューニングできます。この記事では、Kubernetes によるワークロードのスケジューリングを簡単にカスタマイズする概念実証 (a proof of concept) を紹介します。

Kubernetes におけるワークロードのスケジューリング

Kubernetes のスケジューラープロセス (kube-scheduler) は、ノードに Pod を割り当てるコントロールプレーンのプロセスです。Pod を作成すると、kube-scheduler はクラスター内の最適なノードを選び、そのノードに Pod をスケジュールします。スケジューラーは、リソース要求、アフィニティルール、トポロジー分散などを考慮し、Pod の設定に基づいて条件を満たすノードをフィルタリングし、順位付けします。kube-scheduler は、デフォルトでノード間で Pod を分散しますが、より細かい Pod のスケジューリングの制御が必要な状況も存在します。

例えば、Amazon Elastic Kubernetes Service (Amazon EKS) をご利用のお客様の多くは、Amazon EC2 Spot でワークロードを実行することでコストを削減したいと考えていますが、広範な Spot の中断が生じる可能性を考慮して、少数の Pod を Amazon EC2 オンデマンドで実行したいとも考えています。また、特定のユースケースでは、他のアベイラビリティゾーン (AZ) に対して、ある AZ に限定して Pod を分散したいと考えるお客様もいます。

kube-scheduler は、異なるラベルを持つノード間における任意の比率での Pod のスケジューリングを現状サポートしていません。この記事で提案するソリューションでは、Deployment マニフェスト内でノードのフィルタリングと順位付けに使用するロジックを直接設定する mutating admission webhook を構築します。

mutating admission webhook を使用して Pod のスケジューリングをカスタマイズする

Kubernetes API リクエストのライフサイクルと、mutating と validating admission webhooks

Kubernetes には、Kubernetes の API サーバーへのリクエストを etcd と呼ばれるキーバリューストアに永続化する前にインターセプトするコードの 1 つに、Admission Controller があります。mutating admission controller を使うと、リソース作成前にそのリソースの属性を変更できます。例えば、mutating admission controller を使用して、Pod 作成前にラベルを追加したり、サイドカーを挟み込むことが可能です。

この記事で提案するソリューションでは、mutating admission webhook を使用して、Pod 作成に関するリクエストをインターセプトし、Pod をノードに割り当てます。これにより、ノードラベルを使用して Pod を任意の比率でスケジューリングする、カスタム Pod スケジューリング戦略を定義できます。以下に、カスタムスケジューリング戦略の例を示します。

  annotations:
    custom-pod-schedule-strategy: 'label1Key=label1Value,base=1,weight=0:label2Key=label2Value,weight=1:label3Key=label3Value,weight=1'

Deployment で custom-pod-schedule-strategy アノテーションを指定すると、webhook はこのアノテーションを考慮して、様々なラベルを持つノードに Pod を割り当てます。各ノードラベルは、base パラメーターと weight パラメーターを指定できます。base は、ここで指定した数の Pod が、まずそのラベルを持つノードにスケジュールされることを表します。weight は、異なるラベルを持つノードに割り当てる Pod の相対的な配分 (訳注 : base でスケジュールした Pod を除いて考えます) を表します。ただし、base パラメーターを設定できるノードラベルは 1 つのみであることに注意してください。

理解を深めるために、それぞれ専用のラベルを持つ 2 種類のノード、N1 と N2 を考えてみましょう。webhook が (セレクターを使用して) これらのノードに Pod を割り当てます。

D = Deployment の Pod のレプリカ数
N1 = D のうち、ラベル 1 を持つノードにスケジュールすべき Pod 数
N2 = D のうち、ラベル 2 を持つノードにスケジュールすべき Pod 数

D = N1 + N2

M1 = nodeSelector でラベル 1 を指定した、既存の (running または pending 状態の) Pod 数
M2 = nodeSelector でラベル 2 を指定した、既存の (running または pending 状態の) Pod 数

では、Kubernetes の Deployment のマニフェストの例を見てみましょう。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: test
  annotations:
    custom-pod-schedule-strategy: 'label1Key=label1Value,base=2,weight=1:label2Key=label2Value,weight=3'
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 10
  ....

上の例では、label1Key=label1Value のラベルを持つノードに、base=2weight=1 を指定しています。

ここで、label1Key=label1Value のラベルを持つノードはオンデマンドノードであるとます。また、Spot ノードは label2Key=label2Value のラベルを持ち、weight=3 が指定されているとします。

計算してみましょう。
D = 10 (Deployment のレプリカ数)

N1 = label1Key=label1Value のラベルを持つノードの Pod 数
= base + (D – base) x (ラベル 1 の weight / 全ラベルの weight の総和)
= 2 + (10 – 2) x ( 1 / 4) = 2 + 8/4 = 4

N2 = label2Key=label2Value のラベルを持つノードの Pod 数
= (D – base) x (ラベル 2 の weight / 全ラベルの weight の総和)
= (10 – 2) x ( 3 / 4) = 8 x 3/4 = 6

D (この場合 10) = N1 (この場合 4) + N2 (この場合 6)

したがって、10 個の Pod を作成すると、webhook は (base=2 を指定したので) 最初の2 個の Pod をオンデマンドノードに割り当て、残りの 8 つの Pod を、オンデマンドと Spot インスタンスで (wight で指定した) 1 : 3 の割合で分散します。よって、オンデマンドノードでは 4 Pod (2 + 2)、Spot インスタンスでは 6 Pod を実行します。

提案するソリューションでは、PodToNodeAllocator と呼ばれる 1 つのコンポーネントを使用して、このような動作を実現します。

PodToNodeAllocator

PodToNodeAllocator は、Pod が作成・スケールされると、Pod を一定の比率で割り当てます。PodToNodeAllocator には、Pod 作成に関するリクエストを監視する mutating admission webhook の実装が含まれます。Kubernetes クラスターが Pod 作成に関するリクエストを受け取ると、webhook は custom-pod-schedule-strategy を考慮し、PodSpec に nodeSelector フィールドを追加することで Pod をノードに割り当てます。ただし、Deployment 作成時の Pod のスケジューリングなど、Pod 起動時の比率配分のみを保証するものであることに注意してください。

その後、PodToNodeAllocator は新しい Pod に対して、Deployment のアノテーションにあるカスタム Pod スケジューリングの仕様で指定されたノードラベルごとに、次の手順を実行します。

  • API サーバーで作成された新しい Pod (P) について
    • 各ノードラベル (L) について
      • M = このラベルを持つノードにすでに割り当てられている、既存の (running または pending 状態の) Pod 数を取得します。
      • N = D のうち、このラベルを持つノードにスケジュールすべき Pod 数を計算します。
      • もし M >= N であれば、このラベル L を無視します。
      • もし M < N であれば、Pod P の spec を、ラベル L の nodeSelector で更新します。
        • Pod の spec が nodeSelector によって更新されると、Kubernetes のスケジューラーは指定したラベルを持つノードに Pod を割り当てます。

Pod が作成・スケールされると、Pod を一定の比率で割り当てる PodToNodeAllocator

概念実証の手順

準備

チュートリアルを完了するために、以下を準備してください。

注 : この記事内の CLI の手順は、Amazon Linux 2 でテストしました。

まず、いくつかの環境変数を設定することから始めましょう。

export AWS_REGION=us-east-1 
export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
export CLUSTER_NAME=eks-custom-pod-schedule  # choose an existing EKS Cluster name
export ECR_REPO=custom-kube-scheduler-webhook
export SERVICE=custom-kube-scheduler-webhook
export NAMESPACE=custom-kube-scheduler-webhook
export SECRET=custom-kube-scheduler-webhook-certs

EKS クラスターを作成する

既存のクラスターを使用する場合には、この手順は省略できます。ただし、環境変数 CLUSTER_NAME を EKS クラスター名と一致するように設定してください。

eksctl コマンドラインツールを使用して、EKS クラスターを作成します。

eksctl create cluster \
  --name $CLUSTER_NAME \
  --region $AWS_REGION \
  --version 1.21  \
  --managed

クラスターの作成に成功したら、Karpenter のインストールに進みます。Karpenter は Kubernetes 向けに作成されたオープンソースのノードプロビジョニングプロジェクトで、Kubernetes クラスター上でワークロードを実行する際の効率とコストの改善を目的としています。使用開始するために、このブログ記事を参照してください。

カスタムスケジューリング webhook をデプロイする

EKS クラスターが使用可能になったら、admission webhook のソースコードと Deployment ファイルが含まれる GitHub リポジトリをクローンします。

git clone https://github.com/aws-samples/containers-blog-maelstrom.git
cd custom-kubernetes-scheduler

Amazon Elastic Container Registry (Amazon ECR) リポジトリを作成し、webhook のコンテナイメージを保存します。以下のコマンドは、リポジトリが存在しない場合、新しいリポジトリを作成します。

IMAGE_REPO="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
IMAGE_NAME=${ECR_REPO}
export ECR_REPO_URI=$(aws ecr describe-repositories --repository-name ${IMAGE_NAME}  | jq -r '.repositories[0].repositoryUri')
if [ -z "$ECR_REPO_URI" ]
then
      echo "${IMAGE_REPO}/${IMAGE_NAME} does not exist. So creating it..."
      ECR_REPO_URI=$(aws ecr create-repository \
        --repository-name $IMAGE_NAME\
        --region $AWS_REGION \
        --query 'repository.repositoryUri' \
        --output text)
      echo "ECR_REPO_URI=$ECR_REPO_URI"
else
      echo "${IMAGE_REPO}/${IMAGE_NAME} already exist..."
fi

Go アプリケーションを含むコンテナイメージをビルドし、Amazon ECR にプッシュします。

make

make コマンドの出力は、次のようになります。

Building the custom-kube-scheduler-webhook binary for Docker (linux)...
Building the docker image: custom-kube-scheduler-webhook:latest...
Sending build context to Docker daemon  262.5MB
Step 1/6 : FROM alpine:latest
....
Successfully built bc4560ae8770
Successfully tagged XXXXXXX.dkr.ecr.us-east-1.amazonaws.com/custom-kube-scheduler-webhook:latest
Pushing the docker image for XXXXXXX.dkr.ecr.us-east-1.amazonaws.com/custom-kube-scheduler-webhook:latest ...
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin XXXXXXX.dkr.ecr.us-east-1.amazonaws.com/custom-kube-scheduler-webhook
WARNING! Your password will be stored unencrypted in /home/ec2-user/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
The push refers to repository [XXXXXXX.dkr.ecr.us-east-1.amazonaws.com/custom-kube-scheduler-webhook]
d7eaac728432: Pushed 
b2d5eeeaba3a: Layer already exists 
latest: digest: sha256:5d65e7f5578d95221e7691cdb8415a8a80b1c9c553684bea3011df39262cbe4d size: 740

Kubernetes Namespace を作成する

mutating pod webhook をデプロイする custom-kube-scheduler-webhook Namespace を作成します。

kubectl create ns $NAMESPACE

証明書と Secrets を作成する

署名付き証明書を作成し、mutating pod webhook の Deployment で利用する Kubernetes の Secrets に格納します。

./deploy/webhook-create-signed-cert.sh \
    --service $SERVICE \
    --secret $SECRET \
    --namespace $NAMESPACE

Kubernetes の Secrets が正常に作成されたことを確認します。

kubectl get secret $SECRET -n $NAMESPACE -o json

webhook をデプロイする

MutatingWebhookConfiguration を作成、適用します。

export WEBHOOK_CONFIG="deploy/custom-kube-scheduler-webhook-config.yaml"
cat deploy/custom-kube-scheduler-webhook-config-template.yaml | \
    deploy/webhook-patch-ca-bundle.sh >  $WEBHOOK_CONFIG
kubectl apply -f $WEBHOOK_CONFIG

webhook をデプロイします。

export WEBHOOK_CONTROLLER="deploy/custom-kube-scheduler-webhook-controller.yaml"
envsubst < deploy/custom-kube-scheduler-webhook-controller-template.yaml > $WEBHOOK_CONTROLLER
kubectl apply -f $WEBHOOK_CONTROLLER

サンプルを用いてテストする

このソリューションが動作することを確認します。まず、Namespace を作成し、アノテーションを付与します。これにより、webhook はこの Namespace で新しい Pod を監視するようになります。

kubectl apply -f - <<EOF
---
apiVersion: v1
kind: Namespace
metadata:
  name: test
  labels:
    custom-kube-scheduler-webhook: enabled 
---
EOF

次に、カスタム Pod スケジューリングのアノテーションを使用して、サンプル用の Deployment を作成します。

kubectl apply -f - <<EOF
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: test
  annotations:
    custom-pod-schedule-strategy: 'karpenter.sh/capacity-type=on-demand,base=2,weight=1:karpenter.sh/capacity-type=spot,weight=3'
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 10
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: public.ecr.aws/nginx/nginx:latest
        imagePullPolicy: Always
        name: nginx
        resources:
          limits:
            cpu:  400m
            memory: 1600Mi
          requests:
            cpu: 400m
            memory: 1600Mi        
        ports:
        - name: http
          containerPort: 80
---
EOF

別のターミナルで次のコマンドを実行して、webhook のログを表示します。

kubectl logs -f -lapp=custom-kube-scheduler-webhook  -n custom-kube-scheduler-webhook         
I0110 13:05:45.187535       1 webhook.go:373] flow=CREATE serviceInstanceNum=67 Found a deployment nginx in namespace test with total replicas 10 and strategy=karpenter.sh/capacity-type=on-demand,base=2,weight=1:karpenter.sh/capacity-type=spot,weight=3
I0110 13:05:45.187561       1 webhook.go:378] flow=CREATE serviceInstanceNum=67 nodeLabelStrategyList=[{karpenter.sh/capacity-type=on-demand 4 1} {karpenter.sh/capacity-type=spot 6 3}]
I0110 13:05:45.195088       1 webhook.go:389] flow=CREATE serviceInstanceNum=67 nodeLabel=karpenter.sh/capacity-type=on-demand currently runs 0 pods
I0110 13:05:45.195107       1 webhook.go:393] flow=CREATE serviceInstanceNum=67 Currently running 0 pods is less than expected 4, scheduling pod on nodeLabel karpenter.sh/capacity-type=on-demand
I0110 13:05:45.195130       1 webhook.go:232] serviceInstanceNum=67 AdmissionResponse: patch=[{"op":"add","path":"/spec/nodeSelector","value":{"karpenter.sh/capacity-type":"on-demand"}}]
I0110 13:05:45.195150       1 webhook.go:311] Ready to write reponse ...
....
I0110 13:05:45.315759       1 webhook.go:378] flow=CREATE serviceInstanceNum=73 nodeLabelStrategyList=[{karpenter.sh/capacity-type=on-demand 4 1} {karpenter.sh/capacity-type=spot 6 3}]
I0110 13:05:45.339624       1 webhook.go:393] flow=CREATE serviceInstanceNum=73 Currently running 0 pods is less than expected 6, scheduling pod on nodeLabel karpenter.sh/capacity-type=spot
I0110 13:05:45.339643       1 webhook.go:232] serviceInstanceNum=73 AdmissionResponse: patch=[{"op":"add","path":"/spec/nodeSelector","value":{"karpenter.sh/capacity-type":"spot"}}]
I0110 13:05:45.339661       1 webhook.go:311] Ready to write reponse ...

このプロジェクトには、ノードタイプごとの Pod の分布を表示するヘルパースクリプトが含まれています。

./check_pod_spread.sh
NAME                     READY   STATUS    RESTARTS   AGE
nginx-6b6769fd96-fwwtk   1/1     Running   0          2m58s
.....
Number of Pods in namespace test is 1
Number of Pods for on-demand is 4
Number of Pods for spot is 6

ここで、サンプル用の Deployment を 10 レプリカから 20 レプリカにスケールします。

kubectl scale deployment nginx  --replicas=20 -n test

Pod の分布を確認し、新たに作成された Pod が指定した割合で配分されているか、検証します。

jp:~/environment/custom-kubernetes-scheduler/admissionwebhook (main) $ ./check_pod_spread.shNAME                     READY   STATUS    RESTARTS   AGE
nginx-6b6769fd96-2qt9h   1/1     Running   0          19s
...
Number of Pods in namespace test is 20
Number of Pods for on-demand is 6
Number of Pods for spot is 14

ごのように、レプリカ数を 2 倍にすると、それに比例して新しい Pod がスケジュールされていることが確認できます。

後片付け

この記事内で作成したリソースを削除するには、以下のコマンドを使用します。

kubectl delete deployment nginx -n test
kubectl delete ns test
kubectl delete -f $WEBHOOK_CONTROLLER
kubectl delete -f $WEBHOOK_CONFIG
kubectl delete secret $SECRET -n $NAMESPACE 
kubectl delete ns $NAMESPACE
# if you created a new EKS cluster, then delete the deleter
eksctl delete cluster  --name $CLUSTER_NAME  --region $AWS_REGION

まとめ

この記事では、mutating pod admission webhook を使用して、ノード間での Pod のスケジューリングをカスタマイズする方法を紹介しました。このソリューションは、データ転送コストを削減するために特定の AZ のノードを優先する、 AZ 間でワークロードを分散する、またはオンデマンドと Spot インスタンスを併用してワークロードを実行するなど、様々なユースケースに利用できます。