Argo CD Lesson 3 of 45

Installing Argo CD: Manifests vs Helm, non-HA vs HA, First Login & the CLI — on AKS, EKS and GKE

Before Argo CD can sync a single application, it has to exist — as a set of pods in a namespace on a real Kubernetes cluster. That sounds trivial, and on a laptop cluster it nearly is: one kubectl apply and you have it. But the moment you install it somewhere real, three questions land on you at once, and getting any of them wrong costs you an afternoon (or a surprise cloud bill):

  1. How do you install it — the plain manifests, the HA manifests, or the Helm chart? They are not interchangeable, and the choice affects how you upgrade for the next three years.
  2. How much do you install — the single-replica default, or the high-availability topology with a Redis cluster and multiple controllers? Install HA on a one-node cluster and half the pods sit Pending forever.
  3. How do you reach it — because a freshly installed argocd-server is a ClusterIP you cannot open in a browser, and the way you expose it is completely different on Azure, AWS and Google. This is the part every “just run kubectl apply” tutorial skips, and it is where this lesson spends most of its time.

By the end you will have installed Argo CD (for real, with exact commands and the output you should see), logged in, rotated the bootstrap password, and exposed the UI on AKS via Application Gateway, EKS via an ALB, and GKE via a Google Cloud Load Balancer — and torn it all down cleanly so nothing keeps billing.


Why this matters

Argo CD is a pull-based GitOps controller: it runs inside your cluster, watches a Git repository, and continuously reconciles the cluster toward what Git says. That “runs inside your cluster” is the whole reason installation is its own topic. Unlike a CI system that lives on a SaaS somewhere and pushes to you, Argo CD is a workload you operate — pods, a Redis cache, CRDs, RBAC, a web server that needs exposing. Installing it well is the difference between a control plane your team trusts and a flaky box someone reinstalls every quarter.

It helps to be honest up front about what Argo CD is not. It is not a CI system, and keeping that boundary clear stops you from trying to make the installer do things it was never meant to do — CI ends where Argo CD begins.

Argo CD does Argo CD does not
Watch Git and reconcile the cluster toward it (pull model) Build images, run tests, or push to a registry (that’s CI)
Render Helm/Kustomize into plain YAML and apply it Author your manifests or decide what to deploy
Show live drift between Git and the cluster, and heal it Store desired state itself — Git is the source of truth
Run inside your cluster as pods you operate Live as an external SaaS that pushes into your cluster

The mental model to hold for this entire lesson: the Argo CD control plane is identical on every cloud — the same pods, the same CRDs, the same argocd namespace. Only the edges differ. How you get a kubeconfig, and how you expose argocd-server to real users, are cloud-specific; everything between is portable. That is why the lab installs one identical stack and then forks into three cloud-specific “how do I reach it” paths. GitOps is never “different on AWS” — the reconcile loop is the same everywhere; only the load balancer in front of it changes.

If you want the deeper tour of what each pod does, the companion lesson Argo CD Architecture: the repo-server, application-controller and API server takes the components apart one by one. Here we install them and get logged in.


What actually gets installed

Whichever method you pick, the result is the same short list of workloads in the argocd namespace. Knowing them by name makes every later step — reading kubectl get pods, debugging a stuck install, deciding what HA scales — obvious instead of mysterious.

Component What it does Workload kind (default) Talks to
argocd-server The API + web UI + gRPC endpoint the CLI hits. Stateless. Deployment Everything; this is your front door
argocd-repo-server Clones Git repos and renders manifests (Helm/Kustomize) into plain YAML Deployment Git repos, the controller
argocd-application-controller The reconcile engine: diffs desired (Git) vs live (cluster) and syncs StatefulSet Target clusters’ API servers
argocd-applicationset-controller Generates Applications from generators (git, cluster, matrix…) Deployment Git, the API server
argocd-notifications-controller Sends sync/health notifications (Slack, webhooks) Deployment External notification services
argocd-dex-server Optional SSO federation (OIDC/SAML/LDAP) — off until you configure it Deployment Your IdP (Entra ID, etc.)
argocd-redis Cache for the controller/server (manifests, cluster state) Deployment server + controller

The two you touch most are argocd-server (the thing you log into) and argocd-application-controller (the thing that does the syncing). argocd-repo-server is the quiet workhorse that turns a Helm chart or Kustomize overlay into flat YAML — when a sync says ComparisonError, this pod’s logs are usually where the answer is. Notice that only the application-controller is a StatefulSet; that detail matters later when we talk about sharding it across many clusters.

Two facts to file away now. First, the installer also creates a handful of CustomResourceDefinitionsapplications.argoproj.io, applicationsets.argoproj.io, and appprojects.argoproj.io — under the API group argoproj.io/v1alpha1. Those CRDs are the vocabulary of everything you do in Argo CD. Second, it creates a few ConfigMaps and Secrets you will edit constantly: argocd-cm (main config), argocd-rbac-cm (RBAC), argocd-cmd-params-cm (component command-line flags, including the crucial server.insecure), and argocd-secret (where the hashed admin password lives).


