この記事は何?
KEP-3633のバックポート実装を以下のリポジトリで開発しました。
筆者がKEP-3633に注目した動機と、上記バックポート実装の使い方を簡単に解説します。
免責事項
記事全体を通して自身の業務を踏まえたモデルストーリーに沿って説明しますが、実際の業務そのものではなく、説明のしやすさのため一部改変しています。
また、注意は払っていますが、改変により矛盾点を含んでいる可能性もあるので実業務への応用はご自身の責任で十分な検証を行ってください。
KEP-3633とは
この節の内容は現在実装作業中の提案内容にもとづいています。今後の実装作業や議論の結果仕様が変更され変わることがあります。最新の内容はKEP-3633や関連するissue/pull-requestを確認してください。
そもそもKEPとは Kubernetes Enhancement Proposals の略で、Kubernetesプロジェクトでの取り組みについて提案とその後の議論、方針決定などを行うための方法1です。
KubernetesプロジェクトはSIG(Special Interest Groups)に分かれて作業を行っており、KEPも多くがそれぞれのSIGに分かれいます。KEP-3633はその内容からsig-schedulingに割り当てられています。2
KEP-3633では podAffinity
や podAntiAffinity
において matchLabelKeys
/mismatchLabelKeys
というフィールドで代表される機能を実装しようと提案しています。
podAffinity
は、Podに設定され、「設定されたPod」と「すでに存在しているPodの集団」を「特定のNodeのグループ分けについて同じグループのNode」にスケジュールするための機能です。 podAntiAffinity
は「同じグループのNode」を「異なるグループのNode」に変えたものです。
podAffinity
において、前述の「すでに存在しているPodの集団」を特定するために labelSelector
というフィールドがあります3。このフィールドは汎用の LabelSelector
が設定できますが、ラベルキーと値が既知である必要があります。しかし、Podには動的に割り当てられるラベルもあります。例えばDeploymentによって管理されるPodには pod-template-hash
というキーを持つラベルが付与され、Deploymentの世代をラベルで区別できるようになっています。こうったラベルが自身と共通の値を持つPodの集団との間に podAffinity
/ podAntiAffinity
を設定したい状況は後述のユースケースなどの場合に存在していました。そこでKEP-3633では matchLabelKeys
という文字列配列フィールドを追加してそのようなラベルキーを設定できるようにしようとしています。これは実は topologySpreadConstraints
にKEP-3243で導入されています。ただしKEP-3633では、「値が一致するPodの集団」のみならず「値が一致しないPodの集団」を設定できるように mismatchLabelKeys
というフィールドも併せて実装することが検討されています。
筆者が注目したきっかけ
筆者が注目したきっかけは、Argo Rolloutsの導入の検討でした。もともと既存のKubernetesクラスタでアプリケーションPodに podAntiAffinity
を利用したavailability zone分散構成を行っているところにArgo Rolloutsを導入すると、肝心のCanaryReleaseやBlue/Green Deploymentを行う際にPodが起動できない問題がありました。
もう少し詳しく説明します。クラウド環境では、一つの地域(Region)でサービスを開始するとその中でも電源やインターネット接続回線などの低レベルインフラを独立させた区画を複数設けて、仮想マシンなどの計算リソースをそれらの区画に分散させることで可用性を向上します。AWSではavailability zone、Azureでは可用性ゾーンなどと呼ばれているようです4。
クラウドベンダーが提供するサービスは何も設定しなくても複数のAZに分散されていることが多いですが、利用者が計算リソースを管理する際は複数のAZに分散する責任も利用者にあります。Kubernetesでアプリケーションを稼働する場合、複数のAZにNodeを作成してクラスタに追加しておくだけでは不十分な場合5もあります。そこでPodにも podAntiAffinity
を設定して複数のAZに配置されたNodeの異なるAZにあるNodeでPodを起動するようにします6。
以下にDeploymentマニフェストを例示します。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
namespace: default
labels:
app: nginx
spec:
replicas: 3
selector:
matchLabels:
app: nginx
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1 # 伏線
template:
metadata:
labels:
app: nginx
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- # AWS環境の一般的な構成では以下キーのラベルにAZ名を値として持つ
topologyKey: topology.kubernetes.io/zone
# 同じDeployment管理下のPodを対象とする
labelSelector:
matchLabels:
app: nginx
containers:
- name: nginx
image: nginx:mainline-alpine
imagePullPolicy: Always
3つのAZに十分なリソースのNodeを持つKubernetesクラスタであれば上記Deploymentで作成されるPodは3つとも異なるAZに配置されます。
さて、上記のような構成でアプリケーションPodのAZ分散を実現しているところに、Argo Rolloutsの導入を検討しました。Argo Rollouts導入の目的はBlue/Green Deployです。
Blue/Green Deployは筆者の環境では以下のような流れで新しいアプリをデプロイすることとしました。
- 新しい設定(典型的にはコンテナイメージの更新)のPod(以下、新Pod)を最終的に起動する数だけ起動する。この時はまだ古い設定のPod(以下、旧Pod)にすべてのトラフィックが送信されている。
- 新Podに新規トラフィックを送信するようLoadBalancerの設定を変更する。
- 動作確認などが終わり、旧Podへの切り戻しが不要になったら旧Podを削除する。
ここで問題になったのが手順の1.です。旧Podを起動したまま新Podを起動する必要がありますが、 podAntiAffinity
の設定は同時に起動している旧Podと新Podの両方が対象となるため、あとから起動する新Podは旧Podと異なるAZに配置せねばならず、そんなAZのNodeはないため、新PodはどのNodeにも割り当てることができないままになってしまいます。実は似たような現象はDeploymentでも起こりえますが、先ほど例示したマニフェストには回避策が打ってありました。それは # 伏線
とコメントがつけられたフィールド、 spec.strategy.rollingUpdate.maxUnavailable
であり、値は1に設定されています。これは、RollingUpdate時に「正常稼働しているPodの数を replicas
の値からいくつ減らしてもよいか?」という設定で、これを1に設定しているのは、「(replicas) - 1
までだったらPodが利用不能でもよい」と指示しています。これは実はAZ分散の副作用を回避してRollingUpdateができるようにするための設定です。これがないと、デフォルト値の 25%
が使われますが、3 Podの25%は 0.75
で1に満たず、小数点以下は切り捨てなので0を設定するのと同じになります。この場合、RollingUpdateにおいても旧Podを一つも消さずに新しいPodを起動する必要があり、そのPodは割り当てるNodeが見つかりません。Deploymentの場合は1つまでなら先にPodを消すことを許容することでRollingUpdateを進めることができましたが、Blue/Green Deployでは「すべての新Podを旧Podからトラフィックを移動する前に起動する」という要件上、そういった回避はできません。
ここでインターネット上の情報を調べたり、 topologySpreadConstraints
への移行なども検討しましたが、その際に topologySpreadConstraints
に matchLabelKeys
というフィールドの実装が進められており、現在Beta(デフォルトenable)であること7を知りました。できれば変更を少なくしたいため、 podAntiAffinity
で同様の新機能はないか探したところ、 KEP-3633を発見した、というわけです。
何を作ったの
KEP-3633では、現在8、matchLabelKeys
/ mismatchLabelKeys
の挙動として、「Podを作成する前に当該ラベルキーと値をLabelSelectorに追加する」という動作で実装する方向になっています。ところで「Podを作成する前に変更を加える」といえばMutatingAdmissionWebhookですよね。上記の動作であればMutatingAdmissionWebhookを作成して同じ動作を行うことができます。 matchLabelKeys
/ mismatchLabelKeys
のようなフィールドを追加できるわけではないので、 podAffinity
/ podAntiAffinity
フィールドに書いている内容を丸ごとアノテーションに書いてもらい、 matchLabelKeys
/ mismatchLabelKeys
についてはPod自身のラベルの値を参照して本物の podAffinity
/ podAntiAffinity
フィールドに設定することにしました。
作成したものが以下のリポジトリにまとまっています:
Helmチャートを用意していますので以下のコマンドでインストールできます9。
helm upgrade -i -n kube-system kep3633alt kep3633alt --repo https://10hin.github.io/kep-3633-alt
インストールができたらPodマニフェストやPodTemplateを以下のように書くことで:
apiVersion: v1
kind: Pod
metadata:
annotations:
kep-3633-alt.10h.in/podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution: |
[
{
"labelSelector": {
"matchLabels": {
"app": "nginx"
}
},
"topologyKey": "topology.kubernetes.io/zone",
"matchLabelKeys": [
"pod-template-hash"
]
}
]
name: nginx
labels:
app: nginx
pod-template-hash: UNEXPECTABLEVALUE
spec:
# ...
次のようなPodが起動します。
apiVersion: v1
kind: Pod
metadata:
annotations:
kep-3633-alt.10h.in/podAntiAffinity.requiredDuringSchedulingIgnoredDuringExecution: |
# reduced
name: nginx
labels:
app: nginx
pod-template-hash: UNEXPECTABLEVALUE
spec:
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: nginx
pod-template-hash: UNEXPECTABLEVALUE
topologyKey: topology.kubernetes.io/zone
# ...
インストール方法、使い方の詳細はREADME.mdに書いています。
最後に
興味があればぜひkep-3633-altを使ってみてください。
kep-3633-altを利用するユースケースがKEP-3633に書かれていないなどの場合はメンテナに相談の上KEP-3633への追加を検討してください。
KEPにユースケースがあること、実装が望まれているものであることを伝えることもOSSへの貢献だと思います。また、KEP-3633の実装・GA昇格を後押しすることになるので、GA昇格を待ち望んでいる筆者にとってもうれしいです。
もしkep-3633-altに問題があればissue/prでお知らせください。
-
https://github.com/kubernetes/enhancements/tree/45f1297fa1c2d9d5495689a7ac182f9cbf9b3d23/keps#kubernetes-enhancement-proposals-keps ↩
-
あまりこれ以降でSIGの割り当てを気にすることはないのですが、KEPのディレクトリが主にSIGで分けられているので紹介しました。 ↩
-
別のNamespaceのPodも選択できるように
namespaces
/namespaceSelector
フィールドもありますが、話を簡単にするために触れないことにします。 ↩ -
筆者はAWSを利用することが多いので以下ではavailability zone、短くAZなどと呼びます。 ↩
-
これは詳しく言うと求められるサービスレベルによります。複数のAZにまたがって十分なNodeがあれば、単一AZに障害が発生した場合でも、ほかのAZのNodeにKubernetesがPodをスケジューリングしてくれます。しかし、AZ障害発生から「(当該AZの)Nodeに問題があるのでPodを削除しよう」とKubernetesが判断するまでには時間がかかります。AZ障害の影響でロードバランサから当該AZで起動したPodに到達できなくなると仮定し、あるアプリケーションのPodがすべて1つのAZで起動していたとすると、そのAZの障害ではすべてのリクエストが何らかのエラーになってしまうことになります。そのエラーは前述のPodの再作成(別ノードでの起動)により自動的に復旧することが見込まれますが、その間はサービス停止となります。このような状況が許容できないようなサービスレベルを設定している場合は、事前にPodが複数のAZに分散起動していることを確実にしておきたいところです。 ↩
-
この目的で
podAntiAffinity
を使うとスケールアウトと相性が悪いことは知られており、その問題を解消するのがtopologySpreadConstraints
なのですが、オートスケールなどをしない場合はpodAntiAffinity
でも十分に対応できます。筆者が管理する環境の場合、Javaアプリケーションが多く、Javaはその設計上、インスタンスごとのオーバーヘッドが大きいため、小さなリソースで複数のPodに分散するのはリソース効率が悪化するため避けています。 ↩ -
2023年7月ごろ ↩
-
今後のリリースも利用する予定があれば(とてもうれしいです)Helmリポジトリを登録することもできます。 ↩