📑

Argo Rolloutsで柔軟なデプロイを実現する

に公開

Argo Rolloutsを使って柔軟なデプロイを行う方法をまとめます。Argo Rolloutsを利用することで、ブルー/グリーンデプロイやカナリアリリース、ロールバックが簡単に実現できます。

本文中のコード: https://github.com/hosimesi/code-for-techblogs/tree/main/argo_rollouts_canary

記事の趣旨

Kubernetes上でのアプリケーションデプロイ手法の一つであるカナリアリリースを、Argo Rolloutsを用いて実現する方法をご紹介します。カナリアリリースを活用することで、新機能のリリースや更新時のリスクを最小限に抑えつつ、ユーザーへの影響を軽減できます。
以前にECSでカナリアリリースを実現する記事を書きましたが、今回はGKEとArgo Rolloutsを用いてカナリアリリースを行います。切り戻し時にメトリクスを自動で設定できるなど、異なる点を確認するために本記事を書きました。
今回、Argo Rolloutsを使って以下の3点のカナリアリリースを構築してみます。

  1. サービスレベルでのPod数ベースのベーシックなカナリアリリース
  2. トラフィックのパーセンテージに基づくカナリアリリース
  3. Analysisに基づく自動ロールバック機能を持つカナリアリリース

今回は、これらをGoogle Cloudを用いて実現します。

事前知識

カナリアリリースとは

カナリアリリースとは、新しいバージョンのアプリケーションを一部のユーザーに対して先行してリリースする手法です。限定的な範囲で新バージョンを提供し、動作やユーザーからのフィードバックを確認します。問題がなければリリース範囲を段階的に拡大し、最終的に全ユーザーに新バージョンをリリースします。
この手法により、万が一問題が発生しても影響を受けるユーザーを最小限に抑え、迅速な切り戻し(ロールバック)が可能です。また、カナリアバージョンへのトラフィック割合を調整することで、A/Bテストとして新機能の効果検証にも活用できます。

Argo Rolloutsとは

Argo Rolloutsは、Kubernetes上で多様なデプロイ戦略を実現するためのオープンソースのコントローラーです。コントローラーとCRD(カスタムリソース定義)のセットで構成されており、ブルー/グリーンデプロイやカナリアデプロイなど、さまざまなデプロイ戦略を実現できます。また、「Argo」を冠したプロダクト群として、ワークフローオーケストレーションツールのArgo Workflows、CDツールのArgo CD、イベントドリブンの自動化ツールのArgo Eventsなどがあります。

Argo Rolloutsの機能

デプロイ戦略

公式ドキュメントによると、Argo Rolloutsが提供する主要な機能は以下の通りです。

  • ブルー/グリーンデプロイメント
  • カナリアデプロイメント
  • 高度なトラフィックルーティングのためのIngressコントローラーやサービスメッシュとの統合
  • ブルー/グリーン分析およびカナリア分析のためのメトリックプロバイダーとの統合
  • 成功または失敗の指標に基づいた自動昇格またはロールバック
    Rolloutは、KubernetesのDeploymentに相当するCRDです。Deploymentでは実現が難しい高度なデプロイ戦略を可能にするため、Deploymentを置き換えて使用します。
    デフォルトではServiceを通じてリクエストの振り分けを行いますが、Ingressやサービスメッシュと統合することで、Ingressレベルでのリクエスト振り分けにも対応しています。

リクエストの振り分け

リクエストの振り分け方法は以下の通りです。

  • パーセンテージに基づく振り分け
  • L7レベルのヘッダー値による振り分け
  • ミラーリングによるトラフィックのコピー

監視

デフォルトでコントローラーのメトリクスをPrometheus形式でエクスポートしているため、Grafanaと連携してメトリクスを監視することも可能です。

Google Cloudのインフラ準備

今回は必要なリソースをすべてTerraformで作成します。

サービスアカウントの準備

まず、GKEのノードプールに付与するサービスアカウントを作成します。

