ArgoCD のイメージ更新をシンプルに！ArgoCD Image Updater の導入方法

# はじめに
ArgoCD Image Updater の概要と導入方法について説明します。
絶賛開発中につき**リリース毎に重大な変更が入る**可能性があります。
最新情報は下記をご確認ください。

### リファレンス

- 公式 Doc
  - https://argocd-image-updater.readthedocs.io/en/stable/
- GitHub
  - https://github.com/argoproj-labs/argocd-image-updater

# 概要
### ArgoCD Image Updater とは
ArgoCD の Application として管理されているワークロードのイメージを更新してくれるツールです。
イメージを管理しているレジストリ (Docker Hub, GCR, etc.) を定期的にチェックし自動更新します。
そのため CI/CD ワークフローを書いてイメージタグを更新する必要がなくなります。

### 更新フロー

1. コミットをトリガーに CI/CD が動く
1. コンテナレジストリにプッシュ
1. ArgoCD Image Updater が定期的にウォッチ
1. 更新があればイメージタグを書き換えてプッシュ
1. ArgoCD の Auto Sync により更新を取り込む
1. 更新後のイメージタグを持つマニフェストで apply

![argocd-image-updater.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/35773327-92df-f261-78e5-8b80511ab27e.png)

### 更新方法
イメージ更新には、2つの方法があります。

**1. ArgoCD API 経由 (デフォルト)**
ArgoCD API で直接マニフェストを上書きする方法です。
下記コマンドと同等の効果を持ちます。

```
argocd app set --p guestbook=image=example/guestbook:latest
```

**2. Git リポジトリ経由**
Git リポジトリに変更を Push する方法です。
kustomization.yml の `image` フィールドを書き換え、対象ブランチに Push する。
※ 上のフロー図は Git リポジトリ経由で更新した場合を表しています。

```kustomization.yml
resources:
  - ../../base
patchesStrategicMerge:
  - ...
images:
  - name: example/guestbook
    newTag: new # ここが変わる
```

### タグ条件
更新するイメージタグに様々な条件を付けることができます。
例えば、正規表現で特定のタグが付与されたときだけ更新する、など。(後述)


# 導入方法
今回はコンテナレジストリとして `GCR` を利用し、`Git リポジトリ経由`でイメージを更新します。

### 環境

- GKE (v1.21.5-gke.1802)
- ArgoCD (v2.1.7)
- Terraform (v1.0.8)
- google provider (v4.3.0)

### 大まかな手順

1. ArgoCD Image Updater を apply
1. クレデンシャルを登録
1. Application に annotation を追加

手順はこちらの記事を参考にさせていただきました。公式 Doc だけだと分かりにくいので非常に助かりました。

https://zenn.dev/nekoshita/articles/02c1e59a487fb4

### 手順1 | ArgoCD Image Updater を apply
ArgoCD Image Updater も ArgoCD の Application として適用します。
`namespace` は ArgoCD が稼働している場所と一致させるのが推奨です。

```argocd-image-updater.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argocd-image-updater
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-image-updater
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: 'https://argoproj.github.io/argo-helm'
    targetRevision: 0.6.0
    helm:
      values: |
        config:
          logLevel: "info"
    chart: argocd-image-updater
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
```

適用完了すると、下記スクショのようなリソースが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619762/d6a50497-141d-0282-fc7b-92a5be8e2117.png)

pod のログを確認すると、2分毎に次のような表示が見て取れます。

```
time="2021-12-27T04:38:06Z" level=info msg="Processing results: applications=0 images_considered=0 images_skipped=0 images_updated=0 errors=0"
```

ここには、更新対象の Application やイメージの数が表示されています。
公式 Doc には記載されていませんが、次のような意味だと認識しています。

- applications: 更新対象となるイメージを所有している Application 数
- images_considered: 更新対象となるイメージ数
- images_skipped: 更新が検知されたがスキップされたイメージ数
- images_updated: 更新が検知され正常に処理されたイメージ数