The three install methods, compared

There are three official ways to get Argo CD onto a cluster, and each is a genuinely different answer to “who owns the upgrades.” Choosing is not a matter of taste — it depends on whether you want to hand-edit YAML, run raw manifests, or drive everything from Helm values that Argo CD itself can later manage.

Method Command shape You control it by Upgrades Self-manage via GitOps? Reach for it when
Plain manifests kubectl apply -f .../install.yaml Editing ConfigMaps after the fact Re-apply a newer install.yaml Awkward (raw YAML, no values layer) Learning, dev clusters, fastest possible start
HA manifests kubectl apply -f .../ha/install.yaml Same, but the HA topology Re-apply a newer ha/install.yaml Awkward, same as above Production without Helm in your stack
Helm chart helm install argocd argo/argo-cd -f values.yaml A single values.yaml helm upgrade (or Argo CD syncs it) Yes — cleanest path Production; anywhere you already use Helm

The plain manifests live at a stable, versioned URL and are the shortest path to a running Argo CD:

# Non-HA, cluster-wide install (the classic quickstart)
kubectl create namespace argocd
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

The stable in that URL is a Git ref that always points at the latest stable release. Pin it in production — replace stable with a release tag so you know exactly what you deployed and upgrades are deliberate:

# Pinned to an exact version — reproducible, auditable
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/install.yaml

There are more manifest flavours than most people realise, and the right one saves you RBAC headaches:

Manifest path What you get Use when
manifests/install.yaml Full install, cluster-scoped RBAC (ClusterRoles) Argo CD manages apps across many namespaces/clusters
manifests/namespace-install.yaml Full install, namespace-scoped RBAC (Roles only) Argo CD may only manage its own namespace; least privilege
manifests/ha/install.yaml HA topology, cluster-scoped Production
manifests/ha/namespace-install.yaml HA topology, namespace-scoped Production + least privilege
manifests/core-install.yaml Argo CD Core — no API server, UI, SSO or RBAC A pure headless GitOps engine driven only by the CLI/kubectl

The Helm chart is the same components expressed as templated values, and it is the method that pays off long-term because a values.yaml is far easier to review, diff, and — critically — hand to Argo CD to manage itself (more on that at the end):

# Add the Argo project's Helm repo and install the chart
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
  --namespace argocd --create-namespace \
  --version 7.7.7 \
  -f values.yaml     # your overrides live here

Pin the chart version (--version) exactly as you pin the manifest tag. The chart version and the Argo CD app version it installs are different numbers — the chart’s appVersion tells you which Argo CD you actually get. Check with helm show chart argo/argo-cd | grep -E 'version|appVersion' before you install.

A minimal-but-real values.yaml for a production-ish Helm install looks like this — note that turning on HA is a single flag:

# values.yaml — a realistic starting point
global:
  domain: argocd.example.com        # used to build callback URLs, etc.

configs:
  params:
    server.insecure: true           # terminate TLS at the ingress, not in argocd-server

redis-ha:
  enabled: true                     # <-- this one flag switches on the HA Redis topology

controller:
  replicas: 1                       # raise + shard only when you manage many clusters
repoServer:
  replicas: 2
server:
  replicas: 2
applicationSet:
  replicas: 2

Non-HA vs HA: what changes, and when you actually need it

The default install is non-HA: one replica of everything and a single Redis pod. It is perfectly fine for learning, dev, and even small production if you can tolerate a minute of control-plane downtime during a node reboot. Crucially, non-HA Argo CD going down does not take your apps down — your workloads keep running; you just temporarily can’t sync new changes or see live status. That is a much gentler failure mode than most people fear, and it is why non-HA is a reasonable default far longer than you’d expect.

HA hardens the control plane against losing a node. Here is precisely what the HA manifests (or redis-ha.enabled: true in Helm) change:

Component Non-HA HA Why
Redis 1 × argocd-redis (Deployment) 3 × argocd-redis-ha (StatefulSet) + 3 × haproxy A single Redis is a cache SPOF; the controller stalls if it dies
argocd-server 1 replica 2 replicas Survive a node loss; spread UI/API load
argocd-repo-server 1 replica 2 replicas Manifest rendering is CPU-heavy; parallelism + redundancy
argocd-application-controller 1 (StatefulSet) 1 by default, shardable to N Shard clusters across replicas at scale
ApplicationSet / notifications / dex 1 each 1 each (leader-elected) Redundant replicas optional; leader election prevents double-runs

The Redis HA piece is the part that trips people up, so understand it before you deploy it. Three redis-ha pods form a Redis cluster with Sentinel for failover, and three HAProxy pods present a single stable endpoint that always routes to the current master. For the cluster to hold quorum, the three Redis pods must land on three different nodes — the manifests enforce this with requiredDuringScheduling pod anti-affinity. On a single-node cluster (or a two-node one), the third pod can never be scheduled and sits Pending forever. That is not a bug; it is HA doing its job. If you only have one node, install non-HA.

So when do you actually reach for HA? Match the signal, don’t cargo-cult it:

Signal Non-HA is fine Go HA
Cluster size 1–2 nodes ≥3 nodes across zones
Environment dev, staging, personal production you’re paged for
Blast radius one team, a few apps many teams, many apps/clusters
Tolerance for a brief control-plane outage yes no
Number of managed clusters a handful dozens (then also shard the controller)

Sharding the application-controller is the one HA knob unique to scale: when a single controller can’t keep up with dozens of target clusters, you run several controller replicas and split the clusters between them. You raise the StatefulSet replica count and set the matching env var so each replica knows the total:

# argocd-application-controller StatefulSet
spec:
  replicas: 3
  template:
    spec:
      containers:
        - name: argocd-application-controller
          env:
            - name: ARGOCD_CONTROLLER_REPLICAS
              value: "3"                    # must match spec.replicas
            - name: ARGOCD_CONTROLLER_SHARDING_ALGORITHM
              value: "round-robin"          # spread clusters evenly across shards

A rough, representative footprint so you can size a node pool (exact requests vary by version and your values.yaml):

Install Pods (typical) Fits on Notes
Non-HA ~7 a single 2 vCPU / 4 GiB node great for kind/minikube and dev
HA ~13–14 ≥3 nodes, ~2 vCPU / 8 GiB each Redis anti-affinity forces 3 nodes minimum

First login: password, CLI, and locking it down

You have pods running — now you need in. Argo CD generates a random admin password at install time and stashes it in a throwaway Secret named argocd-initial-admin-secret. The first-login sequence is always the same five moves:

Step Command What it does
1. Get the initial password kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d Prints the autogenerated admin password
1. (alternative) argocd admin initial-password -n argocd Same value, plus a reminder to delete the secret
2. Port-forward the server kubectl port-forward svc/argocd-server -n argocd 8080:443 Opens localhost:8080 → the UI/API, no LB needed
3. Log in with the CLI argocd login localhost:8080 --username admin --password <pw> --insecure Authenticates the CLI; --insecure skips the self-signed cert
4. Rotate the password argocd account update-password Prompts for current + new password
5. Delete the bootstrap secret kubectl -n argocd delete secret argocd-initial-admin-secret ⚠️ Removes the plaintext credential from the cluster

Fetching the password is a one-liner, but read the pieces: the value in the Secret is base64-encoded (as all Secret data is), so you pipe it through base64 -d to get the real string.

# Grab the initial admin password (base64-decoded)
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath='{.data.password}' | base64 -d ; echo
# Representative output:
# 8xKq-2mNvPtL0rZ9

⚠️ The admin’s real, hashed password lives in argocd-secret under admin.password (bcrypt). The argocd-initial-admin-secret is a convenience copy in plaintext meant only for the first login. Leaving it in the cluster is a standing credential-leak risk, which is exactly why Step 5 deletes it. After you’ve rotated the password, that secret has no further purpose.

Installing the CLI

The argocd CLI talks to argocd-server over gRPC and is how you’ll do almost everything scriptable. Install the version that matches (or is close to) your server:

OS Install command
macOS / Linux (Homebrew) brew install argocd
Linux (direct binary) curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64 && sudo install -m 555 argocd /usr/local/bin/argocd && rm argocd
Linux (ARM64) same URL but ...argocd-linux-arm64
Windows (PowerShell) Invoke-WebRequest -Uri https://github.com/argoproj/argo-cd/releases/latest/download/argocd-windows-amd64.exe -OutFile argocd.exe then put it on your PATH
# Confirm the CLI is installed and see client/server versions
argocd version --client
# argocd: v2.13.3+abc1234
#   BuildDate: ...

Once logged in, prove it end-to-end:

argocd account get-user-info
# Logged In: true
# Username: admin
# Issuer: argocd

Exposing the API and UI: port-forward → LoadBalancer → Ingress

A fresh argocd-server Service is a ClusterIP — reachable only from inside the cluster. There are exactly three ways to open it up, and they form a ladder: use the lowest rung that meets your need, because every rung up adds cost and moving parts.

Option How Who can reach it Cost Use for
kubectl port-forward kubectl port-forward svc/argocd-server -n argocd 8080:443 Just you, while the command runs Free Dev, first login, quick debugging
Service type=LoadBalancer Patch the Service to LoadBalancer Anyone who can route to the cloud LB IP 💲 an L4 cloud LB, per-hour A team, when you don’t need a hostname/cert yet
Ingress An Ingress + ingress controller (per cloud) Anyone, via DNS + TLS 💲 an L7 LB/App Gateway Real, shared, production access
# Rung 2: turn the ClusterIP into a cloud load balancer
kubectl patch svc argocd-server -n argocd \
  -p '{"spec": {"type": "LoadBalancer"}}'
