Argo Rollouts と Istio を使ったカナリアリリースの実現

こんにちは、SREグループのカンタンです!

GO株式会社ではアプリケーションサーバKubernetes で運用することが多いですが、今までは Deployment によるローリングアップデートを利用しデプロイを行っていました。 マイクロサービス構成を利用しているため一つのアプリケーションのデプロイの影響範囲が限定されていて今まで問題なく運用できていましたが、 アプリケーションの規模が大きくなり提供している機能が多様化してきたため、デプロイの影響範囲を最小限に抑えるためにカナリアリリースを導入することになりました。

今回は、Argo RolloutsIstio を使ってカナリアリリースを簡単に実現するためのGO株式会社のやり方について紹介します。

Argo Rolloutsとは

Argo Rolloutsカナリアリリースやブルーグリーンデプロイメントなど様々なデプロイ戦略を実現するためのツールです。 Rollout というカスタムリソースでデプロイ戦略を定義すると、Kubernetes クラスタで動いている Argo Rollouts コントローラがそれに従ってデプロイを行います。

Rollout は Deployment と同じ役割を持っていて、Argo Rollouts 初期リリース時には Deployment を Rollout に完全に置き換える必要がありましたが、現在は Rollout が Deployment を参照することで共存できるようになり、既存ワークロードに導入しやすくなっています。

カナリアリリース実施の際、トラフィックを徐々に切り替えるために Argo Rollouts が Istio など様々な Service Mesh や Ingress コントローラと連携できます。

argo-rollouts

ダッシュボードCLI が提供されているため、デプロイの状況をリアルタイムで確認し操作を簡単に行えます。

dashboard

Argo Rollouts が様々な機能を提供していますが、今回は Istio と連携したカナリアリリースの設定方法について紹介します。

インストールと権限設定

GO株式会社では Helm を使ってツールをインストールすることが多いため、Argo Rollouts の正式な Helm チャートを使ってインストールします。

helm repo add argo https://argoproj.github.io/argo-helm
helm install -f values.yaml argo-rollouts argo/argo-rollouts

values.yaml は以下のように設定します。

providerRBAC:
  providers: # istio のみを有効にする
    istio: true
    smi: false
    ambassador: false
    awsLoadBalancerController: false
    awsAppMesh: false
    traefik: false
    apisix: false
    contour: false
    glooPlatform: false
    gatewayAPI: false

controller:
  resources:
    limits:
      memory: 512Mi
      ephemeral-storage: 1Gi
    requests:
      cpu: 100m
      memory: 512Mi
  pdb:
    enabled: true
    minAvailable: 1
  metrics:
    enabled: true

カナリアリリースの状況を確認したり操作したりするために Argo Rollouts のダッシュボードや CLI を使いますが、裏では Rollout カスタムリソースに対して操作しているため Kubernetes の権限管理 (RBAC) が使えてとても便利です。 例えばデプロイの状況を確認できる rollouts-reader ロールとデプロイの操作ができる rollouts-writer ロールを以下のように定義できます。

kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: rollouts-reader
rules:
  - apiGroups:
      - argoproj.io
    resources:
      - rollouts
      - analysisruns
      - experiments
      - rollouts/status
    verbs:
      - get
      - list
      - watch

---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: rollouts-writer
rules:
  - apiGroups:
      - argoproj.io
    resources:
      - rollouts
      - analysisruns
      - experiments
    verbs:
      - get
      - list
      - watch
  - apiGroups:
      - argoproj.io
    resources:
      - rollouts
      - rollouts/status
    verbs:
      - get
      - patch

以下のようなコマンドを実行することで、付与されたロールに応じてデプロイの確認と操作ができます。

kubectl argo rollouts dashboard
open http://localhost:3100

Rollout 設定

カナリアリリース用の Rollout リソースを設定する前に、以下のような一般的な構成から始めます。

architecture-initial

