Argo Rolloutsのソースコードを読んでみた

## はじめに

この記事では、Argo Rolloutsのソースコードを通して、ArgoがKubernetesをどのように操作するのか、その仕組みの概要を理解できる内容となってます。

ドキュメントを読んでもいまいち分からなかった方にとって、Argoの動作メカニズムを少しでも理解する手助けになれば幸いです。

前提としてそれぞれの機能を深掘りはせず、あくまでKubernetesをどのように操作するのかをまとめたものになります。
そのため、それぞれの詳しい機能に関しては公式ドキュメント[^1]や他の記事を参考にしていただくようお願いいたします。

## Argo Rolloutsとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584506/ccf8551b-e2c7-bd43-1363-c1bec19a8167.png)

Argo Rollouts(アルゴロールアウト) は、Kubernetes環境にデプロイメントのための機能を提供するGo言語で書かれたオープンソースのツールセットです。[^2]

主な特徴としては、以下のような高度なデプロイメント機能が提供されていることです。
* ブルー/グリーンデプロイメント
* カナリアデプロイメント
* 自動ロールバックとプロモーション

Argo Rolloutsは、KubernetesコントローラーとCRDのセットを利用して、これらの高度なデプロイ機能を提供しています。[^1]

オープンソースであることから、Githubにソースコードが公開されています。

https://github.com/argoproj/argo-rollouts/

Argo Projectには、Argo Rolloutsの他にも以下のプロジェクトがあります。

* [Argo CD](https://github.com/argoproj/argo-cd)
* [Argo Workflows](https://github.com/argoproj/argo-workflows)
* [Argo Events](https://github.com/argoproj/argo-events)

## Argo Rolloutsが提供する機能

Argo Rolloutsが提供する主な機能は以下の2つです。
* kubectl-argo-rollouts
* rollouts-controller

https://github.com/argoproj/argo-rollouts/tree/master/cmd

`kubectl-argo-rollouts`は、Kubernetesを操作するときに利用するコマンド`kubectl`の拡張版コマンドのようなものです。

`rollouts-controller`について、公式ドキュメントには以下のような記載がありました。[^3]
> Similar to the deployment object, the Argo Rollouts controller will manage the creation, scaling, and deletion of ReplicaSets. These ReplicaSets are defined by the spec.template field inside the Rollout resource, which uses the same pod template as the deployment object.

`rollouts-controller`はKubernetesクラスター内で、ReplicaSetsの作成、スケーリング、削除を管理するものです。

## Kubernetesリソースの操作について

### kubectl-argo-rollouts
まず、前述した機能の内`kubectl-argo-rollouts`を確認します。
やっていることは、`NewCmdArgoRollouts`を実行なので、次はそのソースコードを確認します。

```golang
func main() {
	klog.InitFlags(nil)
	logutil.SetKLogLogger(log.New())
	streams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr}
	o := options.NewArgoRolloutsOptions(streams)
	root := cmd.NewCmdArgoRollouts(o)
	if err := root.Execute(); err != nil {
		os.Exit(1)
	}
}
```
https://github.com/argoproj/argo-rollouts/blob/master/cmd/kubectl-argo-rollouts/main.go

### NewCmdArgoRollouts
`NewCmdArgoRollouts`には、色々なコマンドをセットして実行していることが分かります。

なかには以下のようなArgo CDのダッシュボードで見慣れた機能があります。

![スクリーンショット 2024-12-02 214607.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584506/a844ee31-d4b9-b23c-cbf8-a065357227ab.png)

今回は「Abort」=`NewCmdAbort`のソースコードを確認します。

```golang
// 抜粋
o.AddKubectlFlags(cmd)
cmd.AddCommand(create.NewCmdCreate(o))
cmd.AddCommand(get.NewCmdGet(o))
cmd.AddCommand(lint.NewCmdLint(o))
cmd.AddCommand(list.NewCmdList(o))
cmd.AddCommand(pause.NewCmdPause(o))
cmd.AddCommand(promote.NewCmdPromote(o))
cmd.AddCommand(restart.NewCmdRestart(o))
cmd.AddCommand(version.NewCmdVersion(o))
cmd.AddCommand(abort.NewCmdAbort(o))
cmd.AddCommand(retry.NewCmdRetry(o))
 ```

### NewCmdAbort

`rolloutIf.Patch`を実行しています。
`Patch`とみて、まず思いつくのが、REST APIにおける`PATCH`です。（一般的には、リソースへの部分的な変更を適用に利用する）
次は`RolloutInterface`を確認します。

```golang
const (
	abortPatch = `{"status":{"abort":true}}`
)
// 省略
func AbortRollout(rolloutIf clientset.RolloutInterface, name string) (*v1alpha1.Rollout, error) {
	ctx := context.TODO()
	ro, err := rolloutIf.Patch(ctx, name, types.MergePatchType, []byte(abortPatch), metav1.PatchOptions{}, "status")
	if err != nil && k8serrors.IsNotFound(err) {
		ro, err = rolloutIf.Patch(ctx, name, types.MergePatchType, []byte(abortPatch), metav1.PatchOptions{})
	}
	return ro, err
}
```
https://github.com/argoproj/argo-rollouts/blob/master/pkg/kubectl-argo-rollouts/cmd/abort/abort.go

### RolloutInterface

`c.client.Patch`は一般的なREST APIを叩くときのソースコードに見えます。
`client`の正体はk8s.io/client-goです。

```golang
// 抜粋
import (
	rest "k8s.io/client-go/rest"
)
type RolloutInterface interface {
	Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Rollout, err error)
}
// rollouts implements RolloutInterface
type rollouts struct {
	client rest.Interface
	ns     string
}
// Patch applies the patch and returns the patched rollout.
func (c *rollouts) Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts v1.PatchOptions, subresources ...string) (result *v1alpha1.Rollout, err error) {
	result = &v1alpha1.Rollout{}
	err = c.client.Patch(pt).
		Namespace(c.ns).
		Resource("rollouts").
		Name(name).
		SubResource(subresources...).
		VersionedParams(&opts, scheme.ParameterCodec).
		Body(data).
		Do(ctx).
		Into(result)
	return
}
```
https://github.com/argoproj/argo-rollouts/blob/master/pkg/client/clientset/versioned/typed/rollouts/v1alpha1/rollout.go

### k8s.io/client-go

`client-go`とは、Kubernetesクラスターをgoアプリから操作するためのライブラリです。

https://pkg.go.dev/k8s.io/client-go

READMEには、以下の記載があります。
> The kubernetes package contains the clientset to access Kubernetes API.

`client-go`を使うことで、Kubernetes APIにアクセスできます。

以下のKubernetesクラスターを構成するコンポーネントから分かる通り、Kubernetes APIは他コンポーネントからリクエストを処理し、同じクラスター内のリソースを操作できます。[^4] [^5]

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3584506/fa5137ca-e3fc-0d66-1f6a-a3ee882b46a5.png)

Argo Rolloutsは内部でKubernetes APIを叩くことで、Kubernetesのリソースを操作していたことが分かりました。

なお、前述の「Abort」については、Kubernetes APIに対して`PATCH`でリクエストを送信し、クラスター内で以下のコマンドを実行しているイメージになります。

```bash
kubectl patch rollout --type merge -p {"status":{"abort":true}}
```

### (おまけ)自動生成ツール

Argo Rolloutsのソースコードを読む中でマークコメントが多かったです。
そのため、ここからはおまけとして、筆者が気になった自動生成ツールを記載します。

前提として、ArgoはCustom Resource Definitions(CRD)という、Kubernetes APIを拡張した独自のリソース(Custom Resource=CR)を定義しています。

CRDは2つの構成要素があり、概要は以下になります。[^6]
* カスタムリソース：特定のクラスター環境に応じたリソースをカスタムリソースを定義する
* カスタムコントローラー：指定したリソースの状態を監視しそれを維持するように動作する

CRについては以下のディレクトリにあります。

https://github.com/argoproj/argo-rollouts/tree/master/manifests

CRDの一例は以下にあります。

https://github.com/argoproj/argo-rollouts/blob/master/pkg/apis/rollouts/v1alpha1/types.go

CRDには以下のマーカーコメントがあります。
```golang
// 抜粋
// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +kubebuilder:resource:path=rollouts,shortName=ro
//+kubebuilder:subresource:scale:specpath=.spec.replicas,statuspath=.status.HPAReplicas,selectorpath=.status.selector
// +kubebuilder:subresource:status
```
https://github.com/argoproj/argo-rollouts/blob/master/pkg/apis/rollouts/v1alpha1/types.go

* `+genclient`
    * [client-gen](https://pkg.go.dev/k8s.io/code-generator/cmd/client-gen)というCRDに対するクライアントコードを自動生成するツールに関するマーカーコメント
    * 前述の`RolloutInterface`を定義しているファイルには、`client-gen`によって生成されたコードであることを示す記載がありました
```golang
// Code generated by client-gen. DO NOT EDIT.
```
* `+k8s:deepcopy-gen`
    * [deepcopy-gen](https://pkg.go.dev/k8s.io/gengo/examples/deepcopy-gen)というdeepcopyを自動生成するツールに関するマーカーコメント
    * [zz_generated.deepcopy.go](https://github.com/argoproj/argo-rollouts/blob/master/pkg/apis/rollouts/v1alpha1/zz_generated.deepcopy.go)には、`deepcopy-gen`によって生成されたコードであることを示す記載がありました
```golang
// Code generated by deepcopy-gen. DO NOT EDIT.
```
* `+kubebuilder`
    * [Kubebuilder](https://book.kubebuilder.io/)というKubernetesを拡張するためのカスタムコントローラー/オペレーターを開発するためのフレームワーク

前述の自動生成ツールを利用することで、Kubernetesを操作する実装を簡易化していることが分かります。

## さいごに

Argo Rolloutsは、Kubernetes APIにリクエストを投げることで、リソースを操作していることが分かりました。
しかし、詳細までは確認できていないため、今後調べていこうと思います！

おまけでKubernetesに関する自動生成ツールについても簡単に調べましたがすごく充実している印象を受けました。
やはりKubernetesとgoの親和性が高いですね。

[^1]:https://argoproj.github.io/argo-rollouts/
[^2]:https://openstandia.jp/oss_info/argorollouts/
[^3]:https://argo-rollouts.readthedocs.io/en/stable/#how-does-it-work
[^4]:https://kubernetes.io/ja/docs/concepts/overview/kubernetes-api/
[^5]:https://kubernetes.io/ja/docs/concepts/overview/components/
[^6]:https://kubernetes.io/ja/docs/concepts/extend-kubernetes/api-extension/custom-resources/