# Watch for the external IP to appear (this is where a stuck <pending> shows up)
kubectl -n argocd get svc argocd-server -w

The one gotcha that catches everyone: TLS and gRPC

argocd-server serves its own TLS with a self-signed certificate by default, on port 443. That’s fine behind port-forward. But the instant you put an ingress or L7 load balancer in front — which also terminates TLS — you have TLS wrapped in TLS, and things break in confusing ways. The fix is to run the server in insecure mode (plain HTTP internally) and let the edge terminate the real TLS:

Setting Where Effect
server.insecure: "true" argocd-cmd-params-cm ConfigMap (or configs.params."server.insecure": true in Helm) argocd-server serves HTTP; the ingress owns TLS
Backend protocol = HTTP Ingress annotation (per cloud) Edge → server is plain HTTP; no double-wrap
argocd login --grpc-web CLI flag Makes the gRPC API tunnel over normal HTTP/1.1 so L7 LBs pass it

That last row is the other classic trap. The CLI speaks gRPC (HTTP/2). Many L7 load balancers — notably AWS ALB and Google’s GCLB — don’t pass raw gRPC to the backend cleanly alongside the HTTP web UI. The universal escape hatch is --grpc-web, which makes the CLI use gRPC-Web over ordinary HTTP so it survives any L7 hop:

# Logging in through an ALB / App Gateway / GCLB? Add --grpc-web
argocd login argocd.example.com --username admin --password <pw> --grpc-web

The multi-cloud edges: reaching the UI on AKS, EKS and GKE

Here is the core of the lesson. The install is identical on all three clouds; only the last hop differs — how you get a kubeconfig, and how you turn argocd-server into something a browser can open. Get the diagram below into your head and the rest is detail.

The flow reads left → right: you kubectl apply or helm install the same HA control plane into the argocd namespace, then expose argocd-server through that cloud’s load balancer or ingress — Application Gateway on AKS, an ALB on EKS, a Google Cloud Load Balancer on GKE — and only then does a real user reach the UI. The badges mark the decisions that bite: the install method (1), the first-login secret you must rotate then delete (2), Redis HA’s quorum requirement (3), terminating TLS exactly once (4), the port-forward → LoadBalancer → Ingress ladder (5), and the fact that every cloud edge bills until you delete it (6).

Left-to-right topology showing kubectl or Helm installing one HA Argo CD control plane — argocd-server, repo-server, application-controller and a 3-node Redis HA — into the argocd namespace, then exposing argocd-server through a per-cloud ingress or load balancer to reach the UI on AKS via Application Gateway, EKS via an ALB, and GKE via a Google Cloud Load Balancer

Step zero: get a kubeconfig

You can’t install anything until kubectl points at the right cluster. Each cloud has its own credential command, and each writes a context into your kubeconfig:

Cloud Get credentials Extra requirement
AKS az aks get-credentials --resource-group <rg> --name <cluster> az CLI, logged in with az login
EKS aws eks update-kubeconfig --region <region> --name <cluster> aws CLI v2, IAM creds with EKS access
GKE gcloud container clusters get-credentials <cluster> --region <region> gcloud + the gke-gcloud-auth-plugin
# AKS
az aks get-credentials --resource-group rg-gitops --name aks-prod --overwrite-existing

# EKS
aws eks update-kubeconfig --region ap-south-1 --name eks-prod

# GKE  (install the auth plugin first: gcloud components install gke-gcloud-auth-plugin)
gcloud container clusters get-credentials gke-prod --region asia-south1 --project my-proj
# Always confirm which cluster you're about to install into
kubectl config current-context
kubectl get nodes

⚠️ Running the wrong argocd/kubectl command against the wrong context is the single most common “how did that get installed there?” mistake. Check kubectl config current-context before every install and every teardown.

Before any Ingress can get an address, the cloud’s controller has to be present. This is the “why is my Service <pending>?” table — install these first:

Cloud Prerequisite for an L7 ingress Installed by default? Without it you get
AKS AGIC add-on (or managed NGINX / App Gateway for Containers) No — enable the add-on An Ingress with no address
EKS AWS Load Balancer Controller (needs IRSA / Pod Identity) No — you must install it Service/Ingress stuck <pending>
GKE GKE Ingress controller (gce) Yes — built in Works out of the box; add BackendConfig for gRPC

The per-cloud exposure matrix

This is the table to bookmark. It answers “what load balancer, what ingress class, what about DNS and certs, and what will trip me up” for all three clouds at once:

