Node Affinity を使って Pod を特定のインスタンスタイプで動かす
Posted on
freee では GitHub Actions の self-hosted runners に Kubernetes (EKS) を使っています。主な用途は VPC の外では実行したくない CI やビルドです。
具体的には actions-runner-controller という Custom Contoroller を使って、Pod を Runner として登録しその中でジョブを実行しています。また、ジョブが終わると Pod が作り直されるため、前回の状態を含まないクリーンな環境が保証されます。この仕組みについては、また勉強会などで発表したいと思います。
実際に運用し始めると、特定の用途でマルチコアを使いたいケースが出てきました。 Pod の配置戦略に関する機能はいくつかありますが、今回は Node Affinity を使って Pod を特定のインスタンスタイプで動かすようにしました。
そのときの検証手順を忘れないように残しておきます。なお、検証では actions-runner-controller は使わずに、Kubernetes 標準の機能だけを使っています。
検証環境の情報
Kubernetes のバージョンは v1.15 で、eksctl を使って EKS クラスタを立てています。
今回は CPU bound な処理をする Pod を c5 系のノードに割り当てます。事前準備として eksctl で必要な nodegroup を作成しておきます。
$ eksctl get nodegroup --cluster hello-world
CLUSTER NODEGROUP CREATED MIN SIZE MAX SIZE DESIRED CAPACITY INSTANCE TYPE IMAGE ID
hello-world nodegroup-1 2020-06-20T13:28:26Z 1 3 1 m5.large ami-03964931fd94c2743
hello-world nodegroup-2 2020-06-21T04:16:49Z 1 3 1 c5.large ami-03964931fd94c2743
ラベルの確認
まず Node のラベルを確認します。今回欲しいインスタンスタイプの情報は beta.kubernetes.io/instance-type
に入っています。
$ kubectl get no
NAME STATUS ROLES AGE VERSION
ip-192-168-59-152.ap-northeast-1.compute.internal Ready <none> 20m v1.15.11-eks-af3caf
ip-192-168-60-145.ap-northeast-1.compute.internal Ready <none> 15h v1.15.11-eks-af3caf
$ kubectl get no/ip-192-168-60-145.ap-northeast-1.compute.internal -o json | jq '.metadata.labels'
{
"alpha.eksctl.io/cluster-name": "hello-world",
"alpha.eksctl.io/instance-id": "i-0a177de60093fb89f",
"alpha.eksctl.io/nodegroup-name": "nodegroup-1",
"beta.kubernetes.io/arch": "amd64",
"beta.kubernetes.io/instance-type": "m5.large",
"beta.kubernetes.io/os": "linux",
"failure-domain.beta.kubernetes.io/region": "ap-northeast-1",
"failure-domain.beta.kubernetes.io/zone": "ap-northeast-1d",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "ip-192-168-60-145.ap-northeast-1.compute.internal",
"kubernetes.io/os": "linux"
}
$ kubectl get no/ip-192-168-59-152.ap-northeast-1.compute.internal -o json | jq '.metadata.labels'
{
"alpha.eksctl.io/cluster-name": "hello-world",
"alpha.eksctl.io/instance-id": "i-053cd19eee0021bc2",
"alpha.eksctl.io/nodegroup-name": "nodegroup-2",
"beta.kubernetes.io/arch": "amd64",
"beta.kubernetes.io/instance-type": "c5.large",
"beta.kubernetes.io/os": "linux",
"failure-domain.beta.kubernetes.io/region": "ap-northeast-1",
"failure-domain.beta.kubernetes.io/zone": "ap-northeast-1d",
"kubernetes.io/arch": "amd64",
"kubernetes.io/hostname": "ip-192-168-59-152.ap-northeast-1.compute.internal",
"kubernetes.io/os": "linux"
}
ビルトインで付与されるラベルは次のとおりです(最新バージョンではいくつか増えているようです)。 OS や CPU の命令セットに関するラベルもあります。
kubernetes.io/hostname
failure-domain.beta.kubernetes.io/zone
failure-domain.beta.kubernetes.io/region
beta.kubernetes.io/instance-type
kubernetes.io/os
kubernetes.io/arch
Node Affinity を設定する
今回は c5.large で実行されるように Node Affinity を設定します。ちなみに、設定する前は 2 つのノードに均等に配置されているのがわかります。
$ kubectl get po -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
nginx-deployment-68c7f5464c-f7r6c ip-192-168-59-152.ap-northeast-1.compute.internal
nginx-deployment-68c7f5464c-f9d46 ip-192-168-59-152.ap-northeast-1.compute.internal
nginx-deployment-68c7f5464c-lhfh8 ip-192-168-60-145.ap-northeast-1.compute.internal
nginx-deployment-68c7f5464c-pwx7p ip-192-168-60-145.ap-northeast-1.compute.internal
PodSpec に nodeAffinity
の設定を追加します。 requiredDuringSchedulingIgnoredDuringExecution
を指定すると条件に一致するノードにしか配置されなくなります。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 4
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: beta.kubernetes.io/instance-type
operator: In
values:
- c5.large
apply すると条件に一致しなくなった 2 つの Pod がすぐに作り直され、すべての Pod が c5.large のノードに再配置されました。
$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-deployment-68c7f5464c-f9d46 1/1 Running 0 26m
nginx-deployment-68c7f5464c-lhfh8 1/1 Running 0 26m
nginx-deployment-68c7f5464c-pwx7p 1/1 Running 0 26m
nginx-deployment-6c5c8b94f8-b29lc 0/1 ContainerCreating 0 4s
nginx-deployment-6c5c8b94f8-vghw6 0/1 ContainerCreating 0 4s
$ kubectl get po -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
nginx-deployment-6c5c8b94f8-b29lc ip-192-168-59-152.ap-northeast-1.compute.internal
nginx-deployment-6c5c8b94f8-n9lhr ip-192-168-59-152.ap-northeast-1.compute.internal
nginx-deployment-6c5c8b94f8-rvb6l ip-192-168-59-152.ap-northeast-1.compute.internal
nginx-deployment-6c5c8b94f8-vghw6 ip-192-168-59-152.ap-northeast-1.compute.internal
条件に一致するノードがない場合も検証してみます。 c5.large のノードを cordon して Pod がスケジューリングされないようにします。
$ kubectl cordon ip-192-168-59-152.ap-northeast-1.compute.internal
node/ip-192-168-59-152.ap-northeast-1.compute.internal cordoned
$ kubectl get no
NAME STATUS ROLES AGE VERSION
ip-192-168-59-152.ap-northeast-1.compute.internal Ready,SchedulingDisabled <none> 4h16m v1.15.11-eks-af3caf
ip-192-168-60-145.ap-northeast-1.compute.internal Ready <none> 19h v1.15.11-eks-af3caf
この状態で再度 apply すると、すべての Pod が Pending になります。
$ kubectl delete deploy/nginx-deployment
deployment.extensions "nginx-deployment" deleted
$ kubectl apply -f nginx.yaml
deployment.apps/nginx-deployment created
$ kubectl get po
NAME READY STATUS RESTARTS AGE
nginx-deployment-6c5c8b94f8-8fsq9 0/1 Pending 0 28s
nginx-deployment-6c5c8b94f8-b92x7 0/1 Pending 0 28s
nginx-deployment-6c5c8b94f8-hbv8c 0/1 Pending 0 28s
nginx-deployment-6c5c8b94f8-sbsgn 0/1 Pending 0 28s
Pod のイベントを見ると次のような Warning が出ており、空いている m5.large のノードに割り当てられなかったことがわかります。
0/2 nodes are available: 1 node(s) didn't match node selector, 1 node(s) were unschedulable.
このクラスタは Cluster Autoscaler を実行していたので、数分後に c5.large のノードが増えて Running になりました。
優先度を付けた Node Affinity を設定する
実際の運用では、c5.large に割り当てられなかったら m5.large に割り当てたほうが可用性は上がります。このように優先順位を付けるには preferredDuringSchedulingIgnoredDuringExecution
を使います。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
spec:
replicas: 4
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:latest
ports:
- containerPort: 80
affinity:
nodeAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 1
preference:
matchExpressions:
- key: beta.kubernetes.io/instance-type
operator: In
values:
- c5.large
- weight: 2
preference:
matchExpressions:
- key: beta.kubernetes.io/instance-type
operator: In
values:
- m5.large
先ほどと同じように c5.large のノードを cordon して apply しても、今度はすぐに Running になります。割り当てられたノードを見ると m5.large に配置されたことがわかります。
$ k get po -o=jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
nginx-deployment-7ffc895f84-7bzdz ip-192-168-60-145.ap-northeast-1.compute.internal
nginx-deployment-7ffc895f84-c277w ip-192-168-60-145.ap-northeast-1.compute.internal
nginx-deployment-7ffc895f84-gb28r ip-192-168-60-145.ap-northeast-1.compute.internal
nginx-deployment-7ffc895f84-kx7bz ip-192-168-60-145.ap-northeast-1.compute.internal
おわりに
Node Affinity とは直接関係ないのですが、今回のように c5.large を必ず起動させたい場合はインスタンスタイプ(もしくはファミリー)ごとに nodegroup を作る必要があります。
Auto Scaling Group の MixedInstancesPolicy は、それぞれのインスタンスタイプの最低台数は指定できない? たとえば、m5.large, r5.large, c5.large の組み合わせでそれぞれ最低 1 台ずつは起動したいケース。これができれば、インスタンスファミリーごとに nodegroup を作らずに済むんだけどなぁ。
— Manabu Sakai (@manabusakai) June 18, 2020
eksctl で複数のインスタンスタイプを混ぜた nodegroup (MixedInstancesPolicy) を作ることはできますが、それぞれのインスタンスタイプの最低台数を指定できません。
nodeGroups:
- name: nodegroup-1
instanceType: mixed
desiredCapacity: 3
instancesDistribution:
instanceTypes:
- m5.large
- r5.large
- c5.large
たとえば上のような nodegroup を作っても、3 台のインスタンスタイプは起動してみるまでわかりません(それぞれ 1 台ずつ起動するわけではない)。