Argo CD はどのように manifest をキャッシュしているのか?

Posted on

Argo CD はキャッシュのために Redis を使っています。 High Availability のページには次のような説明があります。

Redis is only used as a throw-away cache and can be lost. When lost, it will be rebuilt without loss of service.

同じページを読み進めていくと、argocd-repo-server のセクションに manifest をキャッシュしているという記述があります。

Argo CD assumes by default that manifests only change when the repo changes, so it caches generated manifests (for 24h by default).

Argo CD がどのように manifest をキャッシュしているのか気になったので、ソースコードを追いかけながら調べてみました。

検証環境

  • Docker Desktop 3.2.2
  • Kubernetes 1.19.7
  • Argo CD 2.0.0+f5119c0

検証のために Getting Started の guestbook application を sync 済みです。

Redis の中身を覗いてみる

argocd-redis の Pod に入って、中身を覗いてみます。

$ k get po --selector="app.kubernetes.io/name=argocd-redis"
NAME                            READY   STATUS    RESTARTS   AGE
argocd-redis-759b6bc7f4-hwqdv   1/1     Running   0          10m

$ k exec -it argocd-redis-759b6bc7f4-hwqdv -- redis-cli --raw

127.0.0.1:6379> keys *
cluster|info|https://kubernetes.default.svc|1.8.3
appdetails|53e28ff20cc530b9ada2173fbbd64d48338583ba|119999350|1.8.3
mfst||guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3
app|resources-tree|guestbook|1.8.3
app|managed-resources|guestbook|1.8.3
mfst|app.kubernetes.io/instance|guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3

53e28ff... から始まる文字列は argocd-example-apps リポジトリの HEAD (2021/04/12) の commit hash と一致します。

キャッシュされた manifest を確認する

mfst から始まる key が manifest っぽいので、中身を見てみます。

127.0.0.1:6379> keys "mfst|*"
mfst||guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3
mfst|app.kubernetes.io/instance|guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3

127.0.0.1:6379> get "mfst|app.kubernetes.io/instance|guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3"
{"cacheEntryHash":"SeSGwsI-qOE=","manifestResponse":{"manifests":["{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"guestbook\"},\"name\":\"guestbook-ui\"},\"spec\":{\"replicas\":1,\"revisionHistoryLimit\":3,\"selector\":{\"matchLabels\":{\"app\":\"guestbook-ui\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"guestbook-ui\"}},\"spec\":{\"containers\":[{\"image\":\"gcr.io/heptio-images/ks-guestbook-demo:0.2\",\"name\":\"guestbook-ui\",\"ports\":[{\"containerPort\":80}]}]}}}}","{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"guestbook\"},\"name\":\"guestbook-ui\"},\"spec\":{\"ports\":[{\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"guestbook-ui\"}}}"],"revision":"53e28ff20cc530b9ada2173fbbd64d48338583ba","sourceType":"Directory"},"mostRecentError":"","firstFailureTimestamp":0,"numberOfConsecutiveFailures":0,"numberOfCachedResponsesReturned":0}

JSON を見やすくするとこんな感じ。 manifestResponse.manifests に JSON 形式の manifest があります。

{
  "cacheEntryHash": "SeSGwsI-qOE=",
  "manifestResponse": {
    "manifests": [
      "{\"apiVersion\":\"apps/v1\",\"kind\":\"Deployment\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"guestbook\"},\"name\":\"guestbook-ui\"},\"spec\":{\"replicas\":1,\"revisionHistoryLimit\":3,\"selector\":{\"matchLabels\":{\"app\":\"guestbook-ui\"}},\"template\":{\"metadata\":{\"labels\":{\"app\":\"guestbook-ui\"}},\"spec\":{\"containers\":[{\"image\":\"gcr.io/heptio-images/ks-guestbook-demo:0.2\",\"name\":\"guestbook-ui\",\"ports\":[{\"containerPort\":80}]}]}}}}",
      "{\"apiVersion\":\"v1\",\"kind\":\"Service\",\"metadata\":{\"labels\":{\"app.kubernetes.io/instance\":\"guestbook\"},\"name\":\"guestbook-ui\"},\"spec\":{\"ports\":[{\"port\":80,\"targetPort\":80}],\"selector\":{\"app\":\"guestbook-ui\"}}}"
    ],
    "revision": "53e28ff20cc530b9ada2173fbbd64d48338583ba",
    "sourceType": "Directory"
  },
  "mostRecentError": "",
  "firstFailureTimestamp": 0,
  "numberOfConsecutiveFailures": 0,
  "numberOfCachedResponsesReturned": 0
}

JSON から YAML に変換すると見慣れた manifest になります。

$ cat mfst.json | jq -r .manifestResponse.manifests[] | ruby -r yaml -r json -ne 'puts YAML.dump(JSON.parse($_))'
---
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    app.kubernetes.io/instance: guestbook
  name: guestbook-ui
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: guestbook-ui
  template:
    metadata:
      labels:
        app: guestbook-ui
    spec:
      containers:
      - image: gcr.io/heptio-images/ks-guestbook-demo:0.2
        name: guestbook-ui
        ports:
        - containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app.kubernetes.io/instance: guestbook
  name: guestbook-ui
spec:
  ports:
  - port: 80
    targetPort: 80
  selector:
    app: guestbook-ui

この manifest をそのまま apply しても、当然 unchanged になります。

$ cat mfst.json | jq -r .manifestResponse.manifests[] | k apply -f -
deployment.apps/guestbook-ui unchanged
service/guestbook-ui unchanged

適当な commit を重ねると、argocd-repo-server がリポジトリをチェックしたタイミングで新しいキャッシュが作られます。 sync するよりも前にキャッシュが作られるのは意外でした。

127.0.0.1:6379> keys "mfst|*"
mfst||guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3
mfst|app.kubernetes.io/instance|guestbook|53e28ff20cc530b9ada2173fbbd64d48338583ba|default|119999350|1.8.3
mfst|app.kubernetes.io/instance|guestbook|6300c57c2e4d59dce8af43141a807568b58eb5f8|default|119999350|1.8.3

ソースコードを読んでみる

key の命名規則が気になったので、Argo CD のソースコードを読んでみます。

func manifestCacheKey(revision string, appSrc *appv1.ApplicationSource, namespace string, appLabelKey string, appName string) string {
    return fmt.Sprintf("mfst|%s|%s|%s|%s|%d", appLabelKey, appName, revision, namespace, appSourceKey(appSrc))
}

先ほどの key を順番に見ていくと、次のような意味になります。

  • mfst: manifest を表す文字列
  • appLabelKey: Argo CD がトラッキングするために使っている application.instanceLabelKey の値
  • appName: Argo CD 上の application name
  • revision: commit hash
  • namespace: manifest を適用する先の namespace
  • appSourceKey: application souce を表す FNV-1 hash
    • application の設定を変えなければ同じ hash 値になる

最後の 1.8.3 という文字列は CacheVersion で、set するときに付与しています。

func (c *Cache) SetItem(key string, item interface{}, expiration time.Duration, delete bool) error {
    key = fmt.Sprintf("%s|%s", key, common.CacheVersion)
    // CacheVersion is a objects version cached using util/cache/cache.go.
    // Number should be bumped in case of backward incompatible change to make sure cache is invalidated after upgrade.
    CacheVersion = "1.8.3"

後方互換性のない変更が入った場合に、キャッシュが使われないように version を付けています。なので、アップグレードの際にキャッシュの無効化を気にする必要はありません。