my-app-dep Deployment が my-app-dep-aaa ReplicaSet を作成し、ReplicaSet が Pod を作成しています。my-app-hpa HorizontalPodAutoscaler のスケール設定を元に Kubernetes の HPA コントローラが Deployment のレプリカ数を調整しています。 トラフィックが Istio の VirtualService によって my-app-svc Service にルーティングされ Pod まで到達しています。

サンプルを以下に示します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app-dep
spec:
  selector:
    matchLabels:
      app: my-app
  strategy:
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
    type: RollingUpdate
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: app
          image: "my-image:1"
          ports:
            - containerPort: 8080
              name: http
              protocol: TCP

---
apiVersion: v1
kind: Service
metadata:
  name: my-app-svc
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: http
  selector:
    app: my-app

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          averageUtilization: 70
          type: Utilization
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app-dep

---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: my-app-vs
spec:
  gateways:
    - istio-system/ingress-gateway
  hosts:
    - www.example.com
  http:
    - route:
        - destination:
            host: my-app-svc
            port:
              number: 80

Rollout を導入すると、構成が以下のように変わります。

architecture-overall

  • Rollout が Stable ReplicaSet と Canary ReplicaSet を作る
  • Deployment はあくまでも Rollout の参照情報として利用される(Pod のスペックを定義するため)
  • HPA が Rollout に対して設定される
  • 通信を Stable と Canary に分けるため、Stable と Canary の Service を用意し Virtual Service に設定する
  • Argo Rollouts コントローラが Rollout の状態を常に監視し、必要に応じて他のリソースを操作する
    • カナリアリリース実施時、ReplicaSet をスケールアウト/イン、VirtualService と Service の設定を変更
    • HPA の指示にしたがって、ReplicaSet のスケールアウト/インを行う

まずは新しい Canary Service を以下のように設定します。

apiVersion: v1
kind: Service
metadata:
  name: my-app-canary-svc
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 80
      protocol: TCP
      targetPort: http
  selector:
    app: my-app

そして Virtual Service を以下のように変更します。最初はトラフィックを全て Stable に向けます。

apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: my-app-vs
spec:
  ...
  http:
    - route:
        - destination:
            host: my-app-svc
            port:
              number: 80
+         weight: 100
+       - destination:
+           host: my-app-canary-svc
+           port:
+             number: 80
+         weight: 0

全てのリソースが適用されたら、Rollout を定義します。コメントに記載されている通り、Stable Service、Canary Service、Virtual Service を指定します。

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app-rollout
spec:
  # 元のDeploymentの参照
  workloadRef:
    apiVersion: apps/v1
    kind: Deployment
    name: my-app-dep
    scaleDown: never

  # Rollout対象のPod
  selector:
    matchLabels:
      app: my-app

  # opinionated
  minReadySeconds: 5

  # Canaryリリースの設定
  strategy:
    canary:
      # opinionated
      minPodsPerReplicaSet: 2
      scaleDownDelaySeconds: 30
      abortScaleDownDelaySeconds: 30

      # stableサービスの設定
      stableService: my-app-svc # 利用する Service を指定
      stableMetadata:
        annotations:
          role: stable
        labels:
          role: stable

      # canaryサービス
      canaryService: my-app-canary-svc # 利用する Service を指定
      canaryMetadata:
        annotations:
          role: canary
        labels:
          role: canary

      # Istio 設定
      trafficRouting:
        istio:
          virtualService:
            name: my-app-vs # 利用する VirtualService を指定

      # トラフィック切り替えのステップ
      steps: ... # 以下に参考

カナリアリリースが steps フィールドに定義されたステップに従って実施されます。setWeightトラフィックの割合を変更し、 pause で少し待つようにできます。

