この記事は 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 には、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=2
、weight=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 を割り当てます。
概念実証の手順
準備
チュートリアルを完了するために、以下を準備してください。
注 : この記事内の 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
コマンドの出力は、次のようになります。
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 インスタンスを併用してワークロードを実行するなど、様々なユースケースに利用できます。