ArgoCDでマニフェストを移行する時は、リソースを消してしまわないように気をつけて！- 実践編

# 本記事の内容

ArgoCDで管理しているKubernetesマニフェストで、管理するApplicationを別のApplicationに移行した際に、誤ってリソースを消してしまった記事を先週書きました。

https://qiita.com/showchan33/items/e4f7512ecfdcd97ee744

この記事では、「移行元Applicationのpruneを無効にすれば誤消去を回避できる」という結論でまとめました。

本記事では、具体的にどのようにすれば安全にマニフェストを移行できるのかを、実際にKubernetesのリソースを使って確かめてみます。

# TL;DR

ArgoCDのマニフェストを別のApplicationリソースへ安全に移行する手順は以下です。

* (1) 移行元のApplicationリソースで、**pruneを無効にする**
* (2) 移行先ApplicationのGitリポジトリに、移行するマニフェストを追加する
* (3) **移行先ApplicationでArgoCDのsyncを実行**し、ArgoCDのマニフェストとKubernetesクラスターのマニフェストを同期させる
    * **【重要！】同期後に、移行するマニフェストが移行先Applicationとsyncできていることを確認する**
* (4) 移行元ApplicationのGitリポジトリから、移行が完了したマニフェストを削除する

## 移行元のApplicationで、pruneを有効にしたい場合

上記手順を実施後に移行元のApplicationでpruneを有効にする場合は、「**移行元Applicationで、`OutOfSync`になっている移行対象リソースが存在しないこと**」を確認します。もし移行元Appで`OutOfSync`のリソースが残っている場合、pruneを有効にする前に `OutOfSync` を解消する必要があります。

:::note warn
移行元Appで事前に`OutOfSync`を解消しておかないと、pruneを有効にした際に移行が済んだはずのリソースが消えてしまうので注意です！
:::

## どうしても消したくなリソースには、`argocd.argoproj.io/sync-options`のアノテーションを付与する

```yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-options: Prune=false
```

参考ドキュメント

https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#no-prune-resources

# 環境情報

今回利用したArgoCDのバージョンは、v2.13.3です。

## マニフェストを格納しているディレクトリ

`app_a`と`app_b`のディレクトリがあり、それぞれにKubernetesのマニフェストが格納されています。

```
.
├── app_a
│   ├── config-a.cm.yaml
│   └── config-move.cm.yaml
└── app_b
    └── config-b.cm.yaml
```

`app_a`と`app_b`のディレクトリはGit管理されており、リモートリポジトリがoriginという名前で次のように設定されています。

```
$ cd app_a/
$ git remote -v
origin  https://path/to/repo/app_a.git (fetch)
origin  https://path/to/repo/app_a.git (push)

$ cd ../app_b/
$ git remote -v
origin  https://path/to/repo/app_b.git (fetch)
origin  https://path/to/repo/app_b.git (push)
```

## Applicationリソース

Kubernetesクラスターには、`app_a`と`app_b`のGitリモートと同期するためのApplicationリソースが、以下のようにデプロイされています。

```
$ kubectl get app -n argocd
NAME                  SYNC STATUS   HEALTH STATUS
app-a                 Synced        Healthy
app-b                 Synced        Healthy
```

各Applicationリソースのマニフェストは以下です。`app-a`と`app-b`で、リソース名と`repoURL`以外は全て同じ内容です。

```yaml:app-a.app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-a
spec:
  project: default
  source: # 解説(1)
    repoURL: "https://path/to/repo/app_a.git"
    path: "."
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc # 解説(2)
    namespace: default
  syncPolicy:
    automated:
      selfHeal: true # 解説(3)
      prune: true    # 解説(4)
```

```yaml:app-b.app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-b
spec:
  project: default
  source: # 解説(1)
    repoURL: "https://path/to/repo/app_b.git"
    path: "."
    targetRevision: main
  destination:
    server: https://kubernetes.default.svc # 解説(2)
    namespace: default
  syncPolicy:
    automated:
      selfHeal: true # 解説(3)
      prune: true    # 解説(4)

```

### 解説(1) : `spec.source`

