🌊

プレビュー機能の Cloud Deploy カナリアリリースを試してみる

2023/06/12に公開

こんにちは、クラウドエース SRE ディビジョンの松島です。
本記事では4月にプレビューになった Cloud Deploy のカナリアリリース機能について紹介したいと思います。

Cloud Deployとは

Cloud Deployは、Google Cloud が提供する継続的デリバリーのためのマネージドサービスです。
デプロイ先ターゲット(開発、本番環境など)と、それらにどのような順でデプロイしていくかをパイプラインとして定義しておくことで、任意のバージョンのアプリケーションをパイプラインで定義した順にデプロイしていくことができます。

Cloud Deploy のカナリアリリース機能概要

カナリアリリースとは、アプリケーションリリースの際、トラフィックの一部だけを新しいバージョンにルーティングすることで、新バージョンを本番環境に導入するリスクを低減するために使用されるリリース手法です。

4月に Cloud Deploy でもカナリアリリースを実現する機能がプレビューで公開されました。
Cloud Deploy では次の3つのカナリアリリースの定義方法があります。

自動カナリア

単純なトラフィック分割を利用したカナリアリリースを簡単に実現することができます。
パイプライン側で新バージョンへのルーティング割合目標を指定すると、自動的に各カナリアフェーズの定義が自動生成されます。

例えば、25%、50%、75%の3段階のカナリアフェーズを指定した場合は

  • canary-25: トラフィック全体の25%が新バージョンに割り当てられる
  • canary-50: トラフィック全体の50%が新バージョンに割り当てられる
  • canary-75: トラフィック全体の75%が新バージョンに割り当てられる
  • stable: 完全に新バージョンに置き換わる

のように、3つのカナリアフェーズと最後の stable フェーズが自動的に定義され、手動でフェーズを canary-25 から順に一つずつ進めていくことで段階的にロールアウトを行うことができます。

トラフィックを配分する仕組みはプロダクトによって異なっており、それぞれ下記の通りです。

【Cloud Run の場合】
フェーズに応じた新しいリビジョンへのトラフィック割合設定によりカナリアリリースが実現されます。

【GKE : service を利用する場合】
カナリアフェーズ用の Deployment を自動作成し、Pod 数を変動させることで指定割合のトラフィック分割を実現します。
例えば、replicas の値が 4 で、20%のカナリアを行う場合は、下図のようにカナリアフェーズの Deployment の Pod を1つ作成して同一 Service に紐づけることで

4/5 ⇨ 旧バージョン(本体の Deployment)
1/5 ⇨ 新バージョン(カナリアフェーズ用 Deployment)

のトラフィック配分を実現します。
このパターンの場合は HorizontalPodAutoscaler の利用及び、50%を超える割合のカナリアリリースは実現できない点に注意が必要です。

【GKE : Gateway を利用する場合】
カナリアフェーズ用の Deployment を自動作成し、指定した割合で現行バージョンとカナリアフェーズ用の Deployment にトラフィックをルーティングするよう、HttpRoute が動的に変更されます。

カスタムカナリア

自動カナリアはルーティング割合のみ指定すればカナリアリリースを実現できましたが、カスタムカナリアではカナリアフェーズの状態を全てプロファイルで定義する必要があります。

柔軟にカナリアフェーズの状態をコントロールできるメリットがある一方、途中の定義がやや複雑で、自動カナリアに比べて実装に手間がかかります。
GKE の場合は Gateway が単純な割合でのトラフィック分割方法以外にヘッダ情報によるトラフィック分割に対応しているため、より柔軟なトラフィック分割を定義することが可能です。
本記事でも、後ほどこれを利用したダークカナリアリリースを紹介します。

カスタム自動カナリア

自動カナリアとカスタムカナリアの中間の方式になります。
この方式では、プロファイルは各フェーズで指定しつつも、自動カナリアと同じようにパイプラインで指定した値からトラフィックを分配してくれます。
(カスタムカナリアと違い、プロファイルおよびマニフェスト側で各フェーズのトラフィック配分を定義する必要がない)

Cloud Deployを利用したカナリアリリースの実装例

Cloud Run で自動カナリアリリース

Cloud Runを dev 環境にデプロイ後、prd 環境に25%ずつロールアウトを進めるカナリアリリースを行うパイプラインを構成し、カナリアリリースを行なってみます。
利用する設定ファイルは下記のようになります。

Cloud Deploy パイプライン定義ファイル
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: <パイプライン名>
description: main application pipeline
serialPipeline:
  stages:
    - targetId: cloud-run-canary-dev
      profiles: [dev]
    - targetId: cloud-run-canary-prd
      profiles: [prd]
      strategy:
        canary:
          runtimeConfig:
            cloudRun:
              automaticTrafficControl: true
          canaryDeployment:
            percentages: [25, 50, 75] # 25%ずつ段階的に新しいリビジョンへのトラフィック割合を上げていく
---

apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: cloud-run-canary-dev
run:
  location: <Cloud Run を配置するプロジェクト、リージョン情報>