main.tf
# GKE用のservice account
resource "google_service_account" "gke_sa" {
  account_id   = "gke-sa"
  display_name = "gke用のservice account"
}

クラスターの準備

GKEのクラスターを準備し、Argo Rollouts用のノードプールを作成します。

main.tf
provider "google" {
  project = var.project
  region  = var.region
}

# GKE Cluster
resource "google_container_cluster" "gke_cluster" {
  name                = "gke-cluster"
  location            = "asia-northeast1-a"
  project             = var.project
  deletion_protection = false

  remove_default_node_pool = true
  initial_node_count       = 1

}

# GKE Node Pool
resource "google_container_node_pool" "argo_rollouts_node_pool" {
  name     = "argo-rollouts-node-pool"
  location = "asia-northeast1-a"

  cluster    = google_container_cluster.gke_cluster.name
  project    = var.project
  node_count = 1

  autoscaling {
    max_node_count = 1
    min_node_count = 1
  }

  node_config {
    preemptible  = true
    machine_type = "e2-medium"

    service_account = google_service_account.gke_sa.email

    metadata = {
      disable-legacy-endpoints = "true"
    }

    oauth_scopes = [
      "https://www.googleapis.com/auth/logging.write",
      "https://www.googleapis.com/auth/monitoring",
    ]

    labels = {
      preemptible = "true"
    }

    tags = ["argo-rollouts-node-pool"]
  }
}

適用

$ cd path/to/terraform
$ terraform init
$ terraform plan
$ terraform apply

k8sマニフェストの作成

クラスターへの接続

先ほど作成したクラスターに接続するために、以下のコマンドを実行します。

$ gcloud container clusters get-credentials gke-cluster --zone asia-northeast1-a --project <your-project-id>

作成したクラスターに接続できていることを確認します。

$ kubectl config get-contexts

Argo Rolloutsのインストール

Argo Rolloutsをインストールします。グローバルにインストールする方法と、ネームスペースを限定してインストールする方法がありますが、今回は標準的なグローバルインストールを行います。
まず、argo-rolloutsというネームスペースを作成します。

$ kubectl create namespace argo-rollouts

次に、Argo Rolloutsをインストールします。

$ kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml

GKEで動作させる場合、サービスアカウントに権限を付与する必要があります。

$ kubectl create clusterrolebinding <your-name>-cluster-admin-binding --clusterrole=cluster-admin --user=<your-email>@gmail.com

また、kubectlコマンドでArgo Rolloutsを操作するために、プラグインをインストールします。

$ brew install argoproj/tap/kubectl-argo-rollouts

アプリケーションの作成

チュートリアル

まずは簡単に動作確認するために、チュートリアルを参考に進めていきます。公式が提供しているイメージを使って、Serviceレベルでのリクエストの振り分けを行います。この方法は簡単ですが、Pod数に基づくルーティングになるため、大まかな振り分けになります。

rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-demo
spec:
  replicas: 5
  strategy:
    canary:
      steps:
        - setWeight: 20
        - pause: {}
        - setWeight: 40
        - pause: { duration: 10 }
        - setWeight: 60
        - pause: { duration: 10 }
        - setWeight: 80
        - pause: { duration: 10 }
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: rollouts-demo
  template:
    metadata:
      labels:
        app: rollouts-demo
    spec:
      containers:
        - name: rollouts-demo
          image: argoproj/rollouts-demo:blue
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          resources:
            requests:
              memory: 32Mi
              cpu: 5m
service.yaml
apiVersion: v1
kind: Service
metadata:
  name: rollouts-demo
spec:
  ports:
  - port: 80
    targetPort: http
    protocol: TCP
    name: http
  selector:
    app: rollouts-demo

上記のマニフェストを適用します。

$ kubectl apply -f ./k8s/tutorial/rollout.yaml
$ kubectl apply -f ./k8s/tutorial/service.yaml