`repoURL` には、ArgoCD がアプリケーションのマニフェストを取得する Git リポジトリの URL が書かれています。`path`には、リポジトリ内でマニフェストがあるディレクトリを書きます。今回はルートディレクトリにマニフェストがあるため、`"."`と記載します。`targetRevision`には、どのブランチやタグ、コミットを使用するかを定義します。

### 解説(2) : `spec.destination.server`

このパラメータには「Applicationリソースが管理対象としているリソースをデプロイする、KubernetesクラスターのAPIサーバーのURL」を指定します。今回のように `https://kubernetes.default.svc` を指定すると、ArgoCD がデプロイされているクラスターと同じクラスターに、管理対象リソースがデプロイされます。他のクラスターにデプロイしたい場合は、ここを `https://[デプロイしたいクラスターのAPIサーバーのURL]:[port]` のように書き換えます。

### 解説(3) : `spec.syncPolicy.automated.selfHeal`

`selfHeal`が`true`だと、KubernetesクラスターのリソースがGitの定義と異なっている場合に、ArgoCDが自動的に修正します。例えば管理対象リソースを `kubectl edit` や `kubectl patch` などで手動変更しても、ArgoCDはGitリポジトリのマニフェストと異なることを検知し、自動的に元の状態に戻します。
一方`selfHeal`が`false`の場合、GitリポジトリのマニフェストやKubernetesクラスターのマニフェストを手動で変更して差分が発生しても、自動で同期はされません。同期はArgoCDのWebUIやCLIを使って、手動で行う必要があります。

### 解説(4) : `spec.syncPolicy.automated.prune`

`prune`は、管理対象から外れたリソースを自動で削除する設定です。`prune`が`true`の場合、Gitリポジトリからマニフェストを削除すると、Kubernetesクラスターで同期していたマニフェストも削除されます。そのため、マニフェストに基づいてデプロイされたリソースも自動的に削除されます。
`prune`が`false`だと、Gitリポジトリからマニフェストを削除してArgoCDのSyncを実行しても、Kubernetesクラスターのマニフェストとリソースは削除されません。


## 管理対象リソースのデプロイ状況

`app_a`と`app_b`のApplicationで管理されているリソースは、以下のように確認できます。

```
$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-a"
NAME          DATA   AGE
config-a      2      80m
config-move   2      80m
```

```
$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-b"
NAME       DATA   AGE
config-b   2      80m
```

これらのリソースには、`argocd.argoproj.io/instance=[application名]`というラベルが付与されているのが特徴です。各Applicationリソースは、このラベルによって自分が管理するリソースを認識しています。ArgoCDはラベルが付いているリソースの状態を監視し、Gitリポジトリの定義とKubernetesクラスター内のリソースを比較して、必要に応じて更新や削除を行います。

# リソースを`app_a`から`app_b`に移行する

現在のApplicationリソースとマニフェストの構成を再掲します。

```
.
├── app_a
│   ├── config-a.cm.yaml
│   └── config-move.cm.yaml
└── app_b
    └── config-b.cm.yaml
```

この状態から、`app_a`管理の`config-move.cm.yaml`に定義されたリソースを、`app_b`で管理するように変更したいです。そのためには、`config-move.cm.yaml`のマニフェストを`app_b`に移行する必要があります。

## マニフェストを`app_a`から`app_b`コピーして、Gitリモートに反映

まずは、`config-move.cm.yaml`のマニフェストを`app_b`にコピーしてGit管理対象にします。以下のコマンドを実行します。

```
cp app_a/config-move.cm.yaml app_b/
cd app_b
git add config-move.cm.yaml
git commit -m "add config-move"
git push origin main
```

以上のコマンドを実行すると、`app_b`のApplicationの管理対象リソースに、`config-move.cm.yaml`に書かれたConfigMapのリソース`config-move`が追加されます。

```
├── app_a
│   ├── config-a.cm.yaml
│   └── config-move.cm.yaml
└── app_b
    ├── config-b.cm.yaml
    └── config-move.cm.yaml # copied
```

