Thursday, 5 March 2026

Introduction to ArgoCD


ArgoCD is a tool for deploying applications in Kubernetes cluster following the GitOps principles. 

Argo CD is a declarative, GitOps-based continuous delivery (CD) tool designed specifically for Kubernetes. It acts as a controller that monitors running applications, compares their live state to the desired state defined in a Git repository, and automatically syncs them to ensure consistency. 

Key Aspects of an Argo CD Application:
  • GitOps Source of Truth: Git repositories hold the desired state (manifests, Helm charts, Kustomize configs), which Argo CD pulls to apply to clusters.
  • Automated Synchronization: It automatically detects "OutOfSync" applications—where the live cluster state differs from Git—and can automatically or manually sync them to match.
  • Continuous Monitoring: It acts as a Kubernetes controller that continuously monitors applications.
  • Visualization & Management: It provides a web UI to visualize application structure, monitor status, and manage rollbacks.
  • Key Capabilities: Supports automated deployment, drift detection, and easy rollbacks. 

Argo CD is often used to ensure that the actual state of a Kubernetes cluster matches the configuration stored in a Git repository, making the deployment process more reliable and transparent.

ArgoCD GitOps flow:
  • Commit and push infra code changes (Terraform, Helm values etc) to the main branch of your GitHub repository.
  • ArgoCD Sync:
    • Manual: Log in to our ArgoCD Dashboard and click the "Sync" or "Refresh" button on the psmdb-default-sharded application.
    • Automatic: If "Self-Heal" or "Auto-Sync" is enabled, ArgoCD will detect the Git change within ~3 minutes and apply it to the cluster automatically.
  • Monitor the Operator: Once ArgoCD syncs, infra will get changed e.g. operators will see the updated custom resources and then trigger the further actions.

Installation


Installing ArgoCD in a Kubernetes cluster is primarily done using manifests or Helm charts. The most common and recommended approach for beginners is using the Official Argo CD Manifests. 

Prerequisites:
  • A running Kubernetes cluster (v1.22 or later).
  • kubectl command-line tool installed and configured to your cluster.
  • At least 2GB of available memory in your cluster.

Applications


ArgoCD defines custom Kubernetes objects like ApplicationAppProject, settings...which can be defined declaratively using Kubernetes manifests and deployed via kubectl apply to the ArgoCD namespace which is argocd by default.

To check ArgoCD applications:

% kubectl get applications -A        
NAMESPACE   NAME                    SYNC STATUS   HEALTH STATUS
argocd      my-app                  Synced        Progressing 



Dashboard


If you don't know the url of the ArgoCD dashboard, find the IP of the ArgoCD server and port-forward it:

% kubectl get svc argocd-server -n argocd
NAME          TYPE      CLUSTER-IP    EXTERNAL-IP PORT(S)        AGE
argocd-server ClusterIP 172.21.103.127 <none>     80/TCP,443/TCP 1d


By default, ArgoCD includes a built-in admin user with full super-user access. For better security practices, it is recommended that you use the admin account only for the initial configuration, then disable it once all required users have been added.

For this example, we can stick to this default admin user. Its password can be obtain via this command:

% kubectl get secret \
argocd-initial-admin-secret \
-n argocd \
-o jsonpath="{.data.password}" \
| base64 -d

Port forwarding:

% kubectl port-forward svc/argocd-server -n argocd 8080:443
Forwarding from 127.0.0.1:8080 -> 8080
Forwarding from [::1]:8080 -> 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
Handling connection for 8080
...

We can now open the address http://localhost:8080/ in the browser to get the ArgoCD dashboard and log in with credentials above.

If there are no application registered with ArgoCD, we will see something like this:



application.yaml vs applicationset.yaml


In the world of Argo CD and Kubernetes GitOps, these two files represent different levels of automation. 

1. application.yaml


An Application is the basic unit of Argo CD. This file defines a single link between a specific Git repository (the source) and a specific Kubernetes cluster/namespace (the destination).

  • The Goal: To tell Argo CD: "Take the code from this folder and make sure it looks like that in my cluster."
  • Key Components:
    • Project: The logical group it belongs to.
    • Source: Argo CD needs three pieces of information to find our files:
      • repoURL: Git URL. Which "building" (repository) do I go to?
      • targetRevision: revision (branch/tag), Which "floor" (branch/tag/commit) am I looking at?
      • path (mandatory): path to the manifests. Which "room" (folder) contains the manifests?
    • Destination: The target cluster URL and namespace.
    • Sync Policy: Whether to automatically sync changes or require a manual button press.