チュートリアル初回デプロイ
次に、新しいイメージに更新します。

$ kubectl argo rollouts set image rollouts-demo rollouts-demo=argoproj/rollouts-demo:yellow

チュートリアルロールアウト
すると、Canary用のPodが作成されてヘルシーになっていることが確認できます。StatusがPausedになっているのは、Rolloutの戦略で手動実行になっているためです。
ダッシュボードでも確認してみます。(※ダッシュボードの構築方法は後述します。)
チュートリアルダッシュボード
次のフェーズに進むには、以下のコマンドを実行します。

$ kubectl argo rollouts promote rollouts-demo

すると、10秒ごとにトラフィックの割合が変わっていきます。Podが一つずつ入れ替わっていれば成功です。
チュートリアルロールアウト更新
途中で中止したい場合は、以下のコマンドで停止し、戻したいイメージを指定して切り戻すことが可能です。

$ kubectl argo rollouts abort rollouts-demo
$ kubectl argo rollouts set image rollouts-demo rollouts-demo=argoproj/rollouts-demo:yellow

ALBでの振り分け

先ほどはServiceレベルでPod数に基づくリクエストの振り分けを行いましたが、より精密なトラフィック管理を実現するために、Ingressと統合して振り分けを行います。ここでは、Google CloudのマネージドサービスであるGoogle Cloud Load Balancingを活用します。こちらのコードを参考に作成していきます。

Gateway APIとの統合

AWSのALBではデフォルトで統合されていますが、Google Cloudで統合するにはGateway APIを使用する必要があります。まず、GKEクラスターがGateway APIを使用できるように設定を変更します。先ほどのTerraformファイルを少し修正します。

main.tf
resource "google_container_cluster" "gke_cluster" {
  name                = "gke-cluster"
  location            = "asia-northeast1-a"
  project             = var.project
  deletion_protection = false

  # Gateway APIの設定
  gateway_api_config {
    channel = "STANDARD"
  }

  remove_default_node_pool = true
  initial_node_count       = 1
}

gloudコマンドを使っても同様に更新できます。

gcloud container clusters update CLUSTER_NAME \
--gateway-api=standard \
--region=COMPUTE_REGION

次に、プロキシを配置するためのサブネットを作成します。こちらもTerraformファイルに追記します。

main.tf
resource "google_compute_subnetwork" "argo_rollouts_subnet" {
  name          = "argo-rollouts-subnet"
  ip_cidr_range = "10.1.1.0/24"
  region        = var.region
  network       = "default"

  purpose = "REGIONAL_MANAGED_PROXY"
  role    = "ACTIVE"
}

こちらもgcloudコマンドを使うことで作成可能です。

gcloud compute networks subnets create demo-subnet \
	--purpose=REGIONAL_MANAGED_PROXY \
	--role=ACTIVE \
	--region=asia-northeast1 \
	--network=default \
	--range=10.1.1.0/24

そして、ゲートウェイを作成します。

gateway.yaml
kind: Gateway
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: internal-http
spec:
  gatewayClassName: gke-l7-rilb
  listeners:
    - name: http
      protocol: HTTP
      port: 80

ゲートウェイを適用します。

$ kubectl apply -f ./k8s/alb/gateway.yaml

この時、動作確認のためゲートウェイのIPアドレスを取得しておきます。

$ kubectl get gateways.gateway.networking.k8s.io internal-http -o=jsonpath="{.status.addresses[0].value}"

Roleの作成

ゲートウェイを操作するための必要な権限を持つクラスターロールを作成します。

clusterrole.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: gateway-controller-role
rules:
  - apiGroups:
      - "*"
    resources:
      - "*"
    verbs:
      - "*"
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: gateway-admin
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: gateway-controller-role
subjects:
  - namespace: argo-rollouts
    kind: ServiceAccount
    name: argo-rollouts

こちらも適用します。

$ kubectl apply -f ./k8s/alb/clusterrole.yaml