executionConfigs:
  - usages:
      - RENDER
      - DEPLOY
    serviceAccount: <Cloud Deploy から呼び出す Cloud Build で利用したいサービスアカウント>
    artifactStorage: <skaffold render 結果を配置する GCS バケット>
---
apiVersion: deploy.cloud.google.com/v1
kind: Target
metadata:
  name: cloud-run-canary-prd
run:
  location: <Cloud Run を配置するプロジェクト、リージョン情報>
executionConfigs:
  - usages:
      - RENDER
      - DEPLOY
    serviceAccount: <Cloud Deploy から呼び出す Cloud Build で利用したいサービスアカウント>
    artifactStorage: <skaffold render 結果を配置する GCS バケット>
skaffold プロファイル
apiVersion: skaffold/v3alpha1
kind: Config
metadata:
  name: deploy-cloud-run
profiles:
  - name: dev
    manifests:
      rawYaml:
        - <dev 環境用 Cloud Run マニフェストの相対パス>
  - name: prd
    manifests:
      rawYaml:
        - <prd 環境用 Cloud Run マニフェストの相対パス>
deploy:
  cloudrun: {}
dev 環境用 Cloud Run マニフェスト
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: <dev 環境用 Cloud Run サービス名>
spec:
  template:
    spec:
      containers:
        - image: <イメージ名>
      serviceAccountName: <Cloud Run に設定するサービスアカウント>
本番環境用 Cloud Run マニフェスト
apiVersion: serving.knative.dev/v1
kind: Service
metadata:
  name: <prd 環境用 Cloud Run サービス名>
spec:
  template:
    spec:
      containers:
        - image: <イメージ名>
      serviceAccountName: <Cloud Run に設定するサービスアカウント>

下図は dev 環境へのデプロイが終わったところです。

この状態からプロモートを選択すると、横に画面が開きロールアウト内のフェーズを指定することができます。
小さい機能改変などでカナリアフェーズが不要な場合は、一気に stable まで上げる選択も取ることができます。
今回は canary-25 を選択します。

canary-25 へのロールアウトが始まりました。

しばらくすると canary-25 へのロールアウトが完了します。
この時点で Cloud Run は、パイプラインで指定したとおり新しいリビジョンにトラフィックの25%が割り振られる設定になっています。

無事 canary-25 へのロールアウトが完了したので、カナリアフェーズを進めてみます。
Cloud Deploy の画面で「canary-50に進む」をクリックすることにより次フェーズに進むことができます。


この状態で Cloud Run を確認すると、新リビジョンに50%のトラフィックが割り当てられるようになっていることが確認できます。

以上を stable フェーズ、つまり新リビジョンに全てのトラフィックを割り振る状態になるまで繰り返すことで段階的なロールアウトを行うことができます。

なお、Cloud Deploy ではロールアウトの中で、検証フェーズを設けることにより、リリースしたアプリケーションに対して自動テストを実行することができますが、現時点では検証に失敗した際の自動ロールバック機能は提供されていません。
この場合は手動でロールバックする必要がある点にご注意ください。

カスタムカナリアを利用して GKE でダークカナリアリリース

標準的なカナリアリリースでは単純に新バージョンのアプリケーションに指定割合のトラフィックを流すのに対し、ダークカナリアリリースは特定の条件のリクエストのみを新バージョンのアプリケーションに流します。
本番環境にデプロイした後一般ユーザには現行バージョンを使用させておき、一通り検証が完了してから新バージョンを一般解放することが可能なのがこの手法の良いところです。
カスタムカナリアで任意のカナリアフェーズを定義できることを利用すると、ダークカナリアリリースを実現することができます。

今回は特定のヘッダが付与されたリクエストだけを新バージョンに流すダークカナリアを実装するため、下図の状態遷移に必要なカナリア、stable フェーズををカスタムカナリアで定義します。

これを実現する Cloud Deploy パイプラインおよび、skaffold の設定は次のようなイメージになります。

Cloud Deploy パイプライン定義ファイル
apiVersion: deploy.cloud.google.com/v1
kind: DeliveryPipeline
metadata:
  name: k8s-canary
description: main application pipeline
serialPipeline:
  stages:
    - targetId: <ターゲット名>
      profiles: [prd]
      strategy:
        canary:
          customCanaryDeployment:
            phaseConfigs:
              - phaseId: "dark-canary" # 専用のプロファイルを指してダークカナリアフェーズを定義
                percentage: 50
                profiles: [prd-dark-canary] 
                verify: false
              - phaseId: "stable" # stable フェーズを定義
                percentage: 100
                profiles: [prd]
                verify: false
...
skaffold.yaml
apiVersion: skaffold/v4beta1
kind: Config
profiles:
  - name: prd-dark-canary
    deploy:
      ... # ここでダークカナリアフェーズの状態を定義したマニフェストを指定
  - name: prd
    deploy:
      ... # stable フェーズの状態を定義したプロファイルを指定

skaffold.yaml で指定するダークカナリアフェーズ、stable フェーズの k8s マニフェストの実装イメージは次のとおりです。

