hisamounaのブログ

アウトプットを習慣化するためのブログ

argo-rolloutsのPromoteについて調べる

argocdのGUI上でargo-rolloutsリソースに対してできるオペレーションに、 PROMOTEPROMOTE-FULLがありますが、 ドキュメントをざっと調べた限り、詳細な挙動が分からなかったのでコードを見てみました。

画面のサンプル

利用できない操作は半透明にして非活性化されています。

f:id:hisamouna:20211121190106p:plain
argo-rollouts-dashboard

argo-rolloutsとは

Deploymentリソースに対してblue-greenやcanaryといった拡張機能を追加することができるCRDsとcontrollerを提供してくれています。

github.com

Deploymentの代わりとなるリソースのため、v1.0からは Workload Referencingと呼ばれる既存のDeploymentを参照できる機能も導入されているようです。

blog.argoproj.io

PromoteのAPIがcallされるまで

ブラウザで、 PROMOTEまたはPROMOTE-FULLボタンをクリック -> promoteのAPIをfetchしていました。

長くなるので、フロントエンドの詳細は今回は割愛します。

APIの定義

APIのインターフェースを確認してみました。 ブラウザから叩かれるAPIでもあるので、grpc-gatewayを利用して、RESTのインターフェースも定義しています。

rpc PromoteRollout(PromoteRolloutRequest) returns (github.com.argoproj.argo_rollouts.pkg.apis.rollouts.v1alpha1.Rollout) {
    option (google.api.http) = {
        put: "/api/v1/rollouts/{namespace}/{name}/promote"
        body: "*"
    };
}

https://github.com/argoproj/argo-rollouts/blob/master/pkg/apiclient/rollout/rollout.proto#L187-L192

APIの実装

ProtoBufから生成されたgoファイル(rollout.pb.go)で書かれているinterfaceを満たすstructを生成し、登録しています。

func (s *ArgoRolloutsServer) newGRPCServer() *grpc.Server {
    grpcS := grpc.NewServer()
    var rolloutsServer rollout.RolloutServiceServer = NewServer(s.Options)
    rollout.RegisterRolloutServiceServer(grpcS, rolloutsServer)
    return grpcS
}

https://github.com/argoproj/argo-rollouts/blob/master/server/server.go#L130-L135

interfaceで定義しているPromoteRolloutメソッドを実装している箇所。

rolloutIfがrolloutのデータや振る舞いが定義されています。

Patch処理もここで定義されていますが、内部ではclient-goを利用していました。

実際に、Promoteも行うメソッドが promote.PromoteRolloutです。

func (s *ArgoRolloutsServer) PromoteRollout(ctx context.Context, q *rollout.PromoteRolloutRequest) (*v1alpha1.Rollout, error) {
    rolloutIf := s.Options.RolloutsClientset.ArgoprojV1alpha1().Rollouts(q.GetNamespace())
    return promote.PromoteRollout(rolloutIf, q.GetName(), false, false, q.GetFull())
}

https://github.com/argoproj/argo-rollouts/blob/master/server/server.go#L384-L387

Promoteのロジック

https://github.com/argoproj/argo-rollouts/blob/master/pkg/kubectl-argo-rollouts/cmd/promote/promote.go

メソッド呼び出し元で、skipCurrentStepとskipAllStepsはfalseにしているので、ここのif処理は常にスキップされます。

trueをセットした呼び出しはtestコードしかなさそうでした。

if skipCurrentStep || skipAllSteps {
    if ro.Spec.Strategy.BlueGreen != nil {
        return nil, fmt.Errorf(skipFlagsWithBlueGreenError)
    }
    if ro.Spec.Strategy.Canary != nil && len(ro.Spec.Strategy.Canary.Steps) == 0 {
        return nil, fmt.Errorf(skipFlagWithNoStepCanaryError)
    }
}

v0.9とv0.10+で互換性を持たせるために getPatchesを実行しています。

status subresourcesと呼ばれるCRDがv0.10からのみ使用されているからのようです。

specPatch, statusPatch, unifiedPatch := getPatches(ro, skipCurrentStep, skipAllSteps, full)

PROMOTE-FULLボタンをクリックしたときはstatus.promoteFullフィールドにtrueをセット。

case full:
        if rollout.Status.CurrentPodHash != rollout.Status.StableRS {
            statusPatch = []byte(promoteFullPatch)
        }

PROMOTEボタンをクリックしたときはもう少し複雑です。

rolloutがpaused状態であったらpausedフィールドをfalseにし、clearPauseConditionsPatchフィールドに値があればnullをセットしています。 paused状態で処理がpendingとなっているのをクリアにするようにしています。

if rollout.Spec.Paused {
            specPatch = []byte(unpausePatch)
        }
        if len(rollout.Status.PauseConditions) > 0 {
            statusPatch = []byte(clearPauseConditionsPatch)

strategyをcanaryにしている場合はcanaryのstepを1つ進めるようにindexをインクリメントしています。

_, index := replicasetutil.GetCurrentCanaryStep(rollout)

if index != nil {
    if *index < int32(len(rollout.Spec.Strategy.Canary.Steps)) {
        *index++
    }
    statusPatch = []byte(fmt.Sprintf(clearPauseConditionsPatchWithStep, *index))
    unifiedPatch = []byte(fmt.Sprintf(unpauseAndClearPauseConditionsPatchWithStep, *index))
}

いざ、Patch実行

client-goを使って、Patchをapi-serverに対してリクエストしています。

if statusPatch != nil {
    ro, err = rolloutIf.Patch(ctx, name, types.MergePatchType,
    ...
}
if specPatch != nil {
    ro, err = rolloutIf.Patch(ctx, name, types.MergePatchType, 
    ...
}

どういったときに利用できるのか

B/Gを利用している場合であれば、 postPromotionAnalysisが一時的な何らかの問題で失敗してしまった時に手動で処理を進めたい際に利用できそうです。

promoteを使えば、rolloutを最初からやり直し(podの再作成)することなく済むかと思います。

Canaryであれば、例えば sleep 120sのstepをスキップしたいときなどに使えそうです。