AKS (Azure) EKS (AWS) GKE (Google)
L4 LoadBalancer Service Azure Standard Load Balancer + public IP NLB (needs AWS Load Balancer Controller) Google L4 Network LB
Internal LB annotation service.beta.kubernetes.io/azure-load-balancer-internal: "true" service.beta.kubernetes.io/aws-load-balancer-scheme: "internal" networking.gke.io/load-balancer-type: "Internal"
L7 ingress → Application Gateway (AGIC) or managed NGINX ALB (AWS Load Balancer Controller) Google Cloud LB (GKE Ingress)
ingressClassName azure-application-gateway alb gce (or gce-internal)
TLS certificate from Key Vault / cert-manager ACM (certificate-arn annotation) Google-managed cert (ManagedCertificate CRD)
Static/public IP Public IP resource ALB-managed global-static-ip-name annotation
Biggest gotcha AGIC add-on must be enabled; App Gateway bills hourly LB Controller not installed by default → Service stuck <pending> GKE Ingress needs a BackendConfig/HTTP2 for gRPC; use --grpc-web
Modern alternative App Gateway for Containers Gateway API: gke-l7-global-external-managed

Now the concrete manifests. First, the simplest real exposure — a LoadBalancer Service — where the annotations are what differ per cloud:

# AKS — internal Azure Standard LB (no public exposure)
apiVersion: v1
kind: Service
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    service.beta.kubernetes.io/azure-load-balancer-internal: "true"
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: argocd-server
  ports:
    - port: 443
      targetPort: 8080
# EKS — internet-facing NLB via the AWS Load Balancer Controller
apiVersion: v1
kind: Service
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: argocd-server
  ports:
    - port: 443
      targetPort: 8080
# GKE — internal Google L4 Network LB
apiVersion: v1
kind: Service
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    networking.gke.io/load-balancer-type: "Internal"
spec:
  type: LoadBalancer
  selector:
    app.kubernetes.io/name: argocd-server
  ports:
    - port: 443
      targetPort: 8080

For a real, shared, HTTPS-with-a-hostname setup you use an Ingress. Remember the TLS rule from the previous section: set server.insecure: true first so the edge terminates TLS and the backend is plain HTTP. The Ingress object differs by class and annotations:

# AKS — Application Gateway Ingress Controller (AGIC)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    appgw.ingress.kubernetes.io/ssl-redirect: "true"
    appgw.ingress.kubernetes.io/backend-protocol: "http"   # server runs --insecure
spec:
  ingressClassName: azure-application-gateway
  rules:
    - host: argocd.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80
# EKS — AWS Load Balancer Controller → ALB, TLS via ACM
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    alb.ingress.kubernetes.io/scheme: internet-facing
    alb.ingress.kubernetes.io/target-type: ip
    alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
    alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-south-1:111122223333:certificate/abcd-1234
    alb.ingress.kubernetes.io/backend-protocol: HTTP        # server runs --insecure
spec:
  ingressClassName: alb
  rules:
    - host: argocd.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80
# GKE — Google Cloud Load Balancer via GKE Ingress + a Google-managed cert
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-server
  namespace: argocd
  annotations:
    kubernetes.io/ingress.global-static-ip-name: "argocd-ip"
    networking.gke.io/managed-certificates: "argocd-cert"
spec:
  ingressClassName: gce
  rules:
    - host: argocd.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80

⚠️ Every one of these edges costs money and keeps costing until you delete it. An Azure Application Gateway and public IP bill per hour, an AWS ALB/NLB bills per hour plus LCUs, and a GCLB bills per forwarding rule plus traffic — whether or not anyone is using Argo CD. On EKS and GKE especially, an Ingress you forgot about can quietly run up a bill for weeks. Delete the Ingress/Service type=LoadBalancer when you’re done (the teardown step does this), and prefer port-forward for anything short-lived.

Getting Argo CD to manage its own install (forward reference)

Here is the idea that makes the Helm path worth it. Once Argo CD is running, you can point it at the very Git repo that holds its own values.yaml, and create an Application that syncs Argo CD to itself. From then on, upgrading Argo CD is a pull request: bump the chart version in Git, and Argo CD rolls out its own upgrade. There is a genuine chicken-and-egg at the very start — you must install it once by hand to bootstrap — but after that, Argo CD becomes just another app under GitOps. You’ll build exactly this in Your First Application: source, destination and sync; for now, just know that the Helm chart is the method that makes self-management clean, because the entire install is one reviewable values.yaml.


Hands-on lab

You will install Argo CD, log in, rotate the password, expose it per-cloud, and tear it all down. There is no cluster on this machine, so the commands below are the exact ones to run against a real AKS/EKS/GKE cluster — or, for free, a local kind cluster (steps 1–7 work identically on kind; only step 8, the cloud LB, needs a real cloud). Every command is followed by the output you should see and a one-line “what just happened.”

For a zero-cost run-through, create a local cluster first: kind create cluster --name argocd-lab. Steps 1–7 are then completely free. Skip step 8 (no cloud LB on kind) and use port-forward.

Step 1 — Point kubectl at your cluster and create the namespace.

# Pick ONE cloud (or kind); confirm the context first
kubectl config current-context
# aks-prod        <- make sure this is the cluster you intend!
kubectl create namespace argocd
# namespace/argocd created

