Featured image of post Deploy ArgoCD and Image Updater with Helm

Deploy ArgoCD and Image Updater with Helm

My ArgoCD and ArgoCD Image Updater have been working great in my cluster for a while now.

I don’t have any problem with them in my daily usage, but maintaining them is another story. I initially set them up using kubectl apply -f ... commands and later converted them to ArgoCD applications. While it works, I realized this was a problem when I thought of upgrading them to newer versions.

It wouldn’t be a huge pain for normal applications, but ArgoCD manifests contain a LOT of things. If something changes in a newer version, I would have to manually compare and edit my manifests to match the new version. Not something I want to spend my weekend time on.

note

This is what happened when I switched to helm instead.

1
2
3
4
kube/argocd-apps/cicd/argocd-image-updater.yml          |    34 +
kube/argocd-apps/cicd/argocd.yml                        |   330 ++
kube/argocd/argocd.yaml                                 | 26668 ------------------------------------------------------------------------------------------------------------------------------------
kube/argocd/image-upadter.yaml                          |   295 --

As for helm, there are also changes across versions in in the values.yaml files, but it would be much easier to track and update them compared to raw manifests.

Environment

  1. Kubernetes: v1.33.4+k3s1
  2. ArgoCD
    1. Old: v3.0.6 installed with kubectl apply -f ...
    2. New: Helm chart 9.1.4 to be installed with Helm
  3. ArgoCD Image Updater
    1. Old: v0.20.3 installed with kubectl apply -f ...
    2. New: Helm chart 1.0.1 to be installed with Helm

ArgoCD

For ArgoCD, the best way I could come up with was to burn down everything and start over. Here is what I did:

  1. Make sure all ArgoCD applications are synced and healthy, and synced with git repo.
  2. Delete the entire ArgoCD namespace.
    1. Manually resolve anything that blocks the deletion, such as finalizers on CRDs.
  3. Compare my ArgoCD manifests with the latest version from official docs, and create a values.yaml file for Helm installation.
  4. Install ArgoCD with Helm using the created values.yaml file.
  5. Create an ArgoCD Application manifest for ArgoCD itself.

Helm Installation

First I created the secret for ArgoCD to access my homelab git repo:

secret.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
apiVersion: v1
kind: Secret
metadata:
  name: git-junyi-me-token
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://git.junyi.me/home/homelab.git
  username: gitlab-token
  password: [GITLAB_PERSONAL_ACCESS_TOKEN]

Then created values.yml:

values.yml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
global:
  domain: argocd.i.junyi.me

configs:
  cm:
    dex.config: |
      connectors:
      - config:
          issuer: https://auth.junyi.me/application/o/argo-cd/
          clientID: [AUTHENTIK_OIDC_CLIENT_ID]
          clientSecret: [AUTHENTIK_OIDC_CLIENT_SECRET]
          insecureEnableGroups: true
          scopes:
            - openid
            - profile
            - email
        name: authentik
        type: oidc
        id: authentik      
    accounts.homepage: apiKey, login

  rbac:
    policy.csv: |
      g, admin, role:admin
      g, readonly, role:readonly
      p, homepage, applications, get, *, allow      

  params:
    server.insecure: "true"

server:
  ingress:
    enabled: true
    ingressClassName: traefik
    annotations:
      traefik.ingress.kubernetes.io/router.entrypoints: internal, internalsecure
    extraTls:
      - hosts:
        - argocd.i.junyi.me
        secretName: junyi-me-production