Example Snippet:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: guestbook-app
spec:
  project: default
  source:
    repoURL: https://github.com/argoproj/argocd-example-apps.git
    targetRevision: main
    path: guestbook
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated:
      selfHeal: true
      prune: true


Argo CD is completely "tool-agnostic." It doesn't care if you use Helm, Kustomize, or just a folder full of .yaml files; it just needs to know where they are.

How Argo CD Decides


When Argo CD looks at the path you provided, it follows this logic:
  • Does it see Chart.yaml? -> It uses Helm.
    • Argo CD uses values.yaml by default if it detects that the directory specified in the path is a Helm chart.
  • Does it see kustomization.yaml? -> It uses Kustomize.
  • Does it see only .yaml or .json files? -> It uses Plain Manifests.

Custom Helm Charts

At an absolute minimum, a Helm chart requires a directory containing these three things:

my-app/                # The "path" referenced in Argo CD
├── Chart.yaml         # Metadata about the chart (Required)
├── values.yaml        # Default configuration variables (Required)
└── templates/         # The folder for your Kubernetes manifests (Required)
    └── deployment.yaml # At least one manifest to actually deploy something


2. applicationset.yaml


The ApplicationSet is a controller that acts as a factory for Applications. Instead of manually creating 50 application.yaml files for 50 different microservices or 10 different clusters, you write one ApplicationSet.
  • The Goal: To automate the creation, update, and deletion of multiple Argo CD Applications at scale.
  • How it works: It uses Generators to decide how many Applications to create.
    • List Generator: Hardcode a list of clusters or environments.
    • Git Generator: Automatically create an App for every folder found in a Git repo.
    • Cluster Generator: Automatically deploy an App to every cluster registered in Argo CD.
  • Key Advantage: It follows the "DRY" (Don't Repeat Yourself) principle. If you add a new cluster to your fleet, the ApplicationSet detects it and automatically deploys your apps there.


Example Snippet:


apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: guestbook-factory
spec:
  generators:
  - list:
      elements:
        - cluster: engineering-dev
        - cluster: engineering-prod
  template: # This looks exactly like a standard Application
    metadata:
      name: '{{cluster}}-guestbook'
    spec:
      project: default
      source:
        repoURL: https://github.com/argoproj/argocd-example-apps.git
        path: guestbook
      destination:
        server: https://{{cluster}}-api.com
        namespace: guestbook



Example with Pull Request Generator


Using the Pull Request Generator is the "gold standard" for enabling ephemeral (preview) environments. It allows Argo CD to watch your GitHub repository for open PRs and automatically spin up a dedicated version of your app for testing.

The following parameters are dynamic variables provided by the generator based on the metadata of the PR:
  • {{ number }} - PR number from GitHub API
    • Role: Unique Identifier.
    • Usage: Since PR numbers are unique and immutable in GitHub, this is the safest way to name your Application and Namespace.
    • Result: Prevents naming collisions when multiple developers are testing different features simultaneously.
  • {{ branch }} - Head branch name of the PR
    • Role: Git Reference.
    • Usage: This tells Argo CD exactly which commit to look at. Without this, Argo CD would try to deploy from main.
    • Result: Your ephemeral environment stays in sync with every push to that specific feature branch.
  • {{ branch_slug }} - URL-safe version of branch name 
  • {{ head_sha }} -  Commit SHA at the tip of the branch
  • {{ labels }} -  List of all labels on the PR e.g. ["preview", "staging"]
    • Role: Filter & Logic.
    • Usage: In the example above, we use the filters block. The generator will ignore any PR that doesn't have the feature-test-deployment label.
    • Advanced Tip: You can also use labels in the template. For example, if you have labels like size:large or db:postgres, you could map those to Helm values to provision different resources for that specific PR.

The Lifecycle Workflow

  • Developer opens a PR in GitHub.
  • Developer adds the label feature-test-deployment.
  • Argo CD detects the label, triggers the ApplicationSet, and creates a new Application named my-app-pr-42.
  • Argo CD creates the namespace preview-pr-42 and deploys the code from the feature branch.
  • Developer closes the PR or removes the label.
  • Argo CD automatically deletes the Application and cleans up the resources (garbage collection).

The ApplicationSet Configuration

This ApplicationSet will scan your repository, filter for the specific label, and then use the PR details to name the application and target the correct branch.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: ephemeral-preview-apps
  namespace: argocd
spec:
  generators:
    - pullRequest:
        github:
          owner: my-organization
          repo: my-app-repo
          # Use a secret for your GitHub Token to avoid rate limits
          tokenRef:
            secretName: github-token
            key: token
        # This filters the PRs based on the label
        filters:
          - labels:
              - feature-test-deployment
        requeueAfterSeconds: 60
  template:
    metadata:
  # Use .number to ensure a unique app name per PR (e.g., my-app-pr-42)
      name: 'my-app-pr-{{number}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/my-organization/my-app-repo.git
        # Use .branch to pull code from the specific feature branch
        targetRevision: '{{branch}}'
        path: kubernetes/manifests
        helm:
          parameters:
  # You can pass the PR number or labels into your app's code/config
            - name: "envName"
              value: "pr-{{number}}"
      destination:
        server: https://kubernetes.default.svc
        # Create a unique namespace for the ephemeral environment
        namespace: 'preview-pr-{{number}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true


The requeueAfterSeconds: 60 means ArgoCD polls GitHub every 60 seconds - when a PR is opened with the feature-test-deployment label, within a minute ArgoCD picks it up, injects these variables into the template, and creates the child Application. When the PR is closed or the label is removed, ArgoCD deletes the child Application (and prune: true tears down the K8s resources).



Go template engine


By enabling the Go template engine (goTemplate: true), Argo CD switches from simple string replacement to a full Go templating context. In this mode, all parameters from the generator are nested under a root object, meaning you must use the leading dot to access them.

Here is the rewritten example using goTemplate: true.


apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: ephemeral-preview-apps
spec:
  # This enables the standard Go template engine
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - pullRequest:
        github:
          owner: my-organization
          repo: my-app-repo
          tokenRef:
            secretName: github-token
            key: token
        filters:
          - labels:
              - feature-test-deployment
  template:
    metadata:
      # Now using the leading dot because goTemplate is true
      name: 'my-app-pr-{{.number}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/my-organization/my-app-repo.git
        targetRevision: '{{.branch}}'
        path: kubernetes/manifests
        helm:
          parameters:
            - name: "envName"
              value: "pr-{{.number}}"
            # goTemplate allows us to use range to iterate over labels
            - name: "appliedLabels"
              value: "{{range .labels}}{{.}},{{end}}"
      destination:
        server: https://kubernetes.default.svc
        namespace: 'preview-pr-{{.number}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true
        syncOptions:
          - CreateNamespace=true


Key Changes with goTemplate: true
  1. The Dot Prefix: Since the parameters are now passed as a data structure to the Go template engine, you access them as fields of the root object: {{.number}}, {{.branch}}, and {{.labels}}.
  2. Missing Key Error: With goTemplateOptions: ["missingkey=error"], if you make a typo (e.g., {{.numbr}}), the ApplicationSet controller will explicitly error out rather than just rendering an empty string. This is much safer for production.
  3. Iteration/Logic: You can now use standard Go template functions. For instance, in the example above, I used a range loop to iterate through the .labels list:
                {{range .labels}}{{.}},{{end}} loops through each label and prints it.


Why use this over the default?

While it requires the extra . syntax, goTemplate: true is much more powerful. It allows you to use if/else logic (e.g., "if label is 'large', request more CPU") and complex string manipulations that the default "fast replacement" engine cannot handle.

If you omit the dot while goTemplate: true is active, the template will fail to render because it will look for a global variable instead of a field in the provided data object.

Using goTemplate: true opens up a world of "smart" deployments where your PR labels can act as configuration switches. This prevents you from needing a separate ApplicationSet for every minor variation.

Here is how you can use conditional logic to adjust resources or features based on the labels found on a PR.

Example: Advanced Conditional ApplicationSet

In this scenario, if a PR has the label env:large, we increase the resource limits. If it has feature:postgres, we enable a database flag.

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: smart-ephemeral-apps
spec:
  goTemplate: true
  goTemplateOptions: ["missingkey=error"]
  generators:
    - pullRequest:
        github:
          owner: my-org
          repo: my-repo
          tokenRef:
            secretName: github-token
            key: token
        filters:
          - labels:
              - feature-test-deployment
  template:
    metadata:
      name: 'app-pr-{{.number}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/my-org/my-repo.git
        targetRevision: '{{.branch}}'
        path: charts/my-app
        helm:
          values: |
            replicaCount: {{ if (contains "env:high-availability" (join "," .labels)) }}3{{ else }}1{{ end }}
            resources:
              {{- if (contains "size:large" (join "," .labels)) }}
              limits:
                cpu: "1000m"
                memory: "2Gi"
              {{- else }}
              limits:
                cpu: "200m"
                memory: "512Mi"
              {{- end }}
            experimentalFeatures:
              enabled: {{ contains "feature:beta" (join "," .labels) }}
      destination:
        server: https://kubernetes.default.svc
        namespace: 'pr-{{.number}}'


Pro-Tips for this Setup

  • The "Contains" Trick: In Go templating, searching a list directly can be clunky. By using (join "," .labels), we turn the list into a single string (e.g., "feature-test-deployment,size:large") and then use contains to check for our specific trigger labels.
  • The Root App Advantage: Because this logic is inside your infrastructure/ folder (managed by your Root App), you can test changes to this logic on a branch. You can actually have a "Development Root App" that points to your branch to see if your new if/else logic works before merging it to main.
  • Cleanup: When you merge the PR and delete the branch in GitHub, the PR Generator sees the PR is closed. It removes the generated Application, and Argo CD's prune logic deletes the namespace and all associated resources automatically.

Summary of the Flow

  1. Management: The Root App (application.yaml) monitors the repo for automation changes.
  2. Automation: The ApplicationSet (applicationset.yaml) monitors GitHub for new PRs with specific labels.
  3. Deployment: The Ephemeral App (the child application.yaml) is created with specific CPU/RAM settings dictated by those PR labels.




How do they work together?


In many production GitOps setups, they must exist together to create a functional hierarchy. In Argo CD, this is often referred to as the "App of Apps" pattern or a "Management Repo" structure.

How They Interact

Think of the relationship as Infrastructure vs. Implementation:
  • The ApplicationSet (applicationset.yaml): Acts as the automation engine. It lives in your management folder and its job is to watch the repository and "spawn" Applications.
  • The Application (application.yaml): These are the individual instances created by the ApplicationSet. You might also have a manual application.yaml for a unique service that doesn't fit the "factory" mold of the ApplicationSet.


Recommended Repository Structure

To keep things clean, teams usually organize their repository like this:

my-gitops-repo/
├── bootstrap/
│   ├── root-app.yaml  # A manual Application that points to this folder
│   └── cluster-addons-set.yaml # An ApplicationSet for things like ingress, monitoring
├── projects/
│   └── microservices-set.yaml  # Your PR Generator ApplicationSet
├── apps/
│   ├── my-unique-legacy-app/
│   │   └── application.yaml    # A one-off manual Application
│   └── templates/    # The base manifests the ApplicationSet points to
│       ├── deployment.yaml
│       └── service.yaml


Common Scenarios for Co-existence


1. The "Bootstrap" Application

You often have a single, manually created application.yaml (sometimes called the Root App) whose only job is to deploy the applicationset.yaml.

Why? This allows you to manage your automation via GitOps. If you update the ApplicationSet logic in Git, the Root App syncs it, and the factory settings update automatically.

2. The "Hybrid" Approach

You might use an ApplicationSet to handle 50 identical microservices across 3 clusters, but keep a standard Application for a third-party tool (like a database or a legacy monolith) that requires very specific, manual configuration that doesn't fit a template.


Important Collision Warning ⚠️

While they can exist together, you should avoid a "Circular Dependency" or "Ownership Fight":
  • Don't have an ApplicationSet generate an Application with the exact same name as a manually created application.yaml in the same namespace.
  • Management: If an ApplicationSet creates an app, it "owns" it. If you try to manually edit that child app in the Argo CD UI, the ApplicationSet controller will immediately revert your changes to match its template.

Summary: They are complementary tools. Use application.yaml for specific, static deployments and applicationset.yaml for templated, dynamic, or multi-environment deployments. Using them together is the standard way to achieve full "Cluster as Code."

The "Root App" (or App-of-Apps) pattern is the ultimate way to manage your Argo CD instance. It allows you to manage your automation through the same GitOps workflow as your code.

The "Root" Application Structure

The Root App is a standard application.yaml that points to a folder in your Git repo containing your ApplicationSet files. Once you apply this one file manually, Argo CD takes over and manages all your "factories" automatically.

So there are two separate layers here that do different jobs, and it's easy to conflate them.


1. Layer 1 — Bootstrap Application (The Root Application) (root-app.yaml)

This is the only file you apply with kubectl apply -f. It tells Argo CD to watch the infrastructure/ directory for any ApplicationSets or other Applications. It manages the ArgoCD-level resources: ApplicationSet, other Applications, ExternalSecrets.... Its only job is to keep the e.g. ApplicationSet definition in sync with the repo. This is what the targetRevision in root-app.yaml controls.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-management-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/gitops-control-plane.git
    targetRevision: HEAD
    path: infrastructure  # Folder where your ApplicationSets live
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true



2. Layer 2 — ApplicationSet (pr-generator-set.yaml)
                                                                                                                                                             
This is the pull-request generator. It watches GitHub for open PRs with the preview label and creates a child Application per PR. Each child Application it creates points at {{ .branch }} — the PR's own
branch:

targetRevision: "{{ .branch }}"
path: deploy/helm-chart



The Directory Layout


To make this work seamlessly, organize your repository so the Root App can find its "children" without getting confused.

gitops-control-plane/
├── root-app.yaml               # Managed manually (once)
└── infrastructure/             # Managed by Root App
    ├── pr-generator-set.yaml   # Your PR Generator with goTemplate: true
    ├── production-cluster-set.yaml
    └── global-monitoring-app.yaml # A standard Application for Prometheus

Why this is powerful:
  • Self-Healing Infrastructure: If someone accidentally deletes your ApplicationSet from the cluster, the Root App will see the discrepancy and recreate it instantly.
  • Version Controlled Automation: If you want to update your goTemplate logic or change a github-token reference, you just submit a PR to the infrastructure/ folder. Once merged, the Root App updates the ApplicationSet, which then updates all your ephemeral environments.
  • Single Source of Truth: You can look at one Git folder and see exactly what automation logic is governing your entire cluster fleet.

So the two layers are independent:

root-app.yaml (tracks main)
└─ manages: ApplicationSet (in cluster)
                        └─ polls GitHub PRs with specified label
                                ├─ PR #1 → Application → branch: feat1-branch → deploy/helm-chart
                                └─ PR #2 → Application → branch: feat2-branch → deploy/helm-chart

Changing root-app.yaml's targetRevision to main only affects whether the ApplicationSet definition itself stays in sync with main. It has zero effect on how individual PR previews are deployed —
those are handled entirely by the generator using each PR's own branch.

The hardcoded branch in root-app.yaml is only necessary during development of this feature branch so that iterative changes to applicationset.yaml were picked up live. Once merged to main, main is
the correct target.


One Final Pro-Tip for PR Generators

When using the PR Generator inside a Root App structure, ensure your github-token Secret is already created in the argocd namespace. The ApplicationSet needs that token to "talk" to the GitHub API to see which PRs have the feature-test-deployment label.




Summary Comparison


Feature            Application (.yaml)                               ApplicationSet (.yaml)
----------            -------------------------                               -----------------------------
Scope               Manages one app in one environment.    Manages many apps across many environments.
Manual Effort  High (must create a file for every app).   Low (one template handles everything).
Use Case          Simple setups or unique, one-off apps.   Microservices, multi-tenant, or multi-cluster                                                                                                    environments.
Relationship    The "Child" object.                                   The "Parent" (Factory) that creates children.


---

Resources:


No comments: