---
title: "ArgoCD vs Helmfile: Applications"
date: 2023-02-13T12:14:09+01:00
draft: false
cover:
    image: "cover.png"
    caption: "ArgoCD"
    relative: false
    responsiveImages: false
ShowToc: true
---

> So as promised in [the previous ArgoCD post]({{< ref "dont-use-argocd-for-infrastructure" >}}), I'll try to show a simple example of Pull Requests for different kinds of setups. This is the first part. Putting everything in the same post seems kind of too much.

# Intro 
I've created three `main` branches and three branches for install two applications. I assume we have two production clusters (If you've read the previous post, you know that by saying 'production', I mean production for SRE team, so they can be dev/stage/whatever for other teams) and one test cluster (the one where SRE team can test anything without affecting other teams)

You can already check all of them here: <https://git.badhouseplants.net/allanger/helmfile-vs-argo/pulls>

I've decided to install [Vertical pod autoscaler](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) to both prod clusters and [goldilocks](https://github.com/FairwindsOps/goldilocks) to only one of them. Therefore, I have to add both to the test-cluster as well. Also, I've promised that I'd implement the CI/CD for all of those solutions, but I think that it's going to be enough just to describe the logic. If you really want to see different implementation of CI/CD, you can shoot me a message, and I will write another post then. 

# Applications (Ann App of Apps)

So here is the PR for installing applications with `Application` manifests. 
<https://git.badhouseplants.net/allanger/helmfile-vs-argo/pulls/2/files>

I've chosen to follow the `App of apps` pattern, because it's including changes that must have been done if you use a "direct" applications installation and `app of apps`. So let's have a look at the main manifests, here you can see the base: <https://git.badhouseplants.net/allanger/helmfile-vs-argo/src/branch/argo-apps-main>

Initially I thought to use only one "Big Application" manifest for all three clusters, but I found out that it's not so easy when you don't have clusters with exactly the same infrastructure. Even with multi-source apps, you will probably have to use an additional tool for templating/substituting, for example like this:
```YAML
# app-of-apss.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argo-system
spec:
  destination:
    namespace: argo-system
    server: https://kubernetes.default.svc
  project: system
  sources:
    - path: ./manifests/$CLUSTER
      repoURL: git@git.badhouseplants.net:allanger/helmfile-vs-argo.git
      targetRevision: argo-apps-main
    - path: ./manifests/common
      repoURL: git@git.badhouseplants.net:allanger/helmfile-vs-argo.git
      targetRevision: argo-apps-main
```

and then, in a pipeline do something like this:
```BASH
export CLUSTER=cluster1
kubectl apply $(envsubst < app-of-apps.yaml) # I haven't tested it out, so this command may no work, but I hope you get the point. 
```

So it's either additional files, or an additional logic in CI/CD.

Also, the `helm-freeze` thing. I wanted to vendor charts, because in this example it's required, but my Gitea instance can't preview file changes when there are 9000+ lines of code updated, so I had to remove.

But logic would be like this
- Manual part:
    - Update `helm-freeze.yaml`
    - Run `helm-freeze sync`
    - Add a new application to the `manifests/$CLUSTER` dir
    - Push
- CI/CD
    - Since it needs to be `GitOps`, you need to check that charts in the `vendor` dir are up-to-date with `helm-freeze.yaml`. *Because if you updated helm-freeze and forgot to execute `helm-freeze sync`, you will have a contradiction between actual and desired states. That's one of the reasons, why I don't like this kind of vendoring. Either it's an addition step in CI, that is verifying that the manual step was done, or it's an additional work for reviewer. You also can add an action that is going to execute it withing the pipeline and push to your branch, but I'm completely against it. (something for another post maybe)*
    
    - Then depending on a branch:
      - If not `main`
        > Then you need to run `argocd diff` for production clusters, and deploy changes to the test clusters, so it's something like
      - If `main`
        > Deploy to all clusters

So let's try to do it

