Module 5: Gitops With Argocd
4. Hands-on: installing ArgoCD
Description
This lesson installs the complete "thermostat": ArgoCD, running inside andes-cargo-cluster, with the project's official manifest — the same kind of single-kubectl apply install you already used with OPA Gatekeeper... except this time that single command fails, with a real error no earlier guide in this ecosystem ran into: an annotation too large for the limit Kubernetes puts on any object. This lesson documents the full error, the single-flag fix that resolves it, and finishes with argocd version confirming the same version — v3.5.1 — on both the client you install on your machine and the server running inside the cluster.
Connection to the module
This lesson is lesson 3's counterpart: there you installed the "notebook" (Gitea); here you install whoever reads it. Lesson 5 explains the anatomy of the Application resource you're going to use to connect both pieces for the first time in lesson 6.
Step 1 — Install the argocd CLI on your machine
Everything that follows in this lesson runs against the cluster (kubectl apply), but you're also going to need the argocd CLI on your own machine for the rest of the module — argocd login, argocd app get, argocd app sync. On macOS, with Homebrew:
brew install argocd
argocd version --client
What to expect (literal, executed — GoVersion, BuildDate, and Platform depend on your system and on when the binary you installed was published):
argocd: v3.5.1+109ca7c.dirty
BuildDate: 2026-08-12T14:32:28Z
GitCommit: 109ca7ca71139e514114499d294a492e7910a965
GitTreeState: dirty
GitTag: v3.5.1
GoVersion: go1.26.5
Compiler: gc
Platform: darwin/arm64
v3.5.1 — confirm you installed a version equal to or newer than the one this module documents before continuing. If your operating system isn't macOS, the official installation page documents the equivalent binary for Linux and Windows.
Step 2 — The official manifest, and the real error a plain kubectl apply produces
The ArgoCD project publishes a complete installation manifest, ready to apply against any Kubernetes cluster:
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
What happened, literal, the first time this command ran while writing this lesson — dozens of resources get created without issue, and the command finishes with an error:
namespace/argocd created
configmap/argocd-gpg-keys-cm created
configmap/argocd-notifications-cm created
configmap/argocd-rbac-cm created
...
networkpolicy.networking.k8s.io/argocd-server-network-policy created
The CustomResourceDefinition "applicationsets.argoproj.io" is invalid: metadata.annotations: Too long: may not be more than 262144 bytes
The message is precise, and the cause has nothing to do with ArgoCD itself: kubectl apply (unlike kubectl create) saves, by default, a complete copy of the applied manifest in every object's kubectl.kubernetes.io/last-applied-configuration annotation — the mechanism that lets it compute diffs on the next apply. The applicationsets.argoproj.io CRD is, by itself, a huge YAML document (it defines a complete validation schema for the ApplicationSet resource), and that full copy, encoded inside a single annotation, exceeds the hard 262144-byte (256 KiB) limit Kubernetes imposes on any annotation of any object — an etcd limit, not specific to ArgoCD.
Step 3 — The fix: --server-side
The solution isn't splitting the manifest or shrinking it — it's changing the apply mode. kubectl apply --server-side uses Server-Side Apply, a Kubernetes feature where the kube-apiserver itself computes field diffs, instead of the client (kubectl) sending it a full copy of the manifest as an annotation. Without that full copy embedded as an annotation, the 262144-byte limit stops being a problem:
kubectl apply -n argocd --server-side --force-conflicts \
-f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
What to expect (literal, executed — complete, with no error this time):
customresourcedefinition.apiextensions.k8s.io/applications.argoproj.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/applicationsets.argoproj.io serverside-applied
customresourcedefinition.apiextensions.k8s.io/appprojects.argoproj.io serverside-applied
serviceaccount/argocd-application-controller serverside-applied
serviceaccount/argocd-applicationset-controller serverside-applied
serviceaccount/argocd-dex-server serverside-applied
serviceaccount/argocd-notifications-controller serverside-applied
serviceaccount/argocd-redis serverside-applied
serviceaccount/argocd-repo-server serverside-applied
serviceaccount/argocd-server serverside-applied
role.rbac.authorization.k8s.io/argocd-application-controller serverside-applied
role.rbac.authorization.k8s.io/argocd-applicationset-controller serverside-applied
role.rbac.authorization.k8s.io/argocd-dex-server serverside-applied
role.rbac.authorization.k8s.io/argocd-notifications-controller serverside-applied
role.rbac.authorization.k8s.io/argocd-redis serverside-applied
role.rbac.authorization.k8s.io/argocd-server serverside-applied
clusterrole.rbac.authorization.k8s.io/argocd-application-controller serverside-applied
clusterrole.rbac.authorization.k8s.io/argocd-applicationset-controller serverside-applied
clusterrole.rbac.authorization.k8s.io/argocd-server serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-application-controller serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-applicationset-controller serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-dex-server serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-notifications-controller serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-redis serverside-applied
rolebinding.rbac.authorization.k8s.io/argocd-server serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/argocd-application-controller serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/argocd-applicationset-controller serverside-applied
clusterrolebinding.rbac.authorization.k8s.io/argocd-server serverside-applied
configmap/argocd-cm serverside-applied
configmap/argocd-cmd-params-cm serverside-applied
configmap/argocd-gpg-keys-cm serverside-applied
configmap/argocd-notifications-cm serverside-applied
configmap/argocd-rbac-cm serverside-applied
configmap/argocd-ssh-known-hosts-cm serverside-applied
configmap/argocd-tls-certs-cm serverside-applied
secret/argocd-notifications-secret serverside-applied
secret/argocd-secret serverside-applied
service/argocd-applicationset-controller serverside-applied
service/argocd-dex-server serverside-applied
service/argocd-metrics serverside-applied
service/argocd-notifications-controller-metrics serverside-applied
service/argocd-redis serverside-applied
service/argocd-repo-server serverside-applied
service/argocd-server serverside-applied
service/argocd-server-metrics serverside-applied
deployment.apps/argocd-applicationset-controller serverside-applied
deployment.apps/argocd-dex-server serverside-applied
deployment.apps/argocd-notifications-controller serverside-applied
deployment.apps/argocd-redis serverside-applied
deployment.apps/argocd-repo-server serverside-applied
deployment.apps/argocd-server serverside-applied
statefulset.apps/argocd-application-controller serverside-applied
networkpolicy.networking.k8s.io/argocd-application-controller-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-applicationset-controller-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-dex-server-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-notifications-controller-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-redis-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-repo-server-network-policy serverside-applied
networkpolicy.networking.k8s.io/argocd-server-network-policy serverside-applied
--force-conflicts is necessary because, if you ran Step 2's failed attempt first, some objects already have a "field manager" assigned by the plain kubectl apply — the flag tells Server-Side Apply to take ownership of those fields anyway, instead of rejecting the change over a conflict. Notice something interesting about the manifest itself: it already ships its own NetworkPolicy objects for each ArgoCD component (argocd-server-network-policy, argocd-repo-server-network-policy, etc.) — the same mechanism you built by hand in Module 4, here applied by the project itself to isolate its own internal components from each other.
Step 4 — Wait for all the Pods to boot
kubectl wait --for=condition=Available deployment --all -n argocd --timeout=180s
kubectl get pods -n argocd
What to expect (literal, executed — each Pod's hash suffix is your variable value; the base names and the count of seven Pods are literal):
deployment.apps/argocd-applicationset-controller condition met
deployment.apps/argocd-dex-server condition met
deployment.apps/argocd-notifications-controller condition met
deployment.apps/argocd-redis condition met
deployment.apps/argocd-repo-server condition met
deployment.apps/argocd-server condition met
NAME READY STATUS RESTARTS AGE
argocd-application-controller-0 1/1 Running 0 43s
argocd-applicationset-controller-579c4c54b8-6rjfl 1/1 Running 0 43s
argocd-dex-server-744c5d4467-6mvzj 1/1 Running 0 43s
argocd-notifications-controller-dd4ff84c-t5sdn 1/1 Running 0 43s
argocd-redis-84497fb7c5-9ll4x 1/1 Running 0 43s
argocd-repo-server-5f46d9f598-5fqmj 1/1 Running 0 43s
argocd-server-7f4549bb69-nnxl5 1/1 Running 0 43s
Notice that argocd-application-controller runs as a StatefulSet (the -0 instead of a hash suffix) — it's the only one of the seven components with its own state to preserve (the comparison cache between Git and the cluster), unlike the other six, which are stateless Deployments.
Step 5 — The initial password, with no tunnel needed
ArgoCD generates a random admin password on its first boot and stores it as a Kubernetes Secret — never in plain text in any manifest. The CLI reads it directly via the Kubernetes API, without even needing the web UI to be reachable yet:
argocd admin initial-password -n argocd --core
What to expect (literal, executed — the generated password is your variable value, different on every installation):
f0gLUztVzFzjD82q
This password must be only used for first time login. We strongly recommend you update the password using `argocd account update-password`.
The --core flag is what makes this step possible with no port-forward at all: it tells the CLI to talk directly to the Kubernetes API (using your ~/.kube/config, the same one kubectl uses) instead of ArgoCD's HTTP server. It's the same mechanism, in spirit, as kubectl get secret ... -o jsonpath — an alternate way of reaching the same Secret.
Step 6 — Access the UI via port-forward, and confirm the full version
kubectl port-forward -n argocd svc/argocd-server 8080:443
In another terminal:
argocd login localhost:8080 --username admin --password 'f0gLUztVzFzjD82q' --insecure
What to expect (literal, executed):
'admin:login' logged in successfully
Context 'localhost:8080' updated
--insecure is necessary because the TLS certificate argocd-server generates for itself at install time is self-signed — the same kind of certificate you already saw with Gatekeeper's admission webhooks, named in this ecosystem's design — there's no real certificate authority behind it in this local lab. In a real EKS (Module 7), this step gets replaced by a valid certificate, normally issued by cert-manager against a real domain.
argocd version
What to expect (literal, executed — confirms client and server match on the same version):
argocd: v3.5.1+109ca7c.dirty
BuildDate: 2026-08-12T14:32:28Z
GitCommit: 109ca7ca71139e514114499d294a492e7910a965
GitTreeState: dirty
GitTag: v3.5.1
GoVersion: go1.26.5
Compiler: gc
Platform: darwin/arm64
argocd-server: v3.5.1
BuildDate: 2026-08-12T11:28:06Z
GitCommit: 109ca7ca71139e514114499d294a492e7910a965
GitTreeState: clean
GitTag: v3.5.1
GoVersion: go1.26.4
Compiler: gc
Platform: linux/arm64
Kustomize Version: v5.8.1 2026-02-09T16:15:27Z
Helm Version: v4.2.1+gd591a19
Kubectl Version: v0.36.1
Jsonnet Version: v0.22.0
argocd-server: v3.5.1 — the version running inside the cluster, on linux/arm64 (the kind node's architecture) — versus argocd: v3.5.1 — the CLI you installed on your darwin/arm64 (or your own machine's platform). Both matching on v3.5.1 isn't a coincidence of this lab: it's a real best practice — a CLI much newer or older than the server can have commands or output formats that don't line up — and the official manifest you applied in Step 3 always installs the latest stable version, the same one brew install argocd gave you in Step 1.
It also brings, as a bonus, something you're going to need in lessons 5 and 8: argocd-server embeds its own copy of Kustomize (v5.8.1) and Helm (v4.2.1) — ArgoCD can sync applications that use either of the two YAML-composition tools, not just plain manifests like andes-cargo-k8s's. This guide uses plain manifests on purpose, for pedagogical simplicity, but it's worth knowing the option exists.
Visual summary: both halves, now installed
andes-cargo-cluster (kind)
namespace: gitea namespace: argocd
┌─────────────────────┐ ┌──────────────────────────┐
│ gitea (Pod) │ │ argocd-server │
│ └─ andes-cargo/ │◄───────────│ argocd-repo-server │
│ andes-cargo-k8s │ (not yet │ argocd-application-controller│
│ (commit 45b14f6) │ connected)│ argocd-applicationset- │
└─────────────────────┘ │ controller │
│ argocd-dex-server │
namespace: andes-cargo │ argocd-redis │
┌─────────────────────┐ │ argocd-notifications- │
│ andes-cargo-status-api│ │ controller │
│ (3 replicas, M1-M4) │ └──────────────────────────┘
└─────────────────────┘
All three pieces exist. None of them know about the other two yet —
that's exactly what lesson 6 connects.
Common mistakes
Running kubectl apply without --server-side and not recognizing the error as an "annotation size" one (this lesson's real error). What happens: someone sees Too long: may not be more than 262144 bytes and assumes the manifest is corrupted or they downloaded an incomplete file. How to spot it: the exact message mentions metadata.annotations — it's an annotation size limit, not a YAML syntax or network issue. How to fix it: add --server-side --force-conflicts to the same command, as Step 3 did — there's no need to split the manifest or download it again.
Using argocd login without --insecure against this lab (expectation, TLS). What happens: the CLI rejects the connection with an untrusted-certificate error. How to spot it: the message mentions certificate signed by unknown authority or similar. How to fix it: --insecure is correct and expected against this local lab's self-signed certificate — in a real EKS with a valid certificate in place, the flag would stop being necessary (and stop being recommended).
Confusing argocd admin initial-password's password with a permanent password (credential hygiene, the CLI itself already warns about it). What happens: someone keeps using the auto-generated password indefinitely, even on a cluster that's going to live beyond this lab. How to spot it: Step 5's own message says it explicitly: "This password must be only used for first time login." How to fix it: for this learning lab, there's no need to change it — the cluster is ephemeral and local — in any real installation, the next command after the first login would be argocd account update-password.
Exercises
Exercise 1 — Explain the --server-side error to a colleague who never saw it. In two or three sentences, without copying this lesson's text, explain why a plain kubectl apply fails on the ApplicationSet CRD and why --server-side fixes it.
See solution
A reasonable explanation: "A plain kubectl apply saves a complete copy of the manifest you just applied inside an annotation on the object itself, so it can compute diffs the next time. The ApplicationSet CRD is such a huge YAML document that this full copy exceeds the 256 KiB limit Kubernetes puts on any annotation. --server-side changes the mechanism: instead of the client sending a full copy, the Kubernetes server computes the field diffs directly — without that full copy embedded, the size limit stops being a problem."
Exercise 2 — Confirm you understand what --core is. Without re-reading Step 5, explain the difference between argocd admin initial-password -n argocd --core and argocd login — why doesn't the first one need any active port-forward?
See solution
argocd admin initial-password --core talks directly to the Kubernetes API (the same ~/.kube/config kubectl uses), reading the argocd-initial-admin-secret Secret like any other Kubernetes object — it doesn't need ArgoCD's HTTP server (argocd-server) to be reachable from your machine at all. argocd login, on the other hand, does talk to ArgoCD's HTTP/gRPC server (argocd-server) — that's why it needs Step 6's port-forward, or any other network path that reaches that Service.
Exercise 3 — Predict what version argocd version would report if you only updated your machine's CLI. If you ran brew upgrade argocd tomorrow and the project had already published v3.6.0, without touching anything inside the cluster, what would argocd version show?
See solution
It would show argocd: v3.6.0 (the CLI updated on your machine) alongside argocd-server: v3.5.1 (the version still running inside the cluster, unchanged, because brew upgrade only touches the local binary). It would be a client/server version mismatch — the exact kind of situation Step 6's note warns you to avoid in a real environment, though the CLI would probably keep working for this module's basic operations.
Summary and next step
This lesson installed complete ArgoCD v3.5.1 inside andes-cargo-cluster, with the project's official manifest — and documented, with the literal error, why a plain kubectl apply fails against the ApplicationSet CRD (the 256 KiB limit on any Kubernetes annotation) and why --server-side --force-conflicts fixes it without splitting any manifest. You confirmed UI access via port-forward, got the initial password with no tunnel needed (--core), and verified that client and server match on the same version.
Before moving on you should be able to: explain the annotation-size error unassisted; distinguish --core from a normal login against the HTTP server; and confirm, on your own terminal, that argocd version reports v3.5.1 on both sides.
Next lesson: anatomy of an ArgoCD Application. There you learn, in detail, the one resource still missing to connect the two pieces you installed in this lesson and the previous one — source, destination, and syncPolicy, with the exact difference between manual and automated sync.
Resources
- Argo CD — Getting Started — official installation documentation, the source for the manifest used in this lesson.
- Argo CD — CLI Installation — CLI installation for different operating systems.
- Kubernetes — Server-Side Apply — official documentation for the mechanism that fixes this lesson's Step 2 error.
- Kubernetes — Annotations — the
256 KiBper-annotation size limit, cited in this lesson's error.