このことは、ArgoCDのCLIで `app_b`で管理されたリソース一覧を出力することで確認できます。
```
$ argocd app resources app-b
GROUP  KIND       NAMESPACE  NAME         ORPHANED
       ConfigMap  default    config-b     No
       ConfigMap  default    config-move  No

$ argocd app get app-b | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Synced                configmap/config-move configured
       ConfigMap  default    config-b     Synced
```

`app_b`で管理しているリソースは全て`Synced`になっており、Kubernetesクラスターのマニフェストが、`app_b`で管理しているマニフェストと同期されていることがわかります。

一方で、`config-move`は`app_a`の管理対象でもあります。

```
$ argocd app resources app-a
GROUP  KIND       NAMESPACE  NAME         ORPHANED
       ConfigMap  default    config-a     No
       ConfigMap  default    config-move  No

$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  OutOfSync                configmap/config-move configured
       ConfigMap  default    config-a     Synced
```

`app_a`から見ると`config-move`のSTATUSが`OutOfSync`です。差分を確認してみましょう。

```
$ argocd app diff app-a

===== /ConfigMap default/config-move ======
11c11
<     argocd.argoproj.io/instance: app-b
---
>     argocd.argoproj.io/instance: app-a
```

`config-move` のラベルの値が`app-a`であってほしいのに、Kubernetesクラスターのマニフェストでは`app-b`になっていることが`OutOfSync`の原因です。

これを解消するには、`app-a`に対して以下のコマンドを実行します。

```
argocd app sync app-a
```
すると、`app-a`の管理リソースは全てsyncされた状態になります。
```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Synced                configmap/config-move configured
       ConfigMap  default    config-a     Synced
```
ところが今度は、`app-b`がラベルの不一致で `OutOfSync` になってしまいます。
```
$ argocd app get app-b | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  OutOfSync                configmap/config-move configured
       ConfigMap  default    config-b     Synced

$ argocd app diff app-b

===== /ConfigMap default/config-move ======
11c11
<     argocd.argoproj.io/instance: app-a
---
>     argocd.argoproj.io/instance: app-b
```

`config-move`リソースは`app-a`と`app-b`の両方に登録されているので、一方が`Synced`の状態であれば、もう一方は`OutOfSync`になります。さらに`app-a`と`app-b`の両方のApplicationリソースで、`selfHeal: true`で自己修復機能が有効になっているため、`config-move`はある時は`app-a`と同期し、また別の時は`app-b`と同期することになります。まるで「（`app-a`）`config-move`は私のものよ！」「（`app-b`）いやあたしのものよ！」と、永遠に取り合いっこをしているような状況です。

`app-a`と`app-b`のどちらが所有しているかは、既に登場した `argocd.argoproj.io/instance`の値で決まります。例えば値が`app-a`ならば`app-a`のApplicationリソースが管理している状態です。

## `app_a`からマニフェストを削除

続いて`app_a`から`config-move`を定義しているマニフェストを削除して、Gitリモートリポジトリに反映します。

```
cd app_a
git rm config-move.cm.yaml
git commit -m "delete config-move"
git push origin main
```

約1分後に管理対象のマニフェストを確認してみると、なんと`config-move`のConfigMapが `app_a`からも`app_b`消えてしまっています...

```
$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-a"
NAME       DATA   AGE
config-a   2      9h

$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-b"
NAME       DATA   AGE
config-b   2      9h
```

`app_a`の状態を確認すると`config-move`の行に`Pruned`と書かれており、`app_a`としては定義通りにリソースを消していそうです...

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Succeeded  Pruned        pruned
       ConfigMap  default    config-a     Synced