What just happened: you’ve confirmed the target cluster and created the namespace everything installs into. The context check is your seatbelt.

Step 2 — Install the HA manifests (or Helm).

# Option A — HA manifests, pinned to a version
kubectl apply -n argocd \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/ha/install.yaml
# customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io created
# serviceaccount/argocd-application-controller created
# ... (many resources) ...
# statefulset.apps/argocd-application-controller created
# deployment.apps/argocd-server created
# Option B — Helm, with HA turned on
helm repo add argo https://argoproj.github.io/argo-helm && helm repo update
helm install argocd argo/argo-cd -n argocd --create-namespace \
  --version 7.7.7 --set redis-ha.enabled=true \
  --set controller.replicas=1 --set server.replicas=2 --set repoServer.replicas=2
# NAME: argocd
# STATUS: deployed

What just happened: the CRDs, RBAC, ConfigMaps, and all the workloads landed in argocd. Nothing is reachable yet — these are ClusterIP Services.

Step 3 — Wait for the pods to come up.

# Give the rollout a couple of minutes; images have to pull
kubectl -n argocd rollout status deploy/argocd-server --timeout=300s
# deployment "argocd-server" successfully rolled out
kubectl -n argocd get pods
# NAME                                              READY   STATUS    RESTARTS   AGE
# argocd-application-controller-0                   1/1     Running   0          2m
# argocd-applicationset-controller-7c...            1/1     Running   0          2m
# argocd-dex-server-6f...                           1/1     Running   0          2m
# argocd-notifications-controller-5d...             1/1     Running   0          2m
# argocd-redis-ha-haproxy-0/1/2                     1/1     Running   0          2m
# argocd-redis-ha-server-0/1/2                      3/3     Running   0          2m
# argocd-repo-server-8b...  (×2)                    1/1     Running   0          2m
# argocd-server-9a...       (×2)                    1/1     Running   0          2m

What just happened: you can now read the topology you learned — two servers, two repo-servers, three redis-ha + three haproxy. If any Redis pod is Pending, your cluster has fewer than 3 schedulable nodes (see troubleshooting).

Step 4 — Fetch the initial admin password.

kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath='{.data.password}' | base64 -d ; echo
# 8xKq-2mNvPtL0rZ9

What just happened: Argo CD generated a random admin password at install and stored it (base64) in a throwaway secret. Copy it.

Step 5 — Port-forward and log in with the CLI.

# In one terminal — leave it running
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Forwarding from 127.0.0.1:8080 -> 8080

# In another terminal
argocd login localhost:8080 --username admin --password '8xKq-2mNvPtL0rZ9' --insecure
# 'admin:login' logged in successfully
# Context 'localhost:8080' updated

What just happened: port-forward tunnels localhost:8080 to argocd-server, and the CLI authenticated. --insecure accepts the server’s self-signed cert. You can now open https://localhost:8080 in a browser too.

Step 6 — Rotate the password and delete the bootstrap secret.

argocd account update-password
# *** Enter password of currently logged in user (admin): <old>
# *** Enter new password for user admin: <new>
# *** Confirm new password for user admin: <new>
# Password updated
# Context 'localhost:8080' updated

kubectl -n argocd delete secret argocd-initial-admin-secret
# secret "argocd-initial-admin-secret" deleted

What just happened: the real (bcrypt) password in argocd-secret is now yours, and the plaintext bootstrap copy is gone — one fewer credential to leak.

Step 7 — Verify you’re genuinely in.

argocd account get-user-info
# Logged In: true
# Username: admin
argocd app list
# NAME  CLUSTER  NAMESPACE  PROJECT  STATUS  HEALTH  ...
# (empty — no apps yet, which is correct)

What just happened: an empty app list is success — Argo CD is installed, authenticated, and simply has nothing to sync yet.

Step 8 — Expose it for real (per cloud). ⚠️ This step creates a billable load balancer. Pick your cloud:

# AKS — internal Azure LB
kubectl annotate svc argocd-server -n argocd \
  service.beta.kubernetes.io/azure-load-balancer-internal="true" --overwrite
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"LoadBalancer"}}'

# EKS — internet-facing NLB (requires AWS Load Balancer Controller installed)
kubectl annotate svc argocd-server -n argocd \
  service.beta.kubernetes.io/aws-load-balancer-type="external" \
  service.beta.kubernetes.io/aws-load-balancer-scheme="internet-facing" --overwrite
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"LoadBalancer"}}'

# GKE — internal Google L4 LB
kubectl annotate svc argocd-server -n argocd \
  networking.gke.io/load-balancer-type="Internal" --overwrite
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"LoadBalancer"}}'
# Watch the external address appear (all clouds)
kubectl -n argocd get svc argocd-server -w
# NAME            TYPE           EXTERNAL-IP      PORT(S)
# argocd-server   LoadBalancer   <pending>        443:31234/TCP     <- provisioning...
# argocd-server   LoadBalancer   20.55.1.42       443:31234/TCP     <- ready

