Argo RolloutsとPairsのデリバリー戦略 -Progressive Deliveryへの移行-

takumi ogawa
Eureka Engineering
Published in
20 min readDec 21, 2022

はじめに

この記事は Eureka Advent Calendar 2022 の21日目の記事です。

こんにちは、最近Splatoonを始めました。Eureka SREチームの@ogadyです。Splatoonをずっとやりたいやりたいと思って数年経っていたのですが、ちょうどSplatoon 3が発売されたタイミングでついに夢が叶いました。近距離も長距離もできるクーゲルシュライバーが理論上最強だと言いながら使い続け、リッターに抜かれてスパッタリーに狩られる日々を過ごしています。

こちらは昨日食べたおいしい鰻(麻布十番 はなぶさ

ところで!! 最近Eureka SREチームではKubernetes(EKS on EC2)で運用しているアプリケーションのデリバリー改善を行っています。初期構築時点からアプリケーションのデリバリーにArgo CD + Argo Rolloutsを利用したBlue/Green Deployを採用していました。そして最近、デリバリー方式をCanary ReleaseベースのProgressive Deliveryに変更しようとしています。

この記事では、 Progressive Deliveryへ変更したいモチベーションと、それに利用したArgo Rolloutsによる実装例などをご紹介しようと思います。

Table of Contents

対象読者
前提
Kubernetesにおけるアプリケーションのデリバリー方式
−− Rolling Update
−− Blue/Green Deploy
−− Canary Release
Progressive Deliveryとは
これまでのPairsデリバリー戦略と課題
Pairsの新しいデリバリー戦略
−− Argo Rollouts Canary StrategyとIstio Ingress Gateway
−− Argo RolloutsによるProgressive Delivery
−− Production環境へのProgressive Delivery導入
まとめ

対象読者

  • EKSでインフラを構築している方
  • Argo Rolloutsを既に導入している方
  • Argo Rolloutsを利用したProgressive Deliveryに興味がある方

前提

  • EKS version 1.21
  • Argo Rollouts version 1.3.1
  • Istio version 1.15.0

Kubernetesにおけるアプリケーションのデリバリー方式

本題に行く前にまずはよく話題に上がるKubernetesでのデリバリー方式について軽く整理してみようと思います。

Rolling Update

  • 順番に新versionのPodを起動し、その分旧versionのPodを破棄していき、最終的に全てが新しいversionになるというデリバリー方式
  • Kubernetesで最初からサポートされているため、簡単に導入できる

Blue/Green Deploy

  • 新versionを旧versionと同数のPod数を起動してから、トラフィックの向き先を一気に切り替えるデリバリー方式
  • トラフィック切り替え前にテストなどを行ってリリース前に動作確認することができる
  • 自動で行うためには、ArgoCDなどのCDツールと連携したり、独自実装する必要がある

Canary Release

  • トラフィックを新version:旧versionを25:75 → 50:50 → 75:25 → 100:0 のように段階的に切り替えるデリバリー方式
  • 各ステップごとにモニタリングを行いながら段階的にリリースするため、問題が発生した時に切り戻すことで、障害影響範囲を軽減することができる
  • 自動で行うためには、ArgoCDなどのCDツールと連携したり、独自実装する必要がある

Progressive Deliveryとは

デリバリー方式についておさらいした上で、Progressive Deliveryについても簡単に。

Progressive DeliveryはContinuous Delivery(CD)を発展させたデプロイフローです。具体的な手法を指しているわけではなく、新しいバージョンを徐々にリリースしつつ、リリースに伴う影響を最小限に抑えることを目的としたデリバリー方式全般を指します。

主にCanary Releaseと組み合わせて、段階リリースしながらメトリクス分析を行い、一定の閾値ベースで自動ロールバックなどを行う手法が有名かと思います。

これまでのPairsデリバリー戦略と課題

これまでのPairsではBlue/Green Deployを採用していました。

新versionのPodを旧versionと同数立ち上げ、readiness/liveness probeが正常になったことを確認しトラフィックを切り替えており、ArgoCDのBlue/Green Strategyを利用して実装していました。

Blue/Green Deployは大きなダウンタイムも発生せずに、かつバックエンドAPIのversionが一気に切り替わるためクライアントサイドでAPIのバージョン混在を気にする必要がないため、これまでのPairsにおいてデリバリー方式が大きな問題になることはありませんでした。

しかし、アプリケーション開発現場では、ライブラリ変更やバージョンアップ、リファクタリング、新機能のビッグバンリリースなどの、影響範囲が把握しづらいリリースがたびたび発生します。こうした時に、CIによるテストで品質を担保したり、QAを行うことでリリースによる障害発生は軽減することができます。一方で、すべてのケースを網羅することは困難ですし、潰し切るためには開発コストとのトレードオフになります。

この時Pairsで導入していたBlue/Greenのデリバリーでは手動ロールバックを採用しており、仮にリリースに問題があった場合は、

  1. Blue/Greenによりすべてのトラフィックが新versionへ流れる
  2. エラーが発生し、http response code 5XXが返却されSLIが低下する
  3. アラート発報され、開発者やSREが手動ロールバックを行う
  4. ロールバックが完了し、復旧が完了する

となります。この時すべてのユーザーが1〜4までの間、影響を受けてしまいます。

そのため、SREチームの戦略として、大きな問題が起きる前に、テストやQAの充実などのリリース前の品質担保とは別に、

  • リリース後に問題があった時にできるだけ素早く切り戻す(= MTTRをできるだけ短縮する)
  • リリースの影響範囲をできるだけ小さくする

という2軸でリリース時の信頼性の向上を図ることとしました。

そのため、さらなる信頼性向上のために、自動ロールバックとCanary ReleaseによるProgressive Deliveryを導入する意思決定を行いました。

Pairsの新しいデリバリー戦略

自動ロールバックとCanary ReleaseによるProgressive Deliveryの実装には、元々Blue/Green Deployで使用していたArgo RolloutsのCanary Strategyの機能を使用することとしました。

ここからは具体的な事例紹介として、各技術要素やPairsのアーキテクチャについてお話ししようと思います。

Argo Rollouts Canary StrategyとIstio Ingress Gateway

Argo Rollouts の Canary Strategy の機能では、トラフィックルーティングの設定で AWS Load Balancer Controller の ingress や IstioNginx Ingress Controller などとインテグレーションが可能です。

Eureka SREチームでは以下理由でトラフィックルーティングにはIstioを選定しました。

  • 開発者用のPullRequest環境(アプリケーションのPull Requestごとに動的に作成される検証環境)を実現するために既にIstioが導入されている
  • service meshの技術として今後採用予定なので早めに導入しておきたい
  • AWSリソース参照のしやすさから、ALBはterraformで管理したいため、AWS Load Balancer Controllerでingressを作成せずTargetGroupBindingのみ使用したい

今回はALB経由で外部からのリクエストを受けるサービスにCanary Releaseを導入したいため、Istio Ingress Gatewayを利用してALBからリクエストを受け付ける場所でトラフィックコントロールを行うようにしました。

結果としてIstioを利用したアーキテクチャは図1のようになりました。

Istio Ingress Gateway をNodePortで公開し、TargetGroupBindingで全てのアプリケーション用ALBのtargetGroupに紐づけています。

各サービスのPodへはIstioのカスタムリソース(Gateway、VirtualService)を使用してHostBaseのルーティングを行なっており、Argo RolloutsはVirtualServiceのdestinationに定義されているCanary serviceとStable serviceのweightを動的に更新することによってCanary Releaseを実現しています。

図1 istioを利用したトラフィックルーティング

Gatewayのmanifest yaml(sample)

apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: sample-gateway
spec:
selector:
istio: ingressgateway # use istio default controller
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- sample.com
- sample-a.com

VirtualServiceのmanifest yaml(sample)

apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: sample-virtualservice
spec:
hosts:
- sample-service-stable.sample-app.svc.cluster.local
- sample-service-canary.sample-app.svc.cluster.local
- sample.com
gateways:
- istio-system/sample-gateway
- mesh
http:
- name: sample-routes
retries:
attempts: 10
route:
- destination:
host: sample-service-stable.sample-app.svc.cluster.local
port:
number: 80
weight: 100
- destination:
host: sample-service-canary.sample-app.svc.cluster.local
port:
number: 80
weight: 0

一方でIstio Ingress Gateway導入時に問題が発生しました。

Istio Ingress Gatewayの実体はenvoy proxyですが、こちらも負荷に合わせてオートスケールの設定を入れていました。このスケールイン時にALBで504エラーが大量に発生するようになりました。

調べた結果、Istio Ingress Gateway(envoy) のPod終了処理は、SIGTERMを受けたのち、下記の挙動になると理解しました。

  1. terminationDrainDuration (default 5s)で指定した期間、graceful drainモードに移行し、drainステータスで待機する。
  2. drainDuration (default 45s)で指定した期間、drainステータス中に新規リクエストを受け付ける。
  3. terminationGracePeriodSeconds (default 30s) で指定した時間が経過したら、SIGKILLによりPodが強制終了される。

※参考記事

https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#ProxyConfig

https://blog.1q77.com/2022/02/istio-exit-on-zero-active-connections/

https://sreake.com/blog/istio-proxy-stop-behavior/

なので、この辺のステータスを余裕もった値に設定しないと、ALB TargetGroup から切り離される前にIstio Ingress GatewayのPodが終了してしまい、ALBでエラーが発生してしまいます。

さまざまな値を試した結果、特に `terminationDrainDuration` と `terminationGracePeriodSeconds` を1分以上に設定したあたりで安定してきました。この辺りは今後も様子を見ながら調整をしていこうと考えています。また、当初HelmでIstio Ingress Gateway v1.15.0をインストールしていましたが、このHelmでは `terminationGracePeriodSeconds` を指定できなかったため、Istio Ingress Gatewayはmanifestsで自前管理しています。

ちなみに、EXIT_ON_ZERO_ACTIVE_CONNECTIONS という機能を有効にすると、アクティブなコネクションが0件になるまで終了処理を待ったりすることができるらしいのですが、こちらはまだ試せていないです。(使ってみたい。。。)

Argo RolloutsによるProgressive Delivery

ArgoCDでProgressive Deliveryを導入するのは簡単です。

Datadog、Prometheus、NewRelicなどの主要なObservabilityツールとインテグレーションし、そのメトリクスを使うことで自動analysisの設定を行うことが可能です。さらに、dry runや複数メトリクスによるanalysis、自動ロールバックなども機能として備えています。

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: request-error-rate
spec:
dryRun:
- metricName: error-rate
metrics:
- name: error-rate
interval: 1m
successCondition: default(result, 0) < 0.01
failureLimit: 5
provider:
datadog:
interval: 1m
query: |
sum:request.count{response_code:5*}.as_count()/
sum:request.count{response_code:*}.as_count()

このようにAnalysisTemplateを使用して、どのようなanalysisを行うかを定義できます。これは、Rolloutリソースにインラインで書くこともできますし、Cluster共通で利用するClusterAnalysisTemplateというリソースも定義できます。

実際に導入する上で検討したのは、analysisのフェーズでどのようなメトリクスを使うかというところです。Eurekaでは、まずスタートとしてSLI/SLOと同様にsuccess rate、error rateをベースに「ユーザーが問題なく使えているか」を軸に分析していく方針で進めました。rateの指標とすることで、リクエスト数に依存しないanalysisを行うことができます。

その上でanalysisで使うメトリクスの要件としては、

  • Canaryのversionのみの、できるだけリアルタイムなメトリクス

であると考えました。

なぜなら、versionごとのリクエスト数がデプロイ中に動的に変わっていくため、新varsionがSLOを満たす信頼性が担保できているかがrateで見た時に追いづらくなってしまうためです。

例えば、SLO 99.95%のサービスをCanaryで25%リリースした時に、全体error rate が 0.04% あったとします。

この時、全体のerror rateをベースにanalysisを組んでいると、SLO 99.95%を満たしているため、そのまま次のステップへ進みます。しかし、0.04%のエラーが全て新versionで発生していた場合、この時点で新versionのerror rateは0.16%となり、そのままリリースを続けるとSLOを割るアプリケーションがリリースされてしまいます。

これを加味して、各パーセントリリースのステップごとにanalysisの閾値を調整することも可能ですが、Canaryのversionのみのメトリクスを見る方がシンプルになります。

Eurekaでは以前からDatadogを採用していたため、analysisのメトリクスとしては、IstioのVirtualServiceのメトリクスを使うことにしました。DatadogはIstioのインテグレーションがあり、destinationごとのrequest数をresponse_codeも含めて取得することができます。このメトリクスはほぼリアルタイムでDatadogへ送信されているため、今回の要件にマッチしました。

結果、このような定義することでほぼリアルタイムなCanaryのerror rateメトリクスを使ったnalysisが実現できました。

AnalysisTemplateのmanifest yaml(sample)

apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: request-error-rate
spec:
dryRun:
- metricName: error-rate
metrics:
- name: error-rate
interval: 1m
# SLOに合わせた閾値
successCondition: default(result, 0) < 0.005
failureLimit: 5
provider:
datadog:
interval: 1m
query: |
sum:istio.mesh.request.count.total{cluster_name:sample,destination_service:sample-service-stable.sample-app.svc.cluster.local,response_code:5*}.as_count()/
sum:istio.mesh.request.count.total{cluster_name:sample,destination_service:sample-service-canary.sample-app.svc.cluster.local}.as_count()

Rolloutのmanifest yaml(抜粋sample)

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: sample-app
spec:
strategy:
canary:
# rollback高速化のために、stableのreplica数を維持したままcanaryリリースする
dynamicStableScale: false
stableMetadata:
labels:
revision: stable
canaryMetadata:
labels:
revision: canary
canaryService: sample-service-canary
stableService: sample-service-stable
trafficRouting:
istio:
virtualService:
name: sample-virtualservice
routes:
- sample-routes
analysis:
templates:
- templateName: request-error-rate
startingStep: 1
steps:
- setWeight: 25
- pause:
duration: 10m

################## 後略 ##################

また、`dynamicStableScale` をfalseに設定することで、stableのreplica数を維持したままcanaryリリースすることができます。これにより、素早いrollbackが可能になるので、今回の目的の一つであるMTTRの短縮にもつながると考え、設定しました。(defaultがfalseなので設定しなくても動作は同じですが、defaultの挙動を忘れがち + 意識したい設定なので、明示的に指定しています。)

stableのreplica数を維持するためリソースコストは高くなりますが、MTTR短縮が目的であるため、今回は許容しています。

Production環境へのProgressive Delivery導入

記事執筆時点では、本格的な運用には至っておらず、analysisをDry Run状態にしてCanary ReleaseのみProduction環境に導入している状況です。

Dry Run状態で特に運用上問題が発生しないことが確認でき次第、analysisを有効にして本格的なProgressive Deliveryへ移行する予定です。(アドカレまでにやり切りたかった、、、)

まとめ

Pairsは新規開発や機能改修、リファクタリングなど日々デプロイが行われているため、リリースのリスクを軽減させることはサービスの信頼性に直結するため、今回ご紹介したような取り組み行なっています。

Eureka SREチームでは、このほかにもさまざまな取り組みをおこなっています。今回のアドベントカレンダーでも各メンバーが記事を執筆しているので是非そちらも読んでみてください!

明日の Eureka Advent Calendar 2022 は、@rennnosuke さんの 「Pairs Back-endのエラー対応運用を改善する話」です。お楽しみに!

--

--