So we create a first `app-of-apps` manifests
```YAML
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argo-system
spec:
  destination:
    namespace: argo-system
    server: https://kubernetes.default.svc
  project: default
  source:
    path: ./manifests/cluster2/
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-updated

```

Then we need to create apps

```YAML
# ./manifests/cluster2/vpa.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vpa
  namespace: argo-system
spec:
  destination:
    namespace: vpa-system
    server: https://kubernetes.default.svc
  project: default
  source:
    helm:
      releaseName: vpa
      valueFiles:
        - ../../values/vpa.common.yaml
    path: ./vendor/vpa
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-updated

```
    
Here we have different options.

- Sync everything automatically (app-of-apps and applications), but it doesn't look too fail-safe to me. And also we can't know diff then, because what's different will be applied immediately. So it's 👎 
- Sync automatically only the `app-of-apps`, and then sync applications with the `argocd` cli. It sounds better, because then we can run diff on applications and know the difference between a wished state and a real state, so it's closer to 👍
- Sync applications automatically, but app-of-apps with cli. Doesn't sound to bad, does it? Maybe not that flexible as the previous option, but still not too bad. So it's closer to 👍 too.
- Sync everything with cli. I would say it will give you the best control, but will become additional steps in the pipeline. Now I don't think it's a hard thing to implement, so let's say "closer to 👍 too".

I don't consider the **first** option a reliable one, so I wouldn't even talk about it. You can try, of course, but your changes won't be visible unless they are deployed. So it's like the "test on production" thing.

The **second**, let's have a look. Let's try adding some values to the `vpa` release, and install Goldilocks (assuming it wasn't installed). 

VPA values: 
```YAML
# ./values/vpa.common.yaml
# I've just changes `false` to `true`
updater:
  enabled: true # <- here
```

Goldilocks app:
```YAML
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: goldilocks
  namespace: argo-system
spec:
  destination:
    namespace: vpa-system
    server: https://kubernetes.default.svc
  project: default
  source:
    helm:
      releaseName: goldilocks
    path: ./vendor/goldilocks
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-updated

```

And I pushed to repo. 

So now let see what I've got in UI:
![Changes in UI](/argocd-vs-helmfile/update-in-ui.png)

This is how `diffs` for VPA look in the UI:
![Diff in UI](/argocd-vs-helmfile/diff-in-ui.png)

{{< details "Here you can find all the diffs from the UI as text" >}}

```diff
+ apiVersion: apps/v1
+ kind: Deployment
+ metadata:
+   labels:
+     app.kubernetes.io/component: updater
+     app.kubernetes.io/instance: vpa
+     app.kubernetes.io/managed-by: Helm
+     app.kubernetes.io/name: vpa
+     app.kubernetes.io/version: 0.11.0
+     argocd.argoproj.io/instance: vpa
+     helm.sh/chart: vpa-1.6.0
+   name: vpa-updater
+   namespace: vpa-system
+ spec:
+   replicas: 1
+   selector:
+     matchLabels:
+       app.kubernetes.io/component: updater
+       app.kubernetes.io/instance: vpa
+       app.kubernetes.io/name: vpa
+   template:
+     metadata:
+       labels:
+         app.kubernetes.io/component: updater
+         app.kubernetes.io/instance: vpa
+         app.kubernetes.io/name: vpa
+     spec:
+       containers:
+         - env:
+             - name: NAMESPACE
+               valueFrom:
+                 fieldRef:
+                   fieldPath: metadata.namespace
+           image: 'k8s.gcr.io/autoscaling/vpa-updater:0.11.0'
+           imagePullPolicy: Always
+           livenessProbe:
+             failureThreshold: 6
+             httpGet:
+               path: /health-check
+               port: metrics
+               scheme: HTTP
+             periodSeconds: 5
+             successThreshold: 1
+             timeoutSeconds: 3
+           name: vpa
+           ports:
+             - containerPort: 8943
+               name: metrics
+               protocol: TCP
+           readinessProbe:
+             failureThreshold: 120
+             httpGet:
+               path: /health-check
+               port: metrics
+               scheme: HTTP
+             periodSeconds: 5
+             successThreshold: 1
+             timeoutSeconds: 3
+           resources:
+             limits:
+               cpu: 200m
+               memory: 1000Mi
+             requests:
+               cpu: 50m
+               memory: 500Mi
+           securityContext: {}
+       securityContext:
+         runAsNonRoot: true
+         runAsUser: 65534
+       serviceAccountName: vpa-updater
```

```DIFF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: >
      {"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRoleBinding","metadata":{"annotations":{},"labels":{"argocd.argoproj.io/instance":"vpa"},"name":"vpa-actor"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"vpa-actor"},"subjects":[{"kind":"ServiceAccount","name":"vpa-recommender","namespace":"vpa-system"}]}
  labels:
    argocd.argoproj.io/instance: vpa
  managedFields:
    - apiVersion: rbac.authorization.k8s.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        'f:metadata':
          'f:labels':
            .: {}
            'f:argocd.argoproj.io/instance': {}
        'f:roleRef': {}
        'f:subjects': {}
      manager: argocd-application-controller
      operation: Update
      time: '2023-02-13T20:58:02Z'
    - apiVersion: rbac.authorization.k8s.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        'f:metadata':
          'f:annotations':
            .: {}
            'f:kubectl.kubernetes.io/last-applied-configuration': {}
      manager: argocd-controller
      operation: Update
      time: '2023-02-13T20:58:02Z'
  name: vpa-actor
  resourceVersion: '34857'
  uid: 71958267-68b4-4923-b2bb-eaf7b3c1a992
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: vpa-actor
subjects:
  - kind: ServiceAccount
    name: vpa-recommender
    namespace: vpa-system
+  - kind: ServiceAccount
+    name: vpa-updater
+    namespace: vpa-system
```
```DIFF
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: ClusterRoleBinding
+ metadata:
+   labels:
+     argocd.argoproj.io/instance: vpa
+   name: vpa-evictionter-binding
+ roleRef:
+   apiGroup: rbac.authorization.k8s.io
+   kind: ClusterRole
+   name: vpa-evictioner
+ subjects:
+   - kind: ServiceAccount
+     name: vpa-updater
+     namespace: vpa-system
```
```DIFF
+ apiVersion: rbac.authorization.k8s.io/v1
+ kind: ClusterRoleBinding
+ metadata:
+   labels:
+     argocd.argoproj.io/instance: vpa
+   name: vpa-status-reader-binding
+ roleRef:
+   apiGroup: rbac.authorization.k8s.io
+   kind: ClusterRole
+   name: vpa-status-reader
+ subjects:
+   - kind: ServiceAccount
+     name: vpa-updater
+     namespace: vpa-system
```
```DIFF
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: >
      {"apiVersion":"rbac.authorization.k8s.io/v1","kind":"ClusterRoleBinding","metadata":{"annotations":{},"labels":{"argocd.argoproj.io/instance":"vpa"},"name":"vpa-target-reader-binding"},"roleRef":{"apiGroup":"rbac.authorization.k8s.io","kind":"ClusterRole","name":"vpa-target-reader"},"subjects":[{"kind":"ServiceAccount","name":"vpa-recommender","namespace":"vpa-system"}]}
  labels:
    argocd.argoproj.io/instance: vpa
  managedFields:
    - apiVersion: rbac.authorization.k8s.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        'f:metadata':
          'f:labels':
            .: {}
            'f:argocd.argoproj.io/instance': {}
        'f:roleRef': {}
        'f:subjects': {}
      manager: argocd-application-controller
      operation: Update
      time: '2023-02-13T20:58:02Z'
    - apiVersion: rbac.authorization.k8s.io/v1
      fieldsType: FieldsV1
      fieldsV1:
        'f:metadata':
          'f:annotations':
            .: {}
            'f:kubectl.kubernetes.io/last-applied-configuration': {}
      manager: argocd-controller
      operation: Update
      time: '2023-02-13T20:58:02Z'
  name: vpa-target-reader-binding
  resourceVersion: '34855'
  uid: 30261740-ad5d-4cd9-b043-0ff18daaf3aa
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: vpa-target-reader
subjects:
  - kind: ServiceAccount
    name: vpa-recommender
    namespace: vpa-system
+  - kind: ServiceAccount
+    name: vpa-updater
+    namespace: vpa-system
```
{{< /details >}}

And for Goldilocks
![Goldilocks Application](/argocd-vs-helmfile/goldilocks-ui.png)

All the diffs are also there, and they look good.

But to seem them I had to push to the target branch. And we want to see changes without pushing.
```YAML
# main
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argo-system
spec:
  destination:
    namespace: argo-system
    server: https://kubernetes.default.svc
  project: default
  source:
    path: ./manifests/cluster2/
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-main

```

Then we need to create apps

```YAML
# ./manifests/cluster2/vpa.yaml
# feature branch
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: vpa
  namespace: argo-system
spec:
  destination:
    namespace: vpa-system
    server: https://kubernetes.default.svc
  project: default
  source:
    helm:
      releaseName: vpa
      valueFiles:
        - ../../values/vpa.common.yaml
    path: ./vendor/vpa
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-main
```

![App of apps in the `main`](/argocd-vs-helmfile/app-of-apps-main.png)

So currently app of apps doesn't know about what's happening in my new branch. And so I can't just do `argocd app vpa diff`. So what should I do?
```BASH
argocd app diff --help
...
Usage:
  argocd app diff APPNAME [flags]
...
```

That means that I can't use it for those new apps that exist inly in my branch, because I need to pass an App name, and since it's not installed yet, I have something like 
```BASH
argocd app diff vpa
FATA[0000] rpc error: code = NotFound desc = error getting application: applications.argoproj.io "vpa" not found
```

There is a `--local` option, but it still requires a name ~~(why if there is a name in manfiests 🙃🙃🙃)~~
```BASH
# Just testing out
argocd app diff vpa --local ./manifests/cluster2/
FATA[0000] rpc error: code = NotFound desc = error getting application: applications.argoproj.io "vpa" not found # 🤪
```

Ok, then we can check the app-of-apps
```BASH
argocd app diff app-of-apps --local ./cluster-1.yaml
Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.FATA[0000] error while parsing source parameters: stat cluster-1.yaml/.argocd-source.yaml: not a directory

argocd app diff app-of-apps --local ./cluster-1.yaml --server-side-generate
FATA[0000] rpc error: code = Unknown desc = failed to get app path: ./manifests/cluster2/: app path does not exist

argocd app diff app-of-apps --local ./cluster-2.yaml --server-side-generate --loglevel debug
FATA[0000] rpc error: code = Unknown desc = failed to get app path: ./manifests/cluster2/: app path does not exist
# I can't get it, maybe anybody could tell me what I'm doing wrong?


argocd app diff app-of-apps --local ./cluster-2.yaml
Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.FATA[0000] error while parsing source parameters: stat cluster-2.yaml/.argocd-source.yaml: not a directory


mkdir /tmp/argo-test
cp cluster-2.yaml /tmp/argo-test
argocd app diff app-of-apps --local /tmp/argo-test --loglevel debug

Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.
===== argoproj.io/Application /app-of-apps ======
0a1,15
> apiVersion: argoproj.io/v1alpha1
> kind: Application
> metadata:
>   labels:
>     argocd.argoproj.io/instance: app-of-apps
>   name: app-of-apps
> spec:
>   destination:
>     namespace: argo-system
>     server: https://kubernetes.default.svc
>   project: default
>   source:
>     path: manifests/cluster2/
>     repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
>     targetRevision: argo-apps-main

# If i change a branch for the app of apps target to the current one 

cat cluster-2.yaml
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argo-system
spec:
  destination:
    namespace: argo-system
    server: https://kubernetes.default.svc
  project: default
  source:
    path: ./manifests/cluster2/
    repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
    targetRevision: argo-apps-updated

kuvectl apply -f cluster-2.yaml
cp cluster-2.yaml /tmp/argo-test
argocd app diff app-of-apps --local /tmp/argo-test --loglevel debug
Warning: local diff without --server-side-generate is deprecated and does not work with plugins. Server-side generation will be the default in v2.7.
===== argoproj.io/Application /app-of-apps ======
0a1,15
> apiVersion: argoproj.io/v1alpha1
> kind: Application
> metadata:
>   labels:
>     argocd.argoproj.io/instance: app-of-apps
>   name: app-of-apps
> spec:
>   destination:
>     namespace: argo-system
>     server: https://kubernetes.default.svc
>   project: default
>   source:
>     path: ./manifests/cluster2/
>     repoURL: ssh://git@git.badhouseplants.net/allanger/helmfile-vs-argo.git
>     targetRevision: argo-apps-updated
```

I don't really understand what it means. *Most probably, I'm just stupid.* But what I see is that it's not working with ` --server-side-generate ` with an error, that I can't really understand. And is saying that I shouldn't use it without the flag, because that way of running it is deprecated. And even without the flag, it's giving me a strange output, that I don't know how to use it. 
 
So as I see, to have a proper diff, you need to apply. But it doesn't look like a fail-safe and scalable way to use. 

I told that we can check different options for syncing, but as I see now, other workflows won't give me a better overview about what's happening. So I don't think it makes a lot of sense. If I find a way to see a proper diff without applying manifests first, I would go back to this topic and write one more post. 

## Maybe it's because an App of Apps layer

Let's try installing apps directly. Remove an app-of-apps from k8s. And let's use manifests from `/manifests/cluster2/` directly. As I see, diffing won't work anyway for applications that are not installed yet. So you can check ones that are already installed, but I couldn't make it work too. I was changing values to check if they are shown, but they weren't. *Again, I could simply screw up, and if you have a positive experience with that, don't hesitate to let me know about it, I'm willing to change my mind*

## Conclusion
So you can check the PR here: <https://git.badhouseplants.net/allanger/helmfile-vs-argo/pulls/2/files>

I like that `values` can be handled as normal values files. (But for handling secrets you might have to add a [CMP](https://argo-cd.readthedocs.io/en/stable/user-guide/config-management-plugins/), that means an additional work and maintenance) But even if adding CMP is fine, I couldn't get proper `diffs` for my changes, that means that I can't see what's happening without applying manifests. And applying manifests will mean that other team members will not be work on other tickets withing the same scope, so it looks like a bottleneck to me. 

But I don't like that you need to add a lot of manifests to manage all the applications. We have only 2 manifests that are copied from folder to folder. So we have a lot of repeating code. And repeating code is never good. So I would write a tool that can let you choose applications from the list of all applications and choose clusters where they need to be deployed. So the config looks like this:
```YAML
app_path: ./manifests/common
clusters:
  - cluster: cluster1
    applications:
      - vpa
  - cluster: cluster2
    applications:
      - vpa
      - goldilocks
  - cluster: cluster3
    applications: 
      - vpa
      - goldilocks
```
But I think that with the whole GitOps pulling concept it will be a hard thing to implement. And in the end it looks like helmfile, so ... 🤷‍♀️🤷‍♀️🤷‍♀️

I can only say, that I see no profit in using argo like this. It only seems like either a very complicated setup (most probably you will be able to implement anything you need, the question is, how much time will you spend with that), or a ~~crippled~~ not complete setup.

And if you compare an amount of lines that area updadated to install these apps as `Applications` to the helmfile stuff, it's going to be ~100 vs ~30. And that's what I also don't like.

In the next post I will try doing the same with `ApplicationSets`, and we'll see, if it looks better or not. 

Thanks,

Oi!