HTTPRoute設定の準備

GKEでHTTP/HTTPSロードバランサを使用するために、Ingressの代わりにGatewayAPIを使用し、HTTPRouteを作成します。

httproute.yaml
kind: HTTPRoute
apiVersion: gateway.networking.k8s.io/v1beta1
metadata:
  name: argo-rollouts-alb-http-route
spec:
  parentRefs:
    - kind: Gateway
      name: internal-http
  rules:
    - backendRefs:
        - name: argo-rollouts-alb-stable-service
          port: 80
        - name: argo-rollouts-alb-canary-service
          port: 80
$ kubectl apply -f ./k8s/alb/httproute.yaml

Serviceの作成

Stable用のサービスとCanary用のサービスを作成します。

service.yaml
apiVersion: v1
kind: Service
metadata:
  name: argo-rollouts-alb-canary-service
spec:
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: rollouts-alb-demo
---
apiVersion: v1
kind: Service
metadata:
  name: argo-rollouts-alb-stable-service
spec:
  ports:
    - port: 80
      targetPort: http
      protocol: TCP
      name: http
  selector:
    app: rollouts-alb-demo
$ kubectl apply -f ./k8s/alb/service.yaml

Rolloutの設定を更新

Rolloutリソースを修正して、Gateway APIとの統合を有効にします。strategyセクションでcanary戦略を設定し、trafficRoutingディレクティブでGateway APIの設定を追加します。

rollout.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-alb-demo
  namespace: default
spec:
  replicas: 10
  strategy:
    canary:
      canaryService: argo-rollouts-alb-canary-service
      stableService: argo-rollouts-alb-stable-service
      trafficRouting:
        plugins:
          argoproj-labs/gatewayAPI:
            httpRoute: argo-rollouts-http-route
            namespace: default
      steps:
        - setWeight: 30
        - pause: {}
        - setWeight: 60
        - pause: {}
        - setWeight: 100
        - pause: {}
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: rollouts-alb-demo
  template:
    metadata:
      labels:
        app: rollouts-alb-demo
    spec:
      containers:
        - name: rollouts-alb-demo
          image: kostiscodefresh/summer-of-k8s-app:v1
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          resources:
            requests:
              memory: 32Mi
              cpu: 5m
$ kubectl apply -f ./k8s/alb/rollout.yaml

ConfigMapの作成

Gateway APIがプラグインになるため、コントローラーにインストールしていきます。今回はConfigMap経由で記載して適用し、コントローラーを再起動します。

configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argo-rollouts-config
  namespace: argo-rollouts
data:
  trafficRouterPlugins: |-
    - name: "argoproj-labs/gatewayAPI"
      location: "https://github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/releases/download/v0.4.1/gatewayapi-plugin-linux-amd64"
$ k apply -f ./k8s/alb/rollout.yaml 
 kubectl rollout restart deployment/argo-rollouts -n argo-rollouts

マニフェストの適用

更新したマニフェストを適用します。先ほど実行したコマンドは以下のとおりです。

$ kubectl apply -f ./k8s/alb/gateway.yaml
$ kubectl apply -f ./k8s/alb/clusterrole.yaml
$ kubectl apply -f ./k8s/alb/service.yaml
$ kubectl apply -f ./k8s/alb/httproute.yaml
$ kubectl apply -f ./k8s/alb/rollout.yaml

上記の手順により、Gateway APIとArgo Rolloutsを統合して、より精密なトラフィック管理が可能になります。これにより、ロードバランサレベルでトラフィックの割合を指定して、新旧バージョンのアプリケーションを安全かつ効率的にロールアウトできます。

リクエストを投げてみる

上記の内部LBに向かってリクエストを投げ、予定通りの割り当てになっているかを確認します。まずはクライアント用のpodを作って適用し、そのPodの中に入ります。

pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
spec:
  containers:
    - name: test-container
      image: curlimages/curl:latest
      command: ["sleep", "3600"]