What just happened: the cloud provisioned an L4 load balancer and gave the Service an external address. A <pending> that never resolves means no LB controller is wired up (very common on EKS — see troubleshooting).

Step 9 — Teardown (do this, or it bills).

# 1) Remove the billable edge FIRST
kubectl -n argocd delete ingress --all             # if you created any Ingress
kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"ClusterIP"}}'  # release the LB

# 2) Remove Argo CD itself
helm uninstall argocd -n argocd     # if you used Helm
# OR, if you used manifests:
kubectl delete -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/ha/install.yaml

# 3) Delete the namespace (also removes any leftover LB Services)
kubectl delete namespace argocd
# namespace "argocd" deleted

# 4) If you used kind
kind delete cluster --name argocd-lab

What just happened: deleting the LoadBalancer/Ingress releases the cloud LB so it stops billing; deleting the namespace clears the rest. Confirm in your cloud console that the LB is actually gone.


Common mistakes and troubleshooting

Real states and messages you’ll see, and what they actually mean.

Symptom Likely cause Fix
Pods stuck Pending Not enough CPU/mem, or no default StorageClass for Redis HA’s PVCs kubectl -n argocd describe pod <p> — read the Events; add nodes or a StorageClass
One argocd-redis-ha-server pod Pending on HA requiredDuringScheduling pod anti-affinity needs 3 distinct nodes Add nodes (≥3), or install non-HA on small clusters
argocd-initial-admin-secret not found Already rotated/deleted, or you used Helm with a custom admin config Reset via argocd-secret (admin.password bcrypt) or argocd admin initial-password if still present
argocd loginx509: certificate signed by unknown authority Server’s self-signed TLS cert not trusted Add --insecure (dev) or trust the cert; behind an LB use the real hostname
argocd loginrpc error: code = Unavailable through an ALB/GCLB L7 LB won’t pass raw gRPC Add --grpc-web to the login command
Service stuck EXTERNAL-IP: <pending> on EKS AWS Load Balancer Controller not installed Install the controller (IRSA + Helm); until then no LB is provisioned
Ingress created but no address (AKS/GKE) AGIC add-on disabled / GKE Ingress needs a BackendConfig or health check Enable AGIC; add BackendConfig + readiness on GKE; check kubectl describe ingress
UI loads but shows a redirect loop / too many redirects Double TLS — server does its own TLS and the ingress terminates TLS Set server.insecure: "true" in argocd-cmd-params-cm, restart argocd-server
CRDs not established / no matches for kind "Application" You applied a namespace-install without CRDs, or applied out of order Apply the full install.yaml; CRDs ship with it — re-apply
Helm install ignores your setting Overriding the wrong key (chart keys are nested, e.g. configs.params."server.insecure") helm get values argocd -n argocd to see what actually applied; fix the path

Three gotchas deserve extra words because they eat the most hours:

1. The Redis-HA quorum trap. By far the most common “why won’t it install?” is dropping the HA manifests onto a one- or two-node cluster and finding a Redis pod stuck Pending. The HA topology requires three nodes because Redis needs quorum and the pods have hard anti-affinity. This is correct behaviour — HA that tolerates a node loss cannot pack all its replicas onto one node. On anything smaller than three nodes, install non-HA; you lose control-plane redundancy, not your apps.

2. TLS wrapped in TLS. argocd-server terminating its own TLS is great for port-forward and terrible behind an ingress. The symptoms are maddening — redirect loops, ERR_TOO_MANY_REDIRECTS, gRPC errors — but they all trace to two things trying to own TLS. The moment you add an ingress or L7 LB, set server.insecure: "true" so the server speaks plain HTTP internally and the edge owns the certificate. This isn’t “less secure” — TLS still terminates at the edge, exactly once.

3. The load balancer you forgot to delete. Cloud LBs bill whether or not they carry traffic, and an orphaned ALB or forwarding rule can survive even a namespace delete if its controller lost track of it. Tear the edge down first, then confirm in the cloud console that the LB, public IP, or App Gateway is actually gone.


Cheat-sheet

Install, log in, expose — the commands you’ll reach for constantly.

Task Command
Non-HA install (pinned) kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/install.yaml
HA install kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/ha/install.yaml
Helm install (HA) helm install argocd argo/argo-cd -n argocd --create-namespace --set redis-ha.enabled=true
Initial admin password kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d
Port-forward the UI/API kubectl port-forward svc/argocd-server -n argocd 8080:443
CLI login (port-forward) argocd login localhost:8080 --username admin --password <pw> --insecure
CLI login (through an L7 LB) argocd login <host> --username admin --password <pw> --grpc-web
Rotate password argocd account update-password
Delete bootstrap secret kubectl -n argocd delete secret argocd-initial-admin-secret
Expose via cloud LB kubectl patch svc argocd-server -n argocd -p '{"spec":{"type":"LoadBalancer"}}'
Set server insecure (behind ingress) kubectl -n argocd patch cm argocd-cmd-params-cm --type merge -p '{"data":{"server.insecure":"true"}}'
AKS kubeconfig az aks get-credentials -g <rg> -n <cluster>
EKS kubeconfig aws eks update-kubeconfig --region <r> --name <cluster>
GKE kubeconfig gcloud container clusters get-credentials <cluster> --region <r>
Teardown kubectl delete ns argocd (after removing any LB/Ingress)