上述の例では、更新対象の Application をまだ設定していないため、全て `0` になっています。

### 手順2 | クレデンシャルを登録
ArgoCD Image Updater が GCR の更新状況をフェッチするためのクレデンシャルを設定します。
なお、執筆時点で Workload Identity 非対応のためサービスアカウントを直接参照する必要があります。

まず、サービスアカウントキーを発行するためのリソースを追加します。

```main.tf
# argocd-image-updaterがGCRへイメージ参照するためのService Account
resource "google_service_account" "argocd-image-updater" {
  account_id   = "argocd-image-updater-${var.env}"
  display_name = "argocd-image-updater-${var.env}"
}

# GCR を参照するためのRoleを付与
resource "google_project_iam_member" "argocd-image-updater" {
  project = var.project_id
  role    = "roles/storage.objectViewer"
  member  = "serviceAccount:${google_service_account.argocd-image-updater.email}"
}

# サービスアカウントキーを発行
resource "google_service_account_key" "argocd-image-updater" {
  service_account_id = google_service_account.argocd-image-updater.name
}
```

続いて、シークレットマネージャーにクレデンシャルを登録します。
本稿では、[External Secrets](https://external-secrets.io/) を利用して値を取得しています。

```main.tf
resource "google_secret_manager_secret" "argocd_image_updater_registry_credentials" {

  secret_id = "argocd_image_updater_registry_credentials"
  replication {
    automatic = true
  }
}

resource "google_secret_manager_secret_version" "argocd_image_updater_registry_credentials" {

  secret = google_secret_manager_secret.argocd_image_updater_registry_credentials.name
  secret_data = jsonencode({
    auths = {
      "https://asia.gcr.io" = {
        username = "_json_key"
        password = base64decode(google_service_account_key.argocd-image-updater.private_key)
        email    = google_service_account.argocd-image-updater.email
        auth     = base64encode("_json_key:${base64decode(google_service_account_key.argocd-image-updater.private_key)}")
      }
    }
  })
}
```

手順 1 の argocd-image-updater.yml をこちらで上書きして適用します。

```argocd-image-updater.yml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argocd-image-updater
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-image-updater
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  source:
    repoURL: 'https://argoproj.github.io/argo-helm'
    targetRevision: 0.6.0
    helm:
      values: |
        config:
          logLevel: "info"
          registries:
          - name: Google Container Registry Asia
            api_url: https://asia.gcr.io
            prefix: asia.gcr.io
            ping: no
            credentials: pullsecret:argocd/argocd-image-updater-secrets
    chart: argocd-image-updater
  destination:
    server: 'https://kubernetes.default.svc'
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
---
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
  name: argocd-image-updater
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-image-updater
spec:
  provider:
    gcpsm:
      projectID: example-gcp
---
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
  name: argocd-image-updater-secrets
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-image-updater
spec:
  refreshInterval: 1m
  secretStoreRef:
    name: argocd-image-updater
    kind: SecretStore
  target:
    name: argocd-image-updater-secrets
    creationPolicy: Owner
  data:
  - secretKey: .dockerconfigjson
    remoteRef:
      key: argocd_image_updater_registry_credentials
```

helm のパラメータに `registries` を追加をしています。
この追加だけで、クレデンシャルが ComfigMap にダンプされ、Pod にマウントされます。([詳細](https://artifacthub.io/packages/helm/argo/argocd-image-updater))

以上で、ArgoCD Image Updater が GCR を参照できるようになりました。

### 手順3 | Application に annotation を追加

最後に、更新対象とするイメージを ArgoCD Image Updater に指示します。
Application のマニフェストに annotation を付加することで設定できます。

ここでは、Kustomize で管理される Application の設定例を示します。
(その他に Helm で管理しているケースもありますがイメージ更新のニーズは少ないと思います)

```example-api-apps.yml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: example-api
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
  annotations:
    argocd-image-updater.argoproj.io/image-list: external-api=asia.gcr.io/example-dev/example-dev #1
    argocd-image-updater.argoproj.io/external-api.update-strategy: latest #2
    argocd-image-updater.argoproj.io/external-api.ignore-tags: latest #3
    argocd-image-updater.argoproj.io/external-api.allow-tags: regexp:^[0-9a-f]{5,40}$ #4
    argocd-image-updater.argoproj.io/write-back-method: git:repocreds #5
    argocd-image-updater.argoproj.io/git-branch: dev #6
    argocd-image-updater.argoproj.io/write-back-target: kustomization:../../../example-api/overlays/dev #7
spec:
  destination:
    namespace: default
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: git@github.com:example/example-infra.git
    path: kubernetes/example-api/overlays/dev
    targetRevision: main
  syncPolicy:
    automated:
      prune: true
```

計7つの annotation を追加しました。それぞれ説明します。

`#1 image-list`
更新対象とするイメージのリストです。
alias を付けられます。イメージ毎の設定をするのに必要ですので付けておきましょう。
複数ある場合は `<alias1>=<image1>,<alias2>=<image2>,...` のようにカンマ区切りで追加します。

`#2 <alias>.update-strategy`
イメージ毎のアップデート戦略を指定します。
いくつか選択肢がありますが、ここでは `latest` にしています。
コンテナレジストリ内で直近に更新されたイメージへ更新してくれます。

`#3 <alias>.ignore-tags`
特定のタグを持つときは更新しないようにします。
新規イメージに「latest」と「コミットハッシュ値」の2つのタグを付与している場合において、
適用中のイメージがどのコミットに紐づくものか把握するためには、コミットハッシュ値の方で更新したいです。
上記でアップデート戦略を `latest` にしていたため、latest タグを ignore することで、
必ずコミットハッシュ値のタグでイメージが更新されるようになります。

`#4 <alias>.allow-tags`
更新対象とするイメージタグを正規表現で制限できます。
ここでは、コミットハッシュ値の正規表現を設定しています。
これにより、「test」などのタグを持つイメージが Push されても除外することができます。

`#5 write-back-method`
イメージタグの更新方法です。
冒頭の説明にもあった通り、`argocd` or `git` を選択します。
ここでは、`git` を選択しています。

`#6 git-branch`
イメージタグ更新を Push するブランチを選択します。

`#7 write-back-target`
更新対象のマニフェストが格納されているディレクトリパスを示します。
ここでは、example-api-apps.yml から更新対象の kustomization.yml が置いてあるディレクトリへの相対パスを示しています。

以上で全ての準備が整いました。

ArgoCD Image Updater の Pod のログを確認すると、applications と images_considered が共に 1 になっています。

```
time="2021-12-28T10:25:47Z" level=info msg="Processing results: applications=1 images_considered=1 images_skipped=0 images_updated=0 errors=0"
```

新しいイメージがコンテナレジストリに Push されると、次のような更新成功のログが確認できます。

```
time="2021-12-28T10:33:56Z" level=info msg="Successfully updated the live application spec" application=example-api
time="2021-12-28T10:33:56Z" level=info msg="Processing results: applications=1 images_considered=1 images_skipped=0 images_updated=1 errors=0"
```

これにより、Git リポジトリに更新されたタグが Push されます。
そして、ArgoCD の Sync によって実際に適用されます。

# まとめ
ArgoCD の Application の概要と導入方法を説明しました。
CI/CD ワークフローを頑張らなくても、イメージ更新できるのは魅力だと感じました。
一方で、ロールバック方法には課題が残ります。Git リポジトリへのタグ更新は Push のみなので、PR の revert によるロールバックは実現できません。[こちらの記事](https://zenn.dev/nekoshita/articles/02c1e59a487fb4)で触れられているような代替手段を検討する必要がありそうです。