Argo Rolloutsでカナリアリリース
概要
Argo Rolloutsを使ってカナリアリリースを行う環境構築のメモ。
ubuntu上にminikubeでk8sクラスタを用意し、SpringBootのアプリケーションをデプロイする。
まずは手動でのカナリアリリースを確認し、Prometheusのメトリクス分析を使った自動リリースも行う。
環境構築
以下の順番でセットアップ
- docker registry
- minikube
- Argo Rollouts
- SpringBootアプリケーション
docker registry
dockerhubにはpullの回数制限があるので、ローカルにprivateレジストリを構築(dockerhubを利用する場合は不要)
docker run -d -p 5000:5000 --restart always --name registry registry:2
これで以下のように使える。
docker push localhost:5000/alpine
minikube
minikube startを参考にminkubeをセットアップ
ローカルのdocker registryの場合localhost:5000だと接続できないのでマシンのIPを指定する必要がある。その場合httpsで接続しようとするので--insecure-registryでminikube起動時に除外する。
minikube start --insecure-registry=10.0.2.15:5000
すでにminikubeを起動している場合、--insecure-registryが反映されないので、一度クラスタを削除する必要がある。
minikube delete
ingressを利用するのでaddonを有効にする
minikube addons enable ingress
Argo Rollouts
Getting Startedを参考に準備。
専用のnamespaceを用意
kubectl create namespace argo-rollouts
インストール
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
kubectlプラグインをインストール
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x ./kubectl-argo-rollouts-linux-amd64
sudo mv ./kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
動作を確認
❯ kubectl argo rollouts version
kubectl-argo-rollouts: v1.8.2+0775302
BuildDate: 2025-03-21T19:14:39Z
GitCommit: 0775302f6fd901f557a0c14be327e31ce75bb45a
GitTreeState: clean
GoVersion: go1.23.7
Compiler: gc
Platform: linux/amd64
SpringBootアプリケーション
リリース時にどのバージョンかを確認するため、versionを返すControllerを用意
@RestController
public class MainController {
@GetMapping(value = "/hello")
public ResponseEntity<String> getUsers() {
return ResponseEntity.ok().body("{\"version\":1}");
}
}
動作確認
❯ curl -sS http://localhost:8080/hello
{"version":1}
k8sリソース
serviceはcanaryとstableを用意する。
apiVersion: v1
kind: Service
metadata:
name: my-api-canary
namespace: dev
spec:
selector:
app: my-api
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
---
apiVersion: v1
kind: Service
metadata:
name: my-api-stable
namespace: dev
spec:
selector:
app: my-api
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: ClusterIP
ホスト名my-api.localで、serviceはstableの方を指定
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-api-stable
namespace: dev
spec:
rules:
- host: my-api.local
http:
paths:
- pathType: ImplementationSpecific
backend:
service:
name: my-api-stable
port:
number: 80
deploymentは定義せず、Rolloutとして設定する。spec.strategy.canary.steps[].setWeightでカナリアの割合を指定(今回は20%)
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-api
namespace: dev
spec:
replicas: 1
strategy:
canary:
canaryService: my-api-canary
stableService: my-api-stable
trafficRouting:
nginx:
stableIngress: my-api-stable
steps:
- setWeight: 20
- pause: {}
revisionHistoryLimit: 2
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
containers:
- name: my-api
image: 10.0.2.15:5000/my-api:v1
imagePullPolicy: Always
ports:
- name: http
containerPort: 8080
protocol: TCP
その他は https://github.com/bassaer/my-api を参照
アプリケーションをビルドしてdocker registryにv1とv2を登録しておく。
./gradlew clean build
docker build -t my-api .
docker tag my-api localhost:5000/my-api:v1
docker push localhost:5000/my-api:v1
registryは以下のような感じ。v1とv2はAPIレスポンス内のversionと対応している。
❯ curl -sS http://localhost:5000/v2/my-api/tags/list
{"name":"my-api","tags":["latest","v2","v1"]}
リリース
まず、k8sリソースをapplyし、v1が起動している状態にする。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 2/2
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1 (stable)
Replicas:
Desired: 1
Current: 1
Updated: 1
Ready: 1
Available: 1
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 7s
└──# revision:1
└──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 7s stable
└──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 7s ready:1/1
動作確認用にminikube ip
で取得したIPを/etc/hostsに設定する
192.168.49.2 my-api.local
これでデプロイされたアプリケーションにリクエストできる。
❯ curl -sS http://my-api.local/hello
{"version":1}
カナリアリリース
以下のコマンドでブラウザから操作できる。
kubectl-argo-rollouts dashboard
ただし、UIからだとポート番号を含むコンテナ名が正しく入力できなかったので、set imageで書き換える。書き換えると自動でカナリアリリースが開始される。
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v2
再度rolloutを見てみると、canaryのpodがデプロイされている。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/2
SetWeight: 20
ActualWeight: 20
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ॥ Paused 2m51s
├──# revision:2
│ └──⧉ my-api-6cd7b5995c ReplicaSet ✔ Healthy 7s canary
│ └──□ my-api-6cd7b5995c-5wvw9 Pod ✔ Running 7s ready:1/1
└──# revision:1
└──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 2m51s stable
└──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 2m51s ready:1/1
一応20%に振り分けられるのかを以下のスクリプトで検証(100回リクエストし、レスポンスのバージョンをカウントする)
import urllib.request
import json
from collections import Counter
def count():
counter = Counter()
for _ in range(100):
try:
with urllib.request.urlopen('http://my-api.local/hello') as response:
data = response.read()
json_data = json.loads(data)
version = json_data.get("version")
if version is not None:
counter[f"version {version}"] += 1
except Exception as e:
counter['error '] += 1
for key, count in counter.items():
print(f"{key} : {count}")
if __name__ == '__main__':
count()
概ね20%になっている。
❯ python3 scripts/counter.py
version 1 : 82
version 2 : 18
ロールバック
不具合などを検知した場合以下を実行し、ロールバックする。
kubectl argo rollouts -n dev abort my-api
rolloutは以下のようになる。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✖ Degraded
Message: RolloutAborted: Rollout aborted update to revision 2
Strategy: Canary
Step: 0/2
SetWeight: 0
ActualWeight: 0
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✖ Degraded 4m44s
├──# revision:2
│ └──⧉ my-api-6cd7b5995c ReplicaSet ✔ Healthy 2m canary,delay:12s
│ └──□ my-api-6cd7b5995c-5wvw9 Pod ✔ Running 2m ready:1/1
└──# revision:1
└──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 4m44s stable
└──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 4m44s ready:1/1
実際にリクエストしてみるとロールバックされているのがわかる。
❯ python3 scripts/counter.py
version 1 : 100
もう1度v1のイメージをセットすると
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v1
StatusがHealthyになって完全にロールバックされる。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 2/2
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1 (stable)
Replicas:
Desired: 1
Current: 1
Updated: 1
Ready: 1
Available: 1
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 7m21s
├──# revision:3
│ └──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 7m21s stable
│ └──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 7m21s ready:1/1
└──# revision:2
└──⧉ my-api-6cd7b5995c ReplicaSet • ScaledDown 4m37s delay:passed
100%リリース
さきほどと同様にv2のカナリアリリースを開始する
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v2
状態はカナリアリリース開始地点と同様
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/2
SetWeight: 20
ActualWeight: 20
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ॥ Paused 9m26s
├──# revision:4
│ └──⧉ my-api-6cd7b5995c ReplicaSet ✔ Healthy 6m42s canary
│ └──□ my-api-6cd7b5995c-kthn7 Pod ✔ Running 7s ready:1/1
└──# revision:3
└──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 9m26s stable
└──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 9m26s ready:1/1
❯ python3 scripts/counter.py
version 1 : 78
version 2 : 22
次はpromoteを実行し、v2を100%リリースする。
kubectl argo rollouts -n dev promote my-api
v2がstableになり完了。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 2/2
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1
10.0.2.15:5000/my-api:v2 (stable)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 12m
├──# revision:4
│ └──⧉ my-api-6cd7b5995c ReplicaSet ✔ Healthy 9m21s stable
│ └──□ my-api-6cd7b5995c-kthn7 Pod ✔ Running 2m46s ready:1/1
└──# revision:3
└──⧉ my-api-5dc98484f8 ReplicaSet ✔ Healthy 12m delay:12s
└──□ my-api-5dc98484f8-gzhvp Pod ✔ Running 12m ready:1/1
レスポンスもすべて2になる。
❯ python3 scripts/counter.py
version 2 : 100
リリース自動化
先ほどは手動でロールバック/100%リリースを行ったがPrometheusのメトリクスを監視し、自動でロールバック・100%リリース行うようにする。
prometheusのセットアップ
helmを使ってprometheusをセットアップする。
prometheusのrepoを登録
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
更新
helm repo update
prometehusをデプロイ
helm upgrade prometheus prometheus-community/prometheus -install --namespace monitoring --create-namespace
メトリクス収集と分析のための修正
以下を設定
- prometheusのスクレイプ用annotation
- canary/stableをメトリクスで判別するためのラベル
- カナリアリリース後に5分停止し、メトリクス分析を行うように修正
diff --git a/deploy/rollout.yaml b/deploy/rollout.yaml
index 2bb5c1c..f158841 100644
--- a/deploy/rollout.yaml
+++ b/deploy/rollout.yaml
@@ -9,12 +9,21 @@ spec:
canary:
canaryService: my-api-canary
stableService: my-api-stable
+ canaryMetadata:
+ labels:
+ role: canary
+ stableMetadata:
+ labels:
+ role: stable
trafficRouting:
nginx:
stableIngress: my-api-stable
steps:
- setWeight: 20
- - pause: {}
+ - pause: {duration: 5m}
+ - analysis:
+ templates:
+ - templateName: application-metrics
revisionHistoryLimit: 2
selector:
matchLabels:
@@ -23,6 +32,10 @@ spec:
metadata:
labels:
app: my-api
+ annotations:
+ prometheus.io/scrape: 'true'
+ prometheus.io/port: '8080'
+ prometheus.io/path: /actuator/prometheus
spec:
containers:
- name: my-api
@@ -32,3 +45,8 @@ spec:
- name: http
containerPort: 8080
protocol: TCP
+ env:
+ - name: RELEASE_ROLE
+ valueFrom:
+ fieldRef:
+ fieldPath: metadata.labels['role']
SpringBoot側では以下のように環境変数からタグを設定
@Configuration
public class MeterRegistryConfiguration {
@Bean
public MeterRegistryCustomizer<MeterRegistry> metricCommonTags() {
return (registry) -> registry.config().commonTags("role", System.getenv("RELEASE_ROLE"));
}
}
AnalysisTemplateは以下のようにレスポンスステータスのエラーレートが5%未満であることをチェックします。Prometheusのアドレスはminikubeの場合minikube tunnel
を実行し、kubectl get service -n monitoring
でprometheus-serverのIPを確認することができる。
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: application-metrics
namespace: dev
spec:
metrics:
- name: error-rate
successCondition: "all(result, # < 0.05)"
provider:
prometheus:
address: http://10.105.225.32
query: |
sum by(role)(rate(http_server_requests_seconds_count{status!="200", uri="/hello"}[5m]))
/
sum by(role)(rate(http_server_requests_seconds_count{uri="/hello"}[5m]))
OR on() vector(0)
image準備
ランダムにInternalServerErrorになるようなv2を用意
@RestController
public class MainController {
@GetMapping(value = "/hello")
public ResponseEntity<String> getUsers() {
if (new Random().nextBoolean()) {
throw new ErrorResponseException(HttpStatus.INTERNAL_SERVER_ERROR);
}
return ResponseEntity.ok().body("{\"version\":2}");
}
}
- v1
-
{"version":1}
を返す
-
- v2
-
{"version":2}
or InternalServerError を返す
-
- v3
-
{"version":3}
を返す
-
動作確認
シナリオ
以下のようにロールバックと100%反映が自動で行わることを確認する
- v1がデプロイされている状態
- v2をカナリアリリース
- 5分後エラーレート上昇を検知し、カナリアリリースが自動停止してv1が100%に戻る
- v3をカナリアリリース
- 5分後エラーレートが正常であることを検知し、自動でv3が100%反映される
エラーレートの変動を今回は確認するのでvegetaで常に10rpsリクエストをかけた状態にしておく
echo "GET http://my-api.local/hello" | vegeta attack -rate=10 -duration=1h > /dev/null
1. v1がデプロイされている状態
Rolloutを全部削除し、すべてのResouceファイルをapplyする。現状v1がデプロイされている状態
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 3/3
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1 (stable)
Replicas:
Desired: 1
Current: 1
Updated: 1
Ready: 1
Available: 1
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 50s
└──# revision:1
└──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 50s stable
└──□ my-api-bc9464b65-qdhhr Pod ✔ Running 50s ready:1/1
2. v2をカナリアリリース
v2のイメージをセットし、カナリアリリースを開始する
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v2
canaryのpodが生成され、5分停止する。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/3
SetWeight: 20
ActualWeight: 20
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ॥ Paused 2m40s
├──# revision:2
│ └──⧉ my-api-667b8d477b ReplicaSet ✔ Healthy 51s canary
│ └──□ my-api-667b8d477b-ckr5c Pod ✔ Running 50s ready:1/1
└──# revision:1
└──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 2m40s stable
└──□ my-api-bc9464b65-qdhhr Pod ✔ Running 2m40s ready:1/1
先ほどのスクリプトでレスポンスを確認するとv2の半分エラーになっている。
❯ python3 scripts/counter.py
version 1 : 78
version 2 : 10
error : 12
ブラウザのUIから確認すると以下のような感じ
3. 5分後エラーレート上昇を検知し、カナリアリリースが自動停止してv1が100%に戻る
5分後に確認すると以下のようにDegradedになっている。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✖ Degraded
Message: RolloutAborted: Rollout aborted update to revision 2: Metric "error-rate" assessed Failed due to failed (1) > failureLimit (0)
Strategy: Canary
Step: 0/3
SetWeight: 0
ActualWeight: 0
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v2 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✖ Degraded 7m13s
├──# revision:2
│ ├──⧉ my-api-667b8d477b ReplicaSet ✔ Healthy 5m24s canary,delay:8s
│ │ └──□ my-api-667b8d477b-ckr5c Pod ✔ Running 5m23s ready:1/1
│ └──α my-api-667b8d477b-2-2 AnalysisRun ✖ Failed 21s ✖ 1
└──# revision:1
└──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 7m13s stable
└──□ my-api-bc9464b65-qdhhr Pod ✔ Running 7m13s ready:1/1
レスポンスもすべてv1
❯ python3 scripts/counter.py
version 1 : 100
UIでもエラーとなっているのがわかる。
手動でv1のイメージをセットし完全に復旧しておく。
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v1
ステータスがhealthyに戻る。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 3/3
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1 (stable)
Replicas:
Desired: 1
Current: 1
Updated: 1
Ready: 1
Available: 1
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 13m
├──# revision:3
│ └──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 13m stable
│ └──□ my-api-bc9464b65-qdhhr Pod ✔ Running 13m ready:1/1
└──# revision:2
├──⧉ my-api-667b8d477b ReplicaSet • ScaledDown 11m delay:passed
└──α my-api-667b8d477b-2-2 AnalysisRun ✖ Failed 6m30s ✖ 1
4. v3をカナリアリリース
v2と同様にv3をカナリアリリース
kubectl argo rollouts -n dev set image my-api my-api=10.0.2.15:5000/my-api:v3
デプロイが完了し、5分停止する。
❯ kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ॥ Paused
Message: CanaryPauseStep
Strategy: Canary
Step: 1/3
SetWeight: 20
ActualWeight: 20
Images: 10.0.2.15:5000/my-api:v1 (stable)
10.0.2.15:5000/my-api:v3 (canary)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ॥ Paused 15m
├──# revision:4
│ └──⧉ my-api-5c7484bf87 ReplicaSet ✔ Healthy 54s canary
│ └──□ my-api-5c7484bf87-td44q Pod ✔ Running 54s ready:1/1
├──# revision:3
│ └──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 15m stable
│ └──□ my-api-bc9464b65-qdhhr Pod ✔ Running 15m ready:1/1
└──# revision:2
├──⧉ my-api-667b8d477b ReplicaSet • ScaledDown 13m delay:passed
└──α my-api-667b8d477b-2-2 AnalysisRun ✖ Failed 8m47s ✖ 1
レスポンスも正常
❯ python3 scripts/counter.py
version 1 : 79
version 3 : 21
5. 5分後エラーレートが正常であることを検知し、自動でv3が100%反映される
v3がstableになり、100%反映されている
kubectl-argo-rollouts get rollout -n dev my-api
Name: my-api
Namespace: dev
Status: ✔ Healthy
Strategy: Canary
Step: 3/3
SetWeight: 100
ActualWeight: 100
Images: 10.0.2.15:5000/my-api:v1
10.0.2.15:5000/my-api:v3 (stable)
Replicas:
Desired: 1
Current: 2
Updated: 1
Ready: 2
Available: 2
NAME KIND STATUS AGE INFO
⟳ my-api Rollout ✔ Healthy 20m
├──# revision:4
│ ├──⧉ my-api-5c7484bf87 ReplicaSet ✔ Healthy 5m19s stable
│ │ └──□ my-api-5c7484bf87-td44q Pod ✔ Running 5m19s ready:1/1
│ └──α my-api-5c7484bf87-4-2 AnalysisRun ✔ Successful 17s ✔ 1
├──# revision:3
│ └──⧉ my-api-bc9464b65 ReplicaSet ✔ Healthy 20m delay:12s
│ └──□ my-api-bc9464b65-qdhhr Pod ✔ Running 20m ready:1/1
└──# revision:2
├──⧉ my-api-667b8d477b ReplicaSet • ScaledDown 18m delay:passed
└──α my-api-667b8d477b-2-2 AnalysisRun ✖ Failed 13m ✖ 1
レスポンスもすべてv3
❯ python3 scripts/counter.py
version 3 : 100
UI上も成功
Discussion
dashboardからポート番号付きでイメージを入力できない問題はこちらでPRを出した