例えばカナリアリリースを自動的に行う場合は以下のように設定できます。Argo Rollouts コントローラが Canary Pod を少しずつ増やしながら VirtualService の設定を変更しトラフィックを切り替えます。

      steps:
        # DBのコネクション数が急増しないように徐々にPodを増やす (解説は以下を参照)
        - setWeight: 1
        - setWeight: 10
        - setWeight: 20
        - setWeight: 30
        - setWeight: 40
        - setWeight: 50
        - setWeight: 60
        - setWeight: 70
        - setWeight: 80
        - setWeight: 90
        - setWeight: 100

        # 15秒待つことでIstioの設定反映時間に関連する503エラーを防ぐ (解説は以下を参照)
        - pause: {duration: 15s}

カナリアリリースを手動で行う場合は以下のように設定できます。

  • 新しい Pod がクラッシュしないで起動することを確認するため、Canary Pod を2つにスケールアウト
  • Argo Rollouts のダッシュボードまたは CLI からの手動アクションを待つ
  • Canary Pod 数をトラフィック割合に合わせる
  • 1% のトラフィックを Canary Service に向かい手動アクションを待つ
  • トラフィックを 10% に変更し手動アクションを待つ
  • トラフィックを 50% まで変更し手動アクションを待つ
  • トラフィックを 100% まで変更する
      steps:
        # Canary Pod を2つにスケールアウト
        - setCanaryScale:
            replicas: 2

        # 手動で承認を待つ
        - pause: {}

        # Canary Pod 数をトラフィック割合に合わせる
        - setCanaryScale:
            matchTrafficWeight: true

        # トラフィックを 1% に変更
        - setWeight: 1
        - pause: {} # 手動で承認を待つ

        # トラフィックを 10% に変更
        - setWeight: 10
        - pause: {} # 手動で承認を待つ

        # トラフィックを 50% まで変更
        - setWeight: 30 # DBコネクション数の急増を防ぐため
        - setWeight: 50
        - pause: {} # 手動で承認を待つ

        # トラフィックを 100% まで変更
        - setWeight: 70 # DBコネクション数の急増を防ぐため
        - setWeight: 90 # DBコネクション数の急増を防ぐため
        - setWeight: 100

        # 15秒待つことでIstioの設定反映時間と関連する503エラーを防ぐ (解説は以下を参照)
        - pause: {duration: 15s}

最後に、HPA の設定を Rollout に向かうように変更します。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
spec:
  ...
  scaleTargetRef:
-   apiVersion: apps/v1
-   kind: Deployment
-   name: my-app-dep
+   apiVersion: argoproj.io/v1alpha1
+   kind: Rollout
+   name: my-app-rollout

これでカナリアリリースの設定が完了しました!Docker イメージのバージョンを変更するなど元の Deployment を変更するとカナリアリリースが開始されます。 ダッシュボードを開いてリリースの状況確認と操作ができます。

dashboard-real

注意点と tips

Argo Rollouts の導入に伴って気づいた注意点と tips を以下にまとめます。

急激な Pod 増加に注意

以下のようにトラフィックを一気に 100% まで切り替える場合、Canary Pod が急に増え DB のコネクション数などが急増し障害が発生する可能性があります (参考)。 setWeight を細かく設定することで Pod が少しずつ増えていくため、急激な増加を防げます。

      # Pod が急に増えてしまう
      steps:
        - setWeight: 100
      # または単純に:
      # steps: {}

      # Pod が徐々に増える
      steps:
        - setWeight: 1
        - setWeight: 20
        - setWeight: 40
        - setWeight: 60
        - setWeight: 80
        - setWeight: 100

503 エラー

トラフィック切り替えの最後に少し待たないと 503 エラーが発生することがありました。 原因をはっきりわかっていませんがトラフィック切り替えの最後に以下のように Stable Service の Pod を Canary の Pod に置き換え、VirtualService と Service の設定を続けて変更し、Istio が設定を反映するまでに時間が必要だと考えられます。15秒程度待つことで解消しました。

  1. 100% のトラフィックが Canary Service に向かうように VirtualService の weight が変更される
  2. Stable Service が Canary Pod に向かうように Service の selector ラベルが変更される
  3. 100% のトラフィックが Stable Service に向かうように VirtualService の weight が変更される

コントローラのダウンタイム