notifications:
  argocdUrl: https://argocd.i.junyi.me
  secret:
    items:
      slack-token: [SLACK_BOT_TOKEN]
  cm:
    create: true
  notifiers:
    service.slack: |
      token: $slack-token      

  templates:
    template.app-deployed: |
      email:
        subject: New version of an application {{.app.metadata.name}} is up and running.
      message: |
        {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} is now running new version of deployments manifests.
      slack:
        attachments: |
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#18be52",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            },
            {
              "title": "Revision",
              "value": "{{.app.status.sync.revision}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      
    template.app-health-degraded: |
      email:
        subject: Application {{.app.metadata.name}} has degraded.
      message: |
        {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} has degraded.
        Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
      slack:
        attachments: |-
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link": "{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#f4c030",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      
    template.app-sync-failed: |
      email:
        subject: Failed to sync application {{.app.metadata.name}}.
      message: |
        {{if eq .serviceType "slack"}}:exclamation:{{end}}  The sync operation of application {{.app.metadata.name}} has failed at {{.app.status.operationState.finishedAt}} with the following error: {{.app.status.operationState.message}}
        Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
      slack:
        attachments: |-
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#E96D76",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      
    template.app-sync-running: |
      email:
        subject: Start syncing application {{.app.metadata.name}}.
      message: |
        The sync operation of application {{.app.metadata.name}} has started at {{.app.status.operationState.startedAt}}.
        Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
      slack:
        attachments: |-
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#0DADEA",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      
    template.app-sync-status-unknown: |
      email:
        subject: Application {{.app.metadata.name}} sync status is 'Unknown'
      message: |
        {{if eq .serviceType "slack"}}:exclamation:{{end}} Application {{.app.metadata.name}} sync is 'Unknown'.
        Application details: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}.
        {{if ne .serviceType "slack"}}
        {{range $c := .app.status.conditions}}
            * {{$c.message}}
        {{end}}
        {{end}}
      slack:
        attachments: |-
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#E96D76",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      
    template.app-sync-succeeded: |
      email:
        subject: Application {{.app.metadata.name}} has been successfully synced.
      message: |
        {{if eq .serviceType "slack"}}:white_check_mark:{{end}} Application {{.app.metadata.name}} has been successfully synced at {{.app.status.operationState.finishedAt}}.
        Sync operation details are available at: {{.context.argocdUrl}}/applications/{{.app.metadata.name}}?operation=true .
      slack:
        attachments: |-
          [{
            "title": "{{ .app.metadata.name}}",
            "title_link":"{{.context.argocdUrl}}/applications/{{.app.metadata.name}}",
            "color": "#18be52",
            "fields": [
            {
              "title": "Sync Status",
              "value": "{{.app.status.sync.status}}",
              "short": true
            },
            {
              "title": "Repository",
              "value": "{{.app.spec.source.repoURL}}",
              "short": true
            }
            {{range $index, $c := .app.status.conditions}}
            {{if not $index}},{{end}}
            {{if $index}},{{end}}
            {
              "title": "{{$c.type}}",
              "value": "{{$c.message}}",
              "short": true
            }
            {{end}}
            ]
          }]      

  triggers:
    trigger.on-deployed: |
       - description: Application is synced and healthy. Triggered once per commit.
         oncePer: app.status.sync.revision
         send:
         - app-deployed
         when: app.status.operationState.phase in ['Succeeded'] and app.status.health.status == 'Healthy'       
    trigger.on-health-degraded: |
      - description: Application has degraded
        send:
        - app-health-degraded
        when: app.status.health.status == 'Degraded'      
    trigger.on-sync-failed: |
      - description: Application syncing has failed
        send:
        - app-sync-failed
        when: app.status.operationState.phase in ['Error', 'Failed']      
    trigger.on-sync-running: |
      - description: Application is being synced
        send:
        - app-sync-running
        when: app.status.operationState.phase in ['Running']      
    trigger.on-sync-status-unknown: |
      - description: Application status is 'Unknown'
        send:
        - app-sync-status-unknown
        when: app.status.sync.status == 'Unknown'      
    trigger.on-sync-succeeded: |
      - description: Application syncing has succeeded
        send:
        - app-sync-succeeded
        when: app.status.operationState.phase in ['Succeeded']      

    defaultTriggers: |
      - on-sync-failed
      - on-health-degraded
      - on-deployed
      - on-sync-status-unknown      

And applied it with:

1
2
3
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm upgrade --install argocd argo/argo-cd --version 9.1.4 --values argocd-values.yml -n argocd

Since I had deleted the entire ArgoCD namespace all the ArgoCD applications were gone. This was trivial to fix, though, as all my applications were already in git repo and I just needed to apply the ArgoCD app of apps.

argocd-apps.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  annotations:
    notifications.argoproj.io/subscribe.slack: production
  name: argocd-apps
  namespace: argocd
spec:
  destination:
    namespace: argocd
    server: https://kubernetes.default.svc
  project: default
  source:
    path: kube/argocd-apps # contains individual ArgoCD applications
    repoURL: https://git.junyi.me/home/homelab.git
    targetRevision: master
    directory:
      recurse: true

After that, I just watched my application tiles grow and turn green.

Beautifully green

ArgoCD ArgoCD application

After installing ArgoCD with Helm, I created an ArgoCD Application for ArgoCD to manage itself.

argocd-app.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  annotations:
    notifications.argoproj.io/subscribe.slack: production
  name: argocd
  namespace: argocd
spec:
  destination:
    namespace: argocd
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://argoproj.github.io/argo-helm
    chart: argo-cd
    targetRevision: 9.1.4
    helm:
      valuesObject:

      # ...
      # values from above `values.yml`

  syncPolicy:
    automated:
      prune: true
      selfHeal: true

ArgoCD Image Updater

Helm Installation

I followed the same steps as ArgoCD for ArgoCD Image Updater, but this one was much easier with less config options:

image-updater.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: argocd-image-updater
  namespace: argocd
  annotations:
    notifications.argoproj.io/subscribe.slack: production
spec:
  destination:
    namespace: argocd
    server: https://kubernetes.default.svc
  project: default
  source:
    repoURL: https://argoproj.github.io/argo-helm
    chart: argocd-image-updater
    targetRevision: 1.0.1
    helm:
      valuesObject:
        config:
          registries:
          - name: Docker Hub
            prefix: docker.io
            api_url: https://registry-1.docker.io
            credentials: pullsecret:argocd/dockerhub
            default: true
          - name: GitLab jy
            prefix: regist.junyi.me
            api_url: https://regist.junyi.me
            credentials: pullsecret:argocd/junyime
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

The pull secrets are already in place, replicated to every namespace by kubernetes-reflector.

Usage update

Since ArgoCD Image Updater v1.0.0, the configuration method has changed from annotations to CRDs. For example, configuration for my blog application changed from:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: blog
  namespace: argocd
  annotations:
    argocd-image-updater.argoproj.io/blog.update-strategy: digest
    argocd-image-updater.argoproj.io/image-list: blog=regist.junyi.me/explosion/jy-blog:prd
    argocd-image-updater.argoproj.io/write-back-method: git
    argocd-image-updater.argoproj.io/write-back-target: kustomization
# ...

to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
  name: my-kustomize
  namespace: argocd
spec:
  writeBackConfig:
    method: "git"
    gitConfig:
      repository: "https://git.junyi.me/home/homelab.git"
      branch: "master"
      writeBackTarget: "kustomization"
  namespace: argocd
  commonUpdateSettings:
    updateStrategy: digest
  applicationRefs:
    - namePattern: "blog"
      images:
        - alias: "blog"
          imageName: "regist.junyi.me/explosion/jy-blog:prd"

and additional applications can be added under applicationRefs section.

IMO this is a cleaner way to manage image updater configs, especially when there are many applications sharing the same method of updating images.

my-kustomize.yml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
apiVersion: argocd-image-updater.argoproj.io/v1alpha1
kind: ImageUpdater
metadata:
  name: my-kustomize
  namespace: argocd
spec:
  writeBackConfig:
    method: "git"
    gitConfig:
      repository: "https://git.junyi.me/home/homelab.git"
      branch: "master"
      writeBackTarget: "kustomization"
  namespace: argocd
  commonUpdateSettings:
    updateStrategy: digest
  applicationRefs:
    - namePattern: "vault-prd-*"
      images:
        - alias: "vfront"
          imageName: "regist.junyi.me/vault/vfront:prd"
        - alias: "pandora"
          imageName: "regist.junyi.me/vault/pandora:prd"
        - alias: "spawner"
          imageName: "regist.junyi.me/vault/spawner:prd"

    - namePattern: "mailu-custom"
      images:
        - alias: "mailgun2smtp"
          imageName: "regist.junyi.me/home/mailgun2smtp:prd"

    - namePattern: "branding"
      images:
        - alias: "branding"
          imageName: "regist.junyi.me/explosion/branding:prd"
    - namePattern: "branding-stg"
      images:
        - alias: "branding"
          imageName: "regist.junyi.me/explosion/branding:stg"

    - namePattern: "blog"
      images:
        - alias: "blog"
          imageName: "regist.junyi.me/explosion/jy-blog:prd"
    - namePattern: "blog-stg"
      images:
        - alias: "blog"
          imageName: "regist.junyi.me/explosion/jy-blog:stg"

    - namePattern: "review"
      images:
        - alias: "review"
          imageName: "regist.junyi.me/explosion/review-planner:prd"
    - namePattern: "review-stg"
      images:
        - alias: "review"
          imageName: "regist.junyi.me/explosion/review-planner:stg"

To test it, I just triggered a new pipeline in my branding repo, and watched the new stg image get deployed to my branding-stg application.

Conclusion

I am convinced once more that Helm is the way to go for managing complex applications like ArgoCD. It abstracts away a lot of the complex manifests that I would probably never touch anyway, and hopefully makes upgrading easier in the future.

ArgoCD’s new dark theme is pretty cool.

Built with Hugo
Theme Stack designed by Jimmy