Key config objects you’ll edit after install:

Object Kind What it holds
argocd-cm ConfigMap Main config: repos, url, SSO/dex, resource customizations
argocd-rbac-cm ConfigMap RBAC policy (roles, group→role mappings)
argocd-cmd-params-cm ConfigMap Component flags — server.insecure, log level, timeouts
argocd-secret Secret Hashed admin password (admin.password), server TLS, signing keys
argocd-initial-admin-secret Secret Plaintext bootstrap password — delete after first login

Interview and exam questions

Q: What are the three ways to install Argo CD, and when would you pick each? A: (1) Plain manifests (install.yaml) — fastest, best for learning/dev; (2) HA manifests (ha/install.yaml) — same components in a redundant topology, for production without Helm; (3) the Helm chart (argo/argo-cd) — values-driven, easiest to upgrade and to let Argo CD self-manage via GitOps. Pick manifests to learn, HA manifests for prod-without-Helm, and Helm anywhere you already use Helm.

Q: What does HA actually change over the default install? A: It replaces the single Redis with a 3-node Redis HA cluster (plus 3 HAProxy pods), and runs multiple replicas of argocd-server and argocd-repo-server. The application-controller stays at one replica unless you shard it. The point is surviving a node loss without a control-plane outage.

Q: Why might a Redis pod be stuck Pending right after an HA install? A: The HA Redis pods have required pod anti-affinity and need three distinct nodes for quorum. On a one- or two-node cluster the third can never schedule. Fix: use ≥3 nodes, or install non-HA on small clusters.

Q: If Argo CD’s control plane goes down, do your applications go down? A: No. Argo CD is a reconcile controller, not a data-plane proxy. Your workloads keep running; you temporarily lose the ability to sync changes and see live status until it recovers. That’s why non-HA is acceptable far longer than people assume.

Q: How do you get the first admin password, and what should you do right after logging in? A: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d. Then log in, run argocd account update-password, and delete argocd-initial-admin-secret so the plaintext bootstrap credential no longer sits in the cluster.

Q: You put Argo CD behind an ingress and now the UI redirect-loops. Why? A: Double TLS — argocd-server terminates its own TLS and so does the ingress. Set server.insecure: "true" in argocd-cmd-params-cm so the server serves HTTP internally and the ingress owns the certificate. TLS still terminates once, at the edge.

Q: The argocd CLI can’t log in through an AWS ALB — what’s the fix? A: L7 LBs like ALB and GCLB don’t cleanly pass raw gRPC to the backend. Add --grpc-web to argocd login so the CLI tunnels gRPC over ordinary HTTP.

Q: On EKS, kubectl get svc argocd-server shows EXTERNAL-IP: <pending> and never resolves. Diagnose it. A: There’s no load-balancer controller reconciling the Service. EKS doesn’t install the AWS Load Balancer Controller by default; install it (with IRSA/Pod Identity and its Helm chart). Until then no NLB/ALB is provisioned.

Q: Compare how you expose the Argo CD UI on AKS, EKS and GKE. A: AKS — Application Gateway via AGIC (ingressClassName: azure-application-gateway) or an Azure Standard LB. EKS — the AWS Load Balancer Controller creating an ALB (ingressClassName: alb, ACM cert) or an NLB. GKE — GKE Ingress creating a Google Cloud LB (ingressClassName: gce, a ManagedCertificate), or the Gateway API gke-l7-* classes. Each also has an internal-LB annotation.

Q: What’s the difference between install.yaml and namespace-install.yaml? A: install.yaml grants cluster-scoped RBAC (ClusterRoles) so Argo CD can manage resources across namespaces/clusters. namespace-install.yaml uses namespace-scoped Roles so Argo CD can only manage its own namespace — least privilege for single-tenant setups.

Q: How does Argo CD end up managing its own installation? A: You bootstrap it once by hand, then commit its Helm values.yaml (or manifests) to Git and create an Application that points Argo CD at itself. Upgrades then become pull requests. There’s an unavoidable chicken-and-egg for the very first install, but after that Argo CD is just another GitOps-managed app.

Q: Why pin the manifest tag or chart version instead of using stable/latest? A: stable and latest move under you, so two installs weeks apart can differ, and upgrades happen by accident. Pinning a tag/version makes installs reproducible and upgrades a deliberate, reviewable change.


Key takeaways

argocdgitopskubernetesakseksgkehelmhigh-availabilityinstallationkubectlingressload-balancerargocd-cli
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments