ArgoCD: ApplicationSet controller で Application 管理を楽にしよう

# ApplicationSet controller とは

ApplicationSet controller は ArgoCD の Application リソースをまとめて生成・管理するためのコントローラです。ApplicationSet controller は ArgoCD のサブプロジェクトとして、複数のクラスタ間やモノレポにある Application リソースの自動化や柔軟な管理を目的に開発が進められたようです。

ApplicationSet controller を利用することで冗長になりやすかった複数ある Application リソースを ApplicationSet リソースとして一つにまとめられメンテナンスコストの削減に繋がります。

この記事では現時点(2021/07/27)で最新版である v0.1.0 (2021/04/07 released) を対象とします。

# インストール方法

*すでに ArgoCD を動作させているクラスタがある前提でインストール手順を紹介します*
手順は簡単で公式マニュアルに書いてあるとおりの手順でインストールすれば動作しました。

ArgoCD がすでに動作しているクラスタ(argocd namespace に存在する前提)であれば以下の手順を実行するだけでインストールすることができます。

```shell
$ kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj-labs/applicationset/v0.1.0/manifests/install.yaml
```

[公式マニュアル](https://argocd-applicationset.readthedocs.io/en/stable/Geting-Started/)には ArgoCD とまとめてインストールできる YAML ファイルも用意されているのでもし未インストールのクラスタで試したい場合はそちらを利用すると良いかもしれません。

インストールできると以下のように Application controller pod が起動すると思います。

```
$ kubectl -n argocd get pod
...
argocd-applicationset-controller-76fc4dc766-8ndzp   1/1     Running   0          20h
...
```

Podが正常に Running になっていたら無事インストール完了です。

# 使い方

ApplicationSet controller の Pod が正常に起動したら ApplicationSet を使って実際に Application リソースを作ってみます。

使い方は名前からなんとなくイメージができると思いますが、Kubernetes の RepliacaSet と Pod の関係と似ていて ApplicationSet に以下の2点を記述し、その2点を ApplicationSet controller が見て Application リソースを生成してくれるというものになります。

- generator
    - 生成する単位とtemplatesを使ってどのように生成するかを指定するフィールド
- templates
    - Application リソースのテンプレート
    - generator 毎に用意された変数をテンプレートに利用できる

実際にやってみましょう。公式にあるサンプルをデプロイしてみます。(一部古いのか誤っている部分があるので修正しています)

```guestbook.yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
spec:
  generators:
  - list:
      elements:
      - cluster: engineering-dev
        url: https://1.2.3.4
      - cluster: engineering-prod
        url: https://2.4.6.8
      - cluster: finance-preprod
        url: https://9.8.7.6
  template:
    metadata:
      name: '{{cluster}}-guestbook'
    spec:
      source:
        repoURL: https://github.com/infra-team/cluster-deployments.git
        targetRevision: HEAD
        path: 'guestbook/{{cluster}}'
      destination:
        server: '{{url}}'
        namespace: guestbook
      project: default
```

```shell
$ kubectl -n argocd apply -f guestbook.yaml
applicationset.argoproj.io/guestbook created
```

デプロイしたので見てみます。

```shell
$ kubectl -n argocd get applicationset
NAME                        AGE
guestbook                   95s
$ kubectl -n argocd get application
No resources found in argocd namespace.
```

Application リソースは生成されていませんでした。
コントローラのログや ApplicationSet リソースの status を見てみましたが、特にエラーなども出ていません。
登録されていないクラスタが指定してあるのが原因なのかもしれません。クラスタを正しい値にして試してみます。

```diff
spec:
  generators:
  - list:
      elements:
      - cluster: engineering-dev
-        url: https://1.2.3.4
+        url: ここを正しいクラスタのURLに変更する
```

```shell
$ kubectl -n argocd get apps　engineering-dev-guestbook -o yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  creationTimestamp: "2021-07-27T02:58:16Z"
  finalizers:
  - resources-finalizer.argocd.argoproj.io
  generation: 5
  name: engineering-dev-guestbook
  namespace: argocd
  ownerReferences:
  - apiVersion: argoproj.io/v1alpha1
    blockOwnerDeletion: true
    controller: true
    kind: ApplicationSet
    name: guestbook
    uid: 06dd86e8-2890-43b0-b7a0-bf588f6cde4d
  resourceVersion: "378942533"
  uid: 72928e11-556c-4af9-b922-575c3be48db3
spec:
  destination:
    namespace: guestbook
    server: https://xxxxxxxxxxxxxxx.git
  project: default
  source:
    path: guestbook/
    repoURL: https://github.com/infra-team/cluster-deployments.git
    targetRevision: HEAD
```

無事生成されました。Health が Unknown になっていますが上記のリポジトリが存在しないため同期ができないためです。正しいリポジトリを設定すれば動作します。
上記のように templates の変数が展開され Application が作られています。
このように生成できるので上記であれば複数の element を list に記述すれば一つのリソースで複数の Application　リソースを管理できるようになります。

また、上記のように finalizers も自動的に設定されるので ApplicationSet リソースの変更などで Application リソースが削除されるときはその Application によってデプロイされたリソースも削除されます。
https://argocd-applicationset.readthedocs.io/en/stable/Application-Deletion/

ApplicationSet リソースを削除しても ownerReferences に指定してあるので Application リソースも削除されます。
その為不要になったら以下のように ApplicationSet リソースを削除すればデプロイされたすべてのリソースが削除されます。 

```shell
$ kubectl -n argocd delete -f guestbook.yaml
```

## Generator

上記のサンプルでは list generator を利用していましたが、 v0.1.0 時点では以下の3つの generator が用意されています。
それぞれ軽く紹介していきます。

### list generator

list generator は先程のサンプルのようにクラスタ名とそのクラスタのURLからなる固定のリストをパラメータとして生成するジェネレータです。
細かい部分は先ほど紹介したので省きますが、一点注意点としてパラメータとして定義できるのは **cluster と url のみ** となっています。
他のパラメータは定義できないので注意してください。

:pencil: 自分も他のパラメータが定義できると思って試してみましたがエラーになるようでした😅 

### cluster generator

cluster generator は ArgoCD に登録されているクラスタのリストを使って各クラスタ毎に Application リソースを生成するジェネレータです。
ArgoCDの複数クラスタのセットアップを行ったことがある人ならわかると思いますが、ArgoCD に登録されているクラスタのリストというのは実際には cluster にアクセスするための情報を保存している secret リソースになります。
具体的には https://argoproj.github.io/argo-cd/operator-manual/declarative-setup/#clusters で作成する Secret リソースです。

勘の良い人はお気づきだと思いますが、cluster generator は上記の Secret リソースを用意しなくても良いローカルクラスタ(つまり ArgoCD が動作しているクラスタ)は Secret がないので、そのままでは clsuter generator の対象になりません。

対処法としては本来は必要がないローカルクラスタように Secret を作成するという方法がドキュメントでは紹介されています。
https://argocd-applicationset.readthedocs.io/en/stable/Generators/#deploying-to-the-local-cluster

他のジェネレータと違う点として label selector が以下のように指定でき、Secret リソースに適応させたいクラスタなどにラベルを付与して、それを　label selector で指定すれば特定のクラスタのみに適応する ApplicationSet なども作ることが可能です。指定の例は以下のとおりです。

```yaml
kind: ApplicationSet
metadata:
  name: guestbook
spec:
  generators:
  - clusters:
      selector:
        matchLabels:
          staging: true
  template:
  # (...)
```

テンプレートして利用できるパラメータはドキュメントにも書いてありますが以下の4つです。

- `name`: Secret に指定してあるクラスタ名
- `server`: Secret に指定してあるクラスタURL
- `metadata.labels.<key>`: Secret に指定してあるラベル
- `metadata.annotations.<key>`: Secret に指定してあるアノテーション

ラベルやアノテーションも指定ができるのでかなり自由にパラメータを設定することが可能なジェネレータとなっています。

### git generator

git generator は git リポジトリのファイルやディレクトリを使って生成するジェネレータです。ファイルとディレクトリの両タイプに対応していて、それぞれ使い方が異なります。

#### ディレクトリ

ディレクトリのほうはリポジトリのディレクトリ構造を利用して生成します。

例えば以下のようなディレクトリ構造だったとすると、

```
├── argo-workflows
│   ├── kustomization.yaml
│   └── namespace-install.yaml
└── prometheus-operator
    ├── Chart.yaml
    ├── README.md
    ├── requirements.yaml
    └── values.yaml
```

以下のように指定することで

```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: cluster-addons
spec:
  generators:
  - git:
      repoURL: https://github.com/argoproj-labs/applicationset.git
      revision: HEAD
      directories:
      - path: examples/git-generator-directory/cluster-addons/*
  template:
    metadata:
      name: '{{path.basename}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj-labs/applicationset.git
        targetRevision: HEAD
        path: '{{path}}'
      destination:
        server: https://kubernetes.default.svc
        namespace: '{{path.basename}}'
```

argo-workflows と prometheus-operator の2つの Application が生成されるというものです。
こちらについてはあまりカスタマイズ性は高くなく、以下のパラメータのみが利用できます。

- `path`： path ワイルドカードと一致する Git リポジトリ内のディレクトリパス
- `path.basename`： path ワイルドカードと一致する Git リポジトリ内のディレクトリパスについて、右端のパス名が抽出されます（たとえば、`/directory/directory2` なら directory2）

#### ファイル

ファイルは指定されたリポジトリ内で見つかった JSON ファイルのコンテンツを使用して生成するジェネレータです。
例えば以下のようなファイル構成だったとすると

```
├── apps
│   └── guestbook
│       ├── guestbook-ui-deployment.yaml
│       ├── guestbook-ui-svc.yaml
│       └── kustomization.yaml
├── cluster-config
│   └── engineering
│       ├── dev
│       │   └── config.json
│       └── prod
│           └── config.json
└── git-generator-files.yaml
```

以下のように path を指定したとします。

```yaml
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook
spec:
  generators:
  - git:
      repoURL: https://github.com/argoproj-labs/applicationset.git
      revision: HEAD
      files:
      - path: "examples/git-generator-files-discovery/cluster-config/**/config.json"
  template:
    metadata:
      name: '{{cluster.name}}-guestbook'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj-labs/applicationset.git
        targetRevision: HEAD
        path: "examples/git-generator-files-discovery/apps/guestbook"
      destination:
        server: '{{cluster.address}}'
        namespace: guestbook
```

そうすると　`examples/git-generator-files-discovery/cluster-config/**/config.json`　に一致する　JSON ファイルの中身を見て JSON ファイルに記載されたパラメータを使って　template から Application リソースを生成します。
この　JSON ファイルでは自由にパラメータを定義できるため、かなり柔軟に Application リソースを生成することができます。

### その他

v0.1.0 では以上の3つの　generator　のみですが、次のバージョンではつぎのいくつかのジェネレータが追加されそうでいくつか　PR がマージされていました。少し紹介します。

- Matrix generator: https://github.com/argoproj-labs/applicationset/pull/205
    - 複数の generator を組み合わせることができるようでした
- Github generator: https://github.com/argoproj-labs/applicationset/pull/209
    - Github の API のレスポンスをパラメータにできるようです
    - ブランチをパラメータとして生成するなどができるようでした


# 仕組み

仕組みは Kubernetes をご存じの方なら書くまでもないですが、CRDといわゆるカスタムコントローラ(Operator)として実装されています。
詳しくは https://argocd-applicationset.readthedocs.io/en/stable/Argo-CD-Integration/ にまとまっているのでそちらを参照するとよいかと思います。

# まとめ

この記事では ArgoCD ApplicationSet controller の概要と簡単な機能を紹介しました。
自分のチームでも同じような Application リソースが大量にあって整理できていなかった部分がありましたが、ApplicationSet を導入することでかなり整理され、見やすくまた管理しやすい状態になりました。

# 参考文献

- Generating Applications with ApplicationSet: https://argoproj.github.io/argo-cd/user-guide/application-set/
- 公式リポジトリ: https://github.com/argoproj-labs/applicationset
- 公式ドキュメント: https://argocd-applicationset.readthedocs.io/en/stable/