LoginSignup
0
0

RollingUpdateやBlue/Green DeployをスムーズにするKEP-3633のバックポート実装を作った

Posted at

この記事は何?

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では podAffinitypodAntiAffinity において 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マニフェストを例示します。

deployment.yaml
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は筆者の環境では以下のような流れで新しいアプリをデプロイすることとしました。

  1. 新しい設定(典型的にはコンテナイメージの更新)のPod(以下、新Pod)を最終的に起動する数だけ起動する。この時はまだ古い設定のPod(以下、旧Pod)にすべてのトラフィックが送信されている。
  2. 新Podに新規トラフィックを送信するようLoadBalancerの設定を変更する。
  3. 動作確認などが終わり、旧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 への移行なども検討しましたが、その際に topologySpreadConstraintsmatchLabelKeys というフィールドの実装が進められており、現在Beta(デフォルトenable)であること7を知りました。できれば変更を少なくしたいため、 podAntiAffinity で同様の新機能はないか探したところ、 KEP-3633を発見した、というわけです。

何を作ったの

KEP-3633では、現在8matchLabelKeys / 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でお知らせください。

  1. https://github.com/kubernetes/enhancements/tree/45f1297fa1c2d9d5495689a7ac182f9cbf9b3d23/keps#kubernetes-enhancement-proposals-keps

  2. あまりこれ以降でSIGの割り当てを気にすることはないのですが、KEPのディレクトリが主にSIGで分けられているので紹介しました。

  3. 別のNamespaceのPodも選択できるように namespaces/namespaceSelector フィールドもありますが、話を簡単にするために触れないことにします。

  4. 筆者はAWSを利用することが多いので以下ではavailability zone、短くAZなどと呼びます。

  5. これは詳しく言うと求められるサービスレベルによります。複数のAZにまたがって十分なNodeがあれば、単一AZに障害が発生した場合でも、ほかのAZのNodeにKubernetesがPodをスケジューリングしてくれます。しかし、AZ障害発生から「(当該AZの)Nodeに問題があるのでPodを削除しよう」とKubernetesが判断するまでには時間がかかります。AZ障害の影響でロードバランサから当該AZで起動したPodに到達できなくなると仮定し、あるアプリケーションのPodがすべて1つのAZで起動していたとすると、そのAZの障害ではすべてのリクエストが何らかのエラーになってしまうことになります。そのエラーは前述のPodの再作成(別ノードでの起動)により自動的に復旧することが見込まれますが、その間はサービス停止となります。このような状況が許容できないようなサービスレベルを設定している場合は、事前にPodが複数のAZに分散起動していることを確実にしておきたいところです。

  6. この目的で podAntiAffinity を使うとスケールアウトと相性が悪いことは知られており、その問題を解消するのが topologySpreadConstraints なのですが、オートスケールなどをしない場合は podAntiAffinity でも十分に対応できます。筆者が管理する環境の場合、Javaアプリケーションが多く、Javaはその設計上、インスタンスごとのオーバーヘッドが大きいため、小さなリソースで複数のPodに分散するのはリソース効率が悪化するため避けています。

  7. KEP-3243

  8. 2023年7月ごろ

  9. 今後のリリースも利用する予定があれば(とてもうれしいです)Helmリポジトリを登録することもできます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0