こんにちは、SREグループのカンタンです!
GO株式会社ではアプリケーションサーバを Kubernetes で運用することが多いですが、今までは Deployment によるローリングアップデートを利用しデプロイを行っていました。 マイクロサービス構成を利用しているため一つのアプリケーションのデプロイの影響範囲が限定されていて今まで問題なく運用できていましたが、 アプリケーションの規模が大きくなり提供している機能が多様化してきたため、デプロイの影響範囲を最小限に抑えるためにカナリアリリースを導入することになりました。
今回は、Argo Rollouts と Istio を使ってカナリアリリースを簡単に実現するためのGO株式会社のやり方について紹介します。
Argo Rolloutsとは
Argo Rollouts はカナリアリリースやブルーグリーンデプロイメントなど様々なデプロイ戦略を実現するためのツールです。 Rollout というカスタムリソースでデプロイ戦略を定義すると、Kubernetes クラスタで動いている Argo Rollouts コントローラがそれに従ってデプロイを行います。
Rollout は Deployment と同じ役割を持っていて、Argo Rollouts 初期リリース時には Deployment を Rollout に完全に置き換える必要がありましたが、現在は Rollout が Deployment を参照することで共存できるようになり、既存ワークロードに導入しやすくなっています。
カナリアリリース実施の際、トラフィックを徐々に切り替えるために Argo Rollouts が Istio など様々な Service Mesh や Ingress コントローラと連携できます。
ダッシュボードと CLI が提供されているため、デプロイの状況をリアルタイムで確認し操作を簡単に行えます。
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 リソースを設定する前に、以下のような一般的な構成から始めます。
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 を導入すると、構成が以下のように変わります。
- 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 を変更するとカナリアリリースが開始されます。 ダッシュボードを開いてリリースの状況確認と操作ができます。
注意点と 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秒程度待つことで解消しました。
- 100% のトラフィックが Canary Service に向かうように VirtualService の
weight
が変更される - Stable Service が Canary Pod に向かうように Service の selector ラベルが変更される
- 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 で集めたメトリクスを利用しエラー率増加の際に自動ロールバックするカナリアリリースを実現することを検討しています!