ダークカナリアフェーズ
このフェーズでは、カナリアフェーズ用の Deployment を立ち上げて、HttpRoute に特定ヘッダが付与された場合のみカナリアフェーズ用 Deployment にトラフィックが流れるような設定を入れます。
なお、現行 stable の状態からの差分だけを定義すれば良いため、Deployment と Service についてはマニフェストは不要です。
以下、このフェーズで必要なマニフェストを列挙します。

カナリアフェーズ用 Deployment ⇨ 本体の Deployment とは別に作成
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary-<本体のDeployment名> # 本体の Deployment とは別で作成
spec:
...
  replicas: 1 # 1つ以上を指定
  template:
    spec:
      containers:
        - image: <新バージョンのイメージ>
          ...
カナリアフェーズ用 Service ⇨ カナリアフェーズ用 Deployment を指すように、本体の Service とは別で作成
apiVersion: v1
kind: Service
metadata:
  name: <本体の Service 名>
spec:
  ports:
    - port: <ポート番号>
      protocol: TCP
      targetPort: <カナリアフェーズ用 Deployment のポート番号>
  selector:
    <カナリアフェーズ用 Deployment の Pod のラベルを指定>
...
HttpRoute ⇨ 特定ヘッダが付与された場合のみ、カナリアフェーズ用 Service にルーティング
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: <Httproute 名>
spec:
  parentRefs:
  - kind: Gateway
    name: <Gateway 名>
  rules:
  - backendRefs:
    - kind: Service
      name: <Service 名>
      port: <Service ポート>
  - matches: # ヘッダ dark-canary-test の値が enabled の場合のみカナリアフェーズ用 Service にルーティング
    - headers:
      - name: dark-canary-test
        value: enabled
    backendRefs:
      - kind: Service
      name: <カナリアフェーズ用 Service 名>
      port: <カナリアフェーズ用 Service ポート>

stableフェーズ
このフェーズではアプリケーション本体のマニフェストを新しいものに更新しつつ、カナリアフェーズ用のリソースへのルーティングを削除します。

Deployment ⇨ 新バージョンのイメージを指定
apiVersion: apps/v1
kind: Deployment
metadata:
  name: <Deployment 名>
spec:
...
  template:
    spec:
      containers:
        - image: <新バージョンのイメージ>
          ...
Service
apiVersion: v1
kind: Service
metadata:
  name: <Service 名>
spec:
  ports:
    - port: <ポート番号>
      protocol: TCP
      targetPort: <Deployment のポート番号>
  selector:
    <Deployment の Pod のラベルを指定>
...
カナリアフェーズ用 Deployment ⇨ Pod 数を0に指定
apiVersion: apps/v1
kind: Deployment
metadata:
  name: canary-<本体のDeployment名> # 本体の Deployment とは別で作成
spec:
...
  replicas: 0 # stable フェーズでは Pod が稼働している必要がないため、0を指定
カナリアフェーズ用 Service ⇨ カナリアフェーズから変更なし
apiVersion: v1
kind: Service
metadata:
  name: <本体の Service 名>
spec:
  ports:
    - port: <ポート番号>
      protocol: TCP
      targetPort: <カナリアフェーズ用 Deployment のポート番号>
  selector:
    <カナリアフェーズ用 Deployment の Pod のラベルを指定>
...
HttpRoute ⇨ カナリアフェーズ用 Service へのルーティングを削除する
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: <Httproute 名>
spec:
  parentRefs:
  - kind: Gateway
    name: <Gateway 名>
  rules:
  - backendRefs:
    - kind: Service
      name: <本体の Service 名>
      port: <本体の Service ポート>



それでは、稼働中のイメージ名を返すアプリケーションを使って実際にダークカナリアリリースを行なってみます。

dev 環境から dark-canary フェーズをを選択してプロモートします。


完了後は、特定ヘッダを付与した場合のみ新しいバージョンにつながるはずです。
これを検証するため、まずは普通に Gateway のエンドポイントにアクセスしてみます。

現行のコンテナ名が返ってきました。

今度はヘッダを指定して Gateway のエンドポイントにアクセスしてみます。

カナリアフェーズの HttpRoute で設定したとおり、新バージョンのコンテナ名が返ってきました。

動作を確認できたので 「stable に進む」から stable フェーズに進めます。

完了後、ヘッダを指定せずに Gateway のエンドポイントにアクセスしてみます。

予定通り新バージョンのコンテナ名を取得でき、意図した通りにリリースできていることを確認できました。

現時点の Cloud Deploy でできないこと

Blue Green Deployment および、Verify 機能やメトリクスと連動して自動的にフェーズを進めたりロールバックするようなより高度なカナリアリリース機能は、現時点の Cloud Deploy に備わっていません。
これらを実現する場合は他のサードパーティ製品を利用する必要があります。

今後の機能増強に期待したいと思います。

おわりに

現在まだプレビューですが、本機能リリースにより、Google Cloud の機能でカナリアリリースを実現できるようになりました。
興味を持たれた方は、ぜひ本記事を参考に手元で動かしてお試しいただければと思います。

参考公式ドキュメント

Discussion