トラフィックが Argo Rollouts コントローラを通っているわけではないため、コントローラが落ちてもダウンタイムが発生することはありません。 カナリアリリース途中にコントローラが落ちると次のステップに進めたりロールバックしたりすることができませんが、コントローラが再起動するとそのまま再開できます。

ただし HPA によるレプリカ数の変更はコントローラが行うため、長いダウンタイムが発生してしまうとオートスケールができなくなりますので注意が必要です。

オペレーションの変更

Deployment ではなく Rollout を操作する必要があるため、オペレーションが変わります。

再起動方法が変わります

  • kubectl rollout restart deployment/xxx ではなくて kubectl argo rollouts restart xxx コマンドを利用する必要がある
  • Deployment を再起動すると新しいカナリアリリースが始まるだけで再起動されない
  • 1つの Pod を削除してから新しい Pod を作成するため、最低でも 2 つの Pod が必要

Pod Disruption Budget (PDB):Stable 用の PDB と Canary 用の PDB を分ける必要があります。 そうしないと再起動の際に Stable/Canary Pod が全て削除される可能性があります (PDB が合計の Pod 数しか見ないため、Stable Pod さえ起動していれば Canary Pod が削除されてしまう可能性がある)。

CPU/メモリの変更:Deployment を変更してからカナリアリリースを実施しないと反映されない

ダウンタイムなしの Rollout 移行方法

ダウンタイムなく Deployment から Rollout に移行するための手順を以下にまとめます。 リソースの内容は上記の「Rollout 設定」をご参照ください。

  • Canary Service を作成
  • VirtualService を変更 (Canary Service 向けの destination を追加)
  • Rollout を paused 状態で作成する。この時点ではRollout の Pod は作成されず、トラフィックが切り替わらない
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app-rollout
spec:
+ paused: true
  ...
  • Pod が急に減らないように HPA の minReplicas を Deployment の現在の Pod 数に設定する
export HPA_MIN=$(kubectl get hpa my-app-hpa -ojson | jq .spec.minReplicas)
export REPLICAS=$(kubectl get deployment my-app-dep -ojson | jq .spec.replicas)

kubectl patch hpa my-app-hpa -p '{"spec":{"minReplicas": '$REPLICAS'}}'
  • Rollout のレプリカ数を Deployment の現在のレプリカ数に設定する。Pod はまだ作成されない
kubectl patch rollout my-app-rollout --type merge -p '{"spec":{"replicas": '$REPLICAS'}}'
  • Rollout を unpause することで Rollout の Pod が作成され、全ての Pod が起動したらトラフィックが一気に Rollout Pods に切り替わる。Deployment の Pod は残るがトラフィックが来なくなる
kubectl patch rollout my-app-rollout --type merge -p '{"spec":{"paused": false}}'
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: my-app-rollout
spec:
- paused: true
+ paused: false
  ...
  • Deployment のレプリカ数を 0 にする
kubectl patch deployment my-app-dep -p '{"spec":{"replicas": 0}}'
  • HPA を Rollout に向かうように変更する
kubectl patch hpa my-app-hpa \
  -p '{"spec":{"scaleTargetRef":{"apiVersion": "argoproj.io/v1alpha1", "kind": "Rollout", "name": "my-app-rollout"}}}'
  • HPA の minReplicas を元に戻す。
kubectl patch hpa my-app-hpa -p '{"spec":{"minReplicas": '$HPA_MIN'}}'

これで Deployment から Rollout に移行することができました!

最後に

Argo Rollouts と Istio を使ったカナリアリリースの実現方法について紹介しました。 カナリアリリースを導入することでデプロイの影響範囲を最小限に抑えることができ、障害のリスクを軽減できます。 GO株式会社のやり方と導入にあたって気づいた注意点と tips を共有したため、参考にしていただければ幸いです。

今後は Argo Rollouts の Analysis 機能を活かし Grafana Mimir で集めたメトリクスを利用しエラー率増加の際に自動ロールバックするカナリアリリースを実現することを検討しています!