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 namerevision
: commit hashnamespace
: manifest を適用する先の namespaceappSourceKey
: 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 を付けています。なので、アップグレードの際にキャッシュの無効化を気にする必要はありません。