$ kubectl apply -f ./k8s/alb/pod.yaml
$ kubectl exec -it test-pod -- sh

そして、その中でリクエストを投げます。

$ curl http://10.146.15.221/callme

下記のように割合に近しいバージョンのレスポンスが返ってくれば成功です。
リクエストの結果
Podの状況

自動での切り戻し

Argo Rolloutsはリクエストの振り分けを柔軟にするだけではなく、問題が起こった場合に自動で切り戻すこともできます。例えば、新しいアプリケーションでエラーが起こった場合でも、ヘルスチェックなどを通過していると、通常では手動で切り戻すしかできません。一方でArgo Rolloutsを使うと、設定したメトリクスに応じて自動で切り戻すことが可能です。
今回は、レスポンスがver: 1.0の場合を成功とし、それ以外の場合を失敗と定義します。そして、ver: 2.0のカナリアリリースを始めた時に自動でロールバックされるかを確認してみます。先ほどのリソースにいくつか追加します。

analysistemplate.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: rollouts-auto-demo-response-check
spec:
  args:
    - name: serviceName
  metrics:
    - name: check-ver-1-0
      interval: 30s
      count: 3
      successCondition: 'result | regexFind("ver: 1.0") != ""'
      failureLimit: 1
      provider:
        web:
          url: 'http://{{args.serviceName}}/callme'
          method: GET
          timeoutSeconds: 5

こちらは、成功・失敗の定義のテンプレートになっており、ver: 1.0の場合を成功と定義しています。次に、rollout.yamlstepsを更新します。

rollout.yaml
      steps:
        - setWeight: 20
        - analysis:
            templates:
              - templateName: rollouts-auto-demo-response-check
            args:
              - name: serviceName
                value: rollouts-auto-demo.default.svc.cluster.local
        - pause: {}
        - setWeight: 40
        - analysis:
            templates:
              - templateName: rollouts-auto-demo-response-check
            args:
              - name: serviceName
                value: rollouts-auto-demo.default.svc.cluster.local
        - pause: { duration: 10 }
        - setWeight: 60
        - analysis:
            templates:
              - templateName: rollouts-auto-demo-response-check
            args:
              - name: serviceName
                value: rollouts-auto-demo.default.svc.cluster.local
        - pause: { duration: 10 }
        - setWeight: 80
        - analysis:
            templates:
              - templateName: rollouts-auto-demo-response-check
            args:
              - name: serviceName
                value: rollouts-auto-demo.default.svc.cluster.local
        - pause: { duration: 10 }

ここでは、各ステップで自動ロールバックが可能になるように、先ほど作成した分析テンプレートを指定します。

$ kubectl apply -f ./k8s/analysis/analysistemplate.yaml
$ kubectl apply -f ./k8s/analysis/rollout.yaml

Auto Rollback
これはテストで簡単に確認できる項目ですが、実用ではレイテンシなど、さまざまなツールと連携してカスタムメトリクスを基準に設定できます。

ダッシュボード

Argo Rolloutsはロールアウトの状況を確認するためのダッシュボードを提供しているので、そちらを使って確認してみます。

$ kubectl argo rollouts dashboard

http://0.0.0.0:3100にアクセスするとDashboard画面があるので、CLIで挙動を確認していたものがGUIで操作できます。
ダッシュボード

まとめ

今回はArgo Rolloutsを使って、通常のPod数での振り分けから、自動ロールバックまでを試してみました。問題があった時に手動で切り戻すことが多かったので、とても便利だなと感じました。
他にもミラーリングがあったりと、本番運用に役立ちそうなリッチな機能が揃っているので、カナリアリリースの選択肢の一つになりそうです。

参考

https://argoproj.github.io/rollouts/
https://github.com/argoproj-labs/rollouts-plugin-trafficrouter-gatewayapi/tree/main/examples/google-cloud
https://github.com/kostis-codefresh/summer-of-k8s-app

Discussion