```

しばらくすると、`app-b`で自己修復機能が働いて`config-move`を再び作成してくれます。

```
$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-b"
NAME          DATA   AGE
config-b      2      9h
config-move   2      6s
```

※ 今回のようにConfigMapの再作成なら問題ないと思いますが、対象がPVC等の永続化リソースだったら、と思うとゾッとします。

# どのようにして消えてしまったか？

リソースが消えてしまう事象は必ず起きるわけではありません。`app_a`で`config-move`のマニフェストをGitリポジトリから削除した時に、たまたま`app_a`がそれを管理していたら削除されます。先ほどの永遠の取り合いでたとえてみると、次のような状況だと削除されます。

* （`app-a`）`config-move`は私のものよ！
* （`app-b`）`config-move`はあたしのものよ！
* （`app-a`）`config-move`は私のものよ！
* （`app-b`）`config-move`はあたしのものよ！
* （`app-a`）`config-move`は私のものよ！
* （`app-a`のGitリポジトリ）`config-move`を消しました
* （`app-a`）はーい。**<font color="red">pruneが有効だから、Kubernetesクラスターの`config-move`のマニフェストも消すねー</font>**
* （`app-b`）`config-move`はあたしのものよ！けどKubernetesクラスターにマニフェストがないから新しく作るね
* （`app-b`）`config-move`はあたしのものよ！
* （`app-b`）`config-move`はあたしのものよ！
* （`app-b`）`config-move`はあたしのものよ！
<br>

一方で、Gitリポジトリが`config-move`を削除した際に、たまたま`app_b`が管理していた場合は削除されません。

* （`app-a`）`config-move`は私のものよ！
* （`app-b`）`config-move`はあたしのものよ！
* （`app-a`）`config-move`は私のものよ！
* （`app-b`）`config-move`はあたしのものよ！
* （`app-a`のGitリポジトリ）`config-move`を消しました
* （`app-a`）はーい。**<font color="#8080ff">けど、`config-move`は私のものじゃないねー</font>**
* （`app-b`）`config-move`はあたしのものよ！
* （`app-b`）`config-move`はあたしのものよ！

危機一髪、削除されずに済んだという感じですね。

# pruneをfalseにすると削除を防げる

マニフェストとリソースの予期せぬ削除は、`app-a`のApplicationリソースのマニフェストで、`prune`を`false`にすることで防げます。（デフォルトはArgoCD v2.13.3だと`false`なので、コメントアウトしても問題ないです）

```diff:app-a.app.yamlのdiff
     namespace: default
   syncPolicy:
     automated:
-      prune: true
+      prune: false
       selfHeal: true
```

記載内容がやや冗長になってしまいますが、再度移行を一から実施していきます。

## マニフェストを`app_a`から`app_b`コピーして、Gitリモートに反映

以下のコマンドを実行します。

```
cp app_a/config-move.cm.yaml app_b/
cd app_b
git add config-move.cm.yaml
git commit -m "add config-move"
git push origin main
```

`config-move`が、`app-b`でも認識されるのを確認します。

```
$ argocd app get app-b | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Synced                configmap/config-move configured
       ConfigMap  default    config-b     Synced
```

## `app_a`からマニフェストを削除

以下のコマンドを実行します。

```
cd app_a
git rm config-move.cm.yaml
git commit -m "delete config-move"
git push origin main
```

この後に`app-a`の状態を確認すると、`config-move`のリソースの行に"ignored (requires pruning)" というメッセージが表示されます。ArgoCDからはマニフェストは削除されたけど、（`prune`が`false`だから）Kubernetesクラスターのマニフェストは残っている状態です。

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  OutOfSync                ignored (requires pruning)
       ConfigMap  default    config-a     Synced                   configmap/config-a configured
```

確かに、`app-a`のラベルがついた`config-move`のリソースは残っています。

```
$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-a"
NAME          DATA   AGE
config-a      2      23h
config-move   2      14h
```

## ArgoCDのマニフェストとKubernetesクラスターのマニフェストを同期させる

しばらく待つと、`app-b`でself healが機能し、「（`app-b`）`config-move`はあたしのものよ！」の状態になります。

```
$ argocd app get app-b | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Synced                configmap/config-move configured
       ConfigMap  default    config-b     Synced                configmap/config-b configured

$ kubectl get cm -n default -l "argocd.argoproj.io/instance=app-b"
NAME          DATA   AGE
config-b      2      15h
config-move   2      14h
```

**【重要！】移行対象の`config-move`のSTATUSが`Synced`になっているのを忘れずに確認してください。**


self healが無効の場合は自動でsyncしてくれないので、以下のコマンドで`config-move`を`app-b`の管理下に移す必要があります。

```
argocd app sync app-b
```

## 移行元(`app_a`)で、`OutOfSync`のリソースが残っていたら要解消（25/02/26追記）

`app-b`で`Synced`が確認できただけでは安心できません。移行元の`app_a`が、移行したはずの`config-move`を中途半端に認識している可能性があるためです。具体的に、`app_a`で`config-move`が以下のように`OutOfSync`のSTATUSになっていたら解消する必要があります。

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  OutOfSync                （何らかのメッセージ）
       ConfigMap  default    config-a     Synced                   configmap/config-a configured
```

`app-a`の`OutOfSync`は、「`app-b`に移行が済んだはずの`config-move`を`app-a`が自らの管理下に置こうとしている」状態です。この状態で`app-a`のpruneを有効にすると、`config-move`が`app-a`の管理対象となり、即座に削除されてしまいます。そうならないように、`app-a`の`OutOfSync`は解消しておきたいです。

私自身がこのケースに遭遇した時は、一回移行元の`app_a`とsyncさせた後、移行先の`app_b`とsyncすることで解決しました。実行コマンドは以下です。

```
argocd app sync app-a
argocd app sync app-b
```

この後に`app_a`の状態を確認すると、次のように`OutOfSync`が`Succeeded`に変わり、`app_a`によって`config-move`が削除されることを回避できました。

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH        HOOK  MESSAGE
       ConfigMap  default    config-move  Succeeded  PruneSkipped        ignored (requires pruning)
       ConfigMap  default    config-a     Synced                         configmap/config-a configured
```

## 以上を確認できたら、pruneに戻してもOK

`config-move`が完全に`app-b`の管理下になったのを確認できたら、`app-a`のApplicationリソースで`prune`を`true`に戻して大丈夫です。

```diff:app-a.app.yamlのdiff
     namespace: default
   syncPolicy:
     automated:
-      prune: false
+      prune: true
       selfHeal: true
```

この後`app-a`のsyncが完了すると、`config-move`は`app-a`から完全に忘れ去られ、`app-b`のみが管理する状態になります。
```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME      STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-a  Synced                configmap/config-a configured

$ argocd app get app-b | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS  HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  Synced                configmap/config-move configured
       ConfigMap  default    config-b     Synced                configmap/config-b configured
```

このようなやり方でマニフェストを移行すると、`config-move`のリソースは確実に残ります。

# 消してほしくないリソースには、アノテーションを追加する（2/27追記）

上記の手順で実施しても、何らかの手違いでリソースを消してしまう可能性は否めなせん。PVCやnamespace等の「どうしても消えると困る」リソースの場合は、該当マニフェストに次のようなアノテーションを付与することで、ArgoCDによるリソース削除を回避できます。（[参考ドキュメント](https://argo-cd.readthedocs.io/en/stable/user-guide/sync-options/#no-prune-resources)）

```yaml
metadata:
  annotations:
    argocd.argoproj.io/sync-options: Prune=false
```

例えばこれまで使ってきた`config-move`の`ConfigMap`リソースだと、次のようにアノテーションを追加します。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: config-move
  namespace: default
  annotations:
    argocd.argoproj.io/sync-options: Prune=false
data:
  key1: value1
  key2: value2
```

このアノテーションを書いておくと、Gitリポジトリから`config-move`のマニフェストを削除した際に、管理しているApplicationのpruneが有効でもリソースが消えることはありません。

`config-move`のマニフェストをGitリポジトリから消した場合、次のように "ignored (no prune)"と出てきて削除していないのがわかります。

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH  HOOK  MESSAGE
       ConfigMap  default    config-move  OutOfSync                ignored (no prune)
       ConfigMap  default    config-a     Synced                   configmap/config-a configured
```

この後、移行先の`app_b`で同期して`config-move`を`app_b`管理下にすると、`app_a`における`config-move`のSTATUSが Succeeded になり、今後`app_a`によって削除される可能性はなくなります。

```
$ argocd app get app-a | grep -A 4 ^GROUP
GROUP  KIND       NAMESPACE  NAME         STATUS     HEALTH        HOOK  MESSAGE
       ConfigMap  default    config-move  Succeeded  PruneSkipped        ignored (no prune)
       ConfigMap  default    config-a     Synced                         configmap/config-a configured
```