Cheat notes · Argo CD
Argo CD: cheat notes
Every module on one page, cut to what you need to remember: the idea, the commands, the pitfalls and a few questions to test yourself. Made for review after you have done the course, not instead of it. Back to the course.
00 Orientation: what Argo CD is and where it fits
This module only · Full module
The idea
Argo CD is a controller inside the cluster that pulls the desired state from Git and keeps the cluster equal to it. CI builds and tests; it never touches the cluster.
Remember
- Pull, not push: Argo CD reads Git with read-only access. No pipeline holds cluster credentials, and every running object has a commit behind it.
- Reconcile, not apply once: a CI job running
kubectl applydeploys and forgets. Argo CD keeps comparing, shows drift and can correct it. - Three repos, three jobs:
argocd-platform(platform team: Argo CD, Applications, projects),checkout-app(team-checkout: chart and code, released as tags),checkout-config(team-checkout: what runs in each environment). - The environment repo decides what runs where. The app repo produces releases; a change in
checkout-configis a deploy. - The layers: code and build (product team), desired state in Git, delivery (Argo CD), guardrails (AppProjects, RBAC, platform-owned namespaces), runtime (Kubernetes).
- The stand-in product:
checkoutruns the public podinfo image, so the course needs no code and no CI.
Pitfalls
- Fixing things with
kubectl edit"just this once". Argo CD undoes it and nobody knows it happened. Changing Git, even when kubectl is quicker, is the habit the course builds. - Treating
checkout-configas the product's code. It holds only the Kubernetes files that say how the product runs.
Test yourself
Answer in your head first, then open the answer.
What does Argo CD do that a CI job running kubectl apply does not?
It keeps comparing the cluster with Git after the deploy, shows drift and can correct it.
Why is pull-based delivery safer?
The credentials that can change production stay inside the cluster. Argo CD only needs read access to Git.
Which repository says which version runs in prod?
The environment repository, checkout-config. The app repo only produces releases.
Where module 00 leaves you: Nothing is installed in a cluster yet. You have your details saved in the course, every tool checked, a local copy of the reference repo to copy from, and a clear picture of what you will build.
01 Install Argo CD the reviewable way
This module only · Full module
The idea
Install Argo CD the way you will run it: a pinned Helm chart, its settings in a values file in Git, nothing changed by hand. Argo CD is a few controllers plus new object types (CRDs) in your cluster.
Remember
- Components:
argocd-server(UI and API),argocd-repo-server(clones Git, renders Helm and Kustomize),argocd-application-controller(compares and syncs), Redis (cache), plus the ApplicationSet and notifications controllers. - CRDs:
Application(what to deploy, from where, to where),AppProject(the fence around Applications),ApplicationSet(generates Applications). - Values file in Git:
argocd/values.yamlholds Argo CD's settings. The chart renders them into ConfigMaps such asargocd-cm. - Pinned chart version: the version is part of the command (later of a manifest), so installs repeat exactly and upgrades are deliberate.
admin: the install creates it with a random password inargocd-initial-admin-secret. Fine to start; module 06 replaces it with personal accounts.- The port-forward: until module 06 the UI and the CLI only work while
kubectl -n argocd port-forward svc/argocd-server 8080:443runs. The CLI login lasts 24 hours.
Pitfalls
- + New App in the UI creates an Application that exists nowhere in Git. Use the UI to watch; change things through Git.
- A hand edit of Argo CD's ConfigMap makes the next
helm upgradefail withconflict ... with "kubectl-patch": Kubernetes records each field's manager. Put the value invalues.yaml, then take the field back once with--force-conflicts. connection refusedfromargocdmeans the port-forward stopped.Unauthenticatedmeans the login expired: log in again.
Commands
helm install argocd argo/argo-cd --version 10.10.5 -n argocd --create-namespace -f argocd/values.yaml --wait | Install Argo CD: this chart, this version, your values |
kubectl get crd | grep argoproj.io | The three object types Argo CD adds |
kubectl -n argocd get deployments,statefulsets | The components. First list to check when Argo CD misbehaves |
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d | The initial admin password |
kubectl -n argocd port-forward svc/argocd-server 8080:443 | Reach the UI and API. Keep it running in its own terminal |
argocd login localhost:8080 --insecure --username admin --password ... | Log the CLI in |
argocd cluster list | Clusters Argo CD can deploy to (in-cluster) |
argocd proj get default | The default project: any repo, any destination |
The values file
global:
domain: argocd.localtest.me
configs:
cm:
timeout.reconciliation: 60s # check Git every minute
dex:
enabled: false # no SSO on a laptop
Test yourself
Answer in your head first, then open the answer.
Which component clones Git and renders Helm charts?
argocd-repo-server. The application controller compares and syncs; the server serves the UI and API.
Why install chart 10.10.5 instead of the latest?
A repeatable, reviewable install: the version is in Git, and upgrades become deliberate changes.
Where does the admin password come from?
The install generates it into the argocd-initial-admin-secret Secret.
helm upgrade fails with a conflict on .data.timeout.reconciliation. What happened?
Someone changed that field by hand. Server-side apply refuses to overwrite another field manager silently.
Where module 01 leaves you: A running cluster with Argo CD installed from your values file, a platform repo with that file committed, and you logged in as admin in both the UI and the CLI. No applications yet.
02 Your first Application, from Git
This module only · Full module
The idea
An Application is a pointer: a source in Git, a destination in a cluster, and a project that allows it. Argo CD reports two independent things about it: sync (does the cluster equal Git?) and health (does it work?).
Remember
- Application: a Kubernetes object in the
argocdnamespace withsource(repoURL, targetRevision, path),destination(server, namespace),projectand an optionalsyncPolicy. - Sync status:
Synced,OutOfSync, orUnknownwhen Argo CD cannot even render or read the source. - Health status:
Healthy,Progressing,Degraded,Missing,Suspended. Independent of sync:Synced+Degradedmeans Git faithfully declares something broken. - Refresh vs sync: refresh re-reads Git and compares (automatic every few minutes;
--refreshonly skips the wait). Sync applies the difference. Without a sync policy, syncing is manual. - Reading it:
OutOfSync+Missing= in Git, not deployed yet.Synced+Progressing= pods starting.Unknown+ComparisonError= read the conditions. - History: one row per sync (a deploy), not per commit.
REVISIONshows the branch or tag and the exact commit.
Pitfalls
- Pushing an Application file to Git does nothing until something applies it:
kubectl applyhere, the root app from module 04. - A typo in
pathgivesUnknownand a ComparisonError. The running workload is untouched: Argo CD changes nothing it cannot render. Synceddoes not mean working. Always read health as well.
Commands
kubectl apply -n argocd -f apps/checkout-dev.yaml | Create or update the Application object. Pushing the file is not enough yet |
argocd app get APP | Sync, health, conditions and every resource |
argocd app get APP --refresh | Look at Git now instead of waiting |
argocd app diff APP | What a sync would change |
argocd app sync APP | Apply the difference |
argocd app wait APP --health --timeout 180 | Wait until it is healthy |
argocd app history APP | One row per deploy, with the commit |
An Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: checkout-dev
namespace: argocd # Applications live here
spec:
project: default # the fence
source: # where in Git
repoURL: https://github.com/YOUR_GITHUB_USER/checkout-config.git
targetRevision: main
path: manifests
destination: # where in the cluster
server: https://kubernetes.default.svc
namespace: checkout-dev
syncPolicy:
syncOptions: [CreateNamespace=true]
Test yourself
Answer in your head first, then open the answer.
An Application is Synced and Degraded. What does that mean?
The cluster matches Git, but the workload is not working.
Without a sync policy, what happens when you push a change?
Argo CD detects it and shows OutOfSync, then waits for someone to sync.
Where does an Application object live?
In the argocd namespace, on the cluster where Argo CD runs. Its destination namespace holds what it deploys.
What does --refresh do, and what does it not do?
It makes Argo CD read Git right now. It deploys nothing.
Where module 02 leaves you: The checkout-config repo holds the manifests, the Application checkout-dev exists in Argo CD, and the demo app (podinfo) runs in the checkout-dev namespace with 2 replicas, deployed from Git.
03 Reconciliation: auto-sync, prune and self-heal
This module only · Full module
The idea
Drift is the cluster no longer matching Git. Automated sync applies new commits, prune deletes what Git removed, and self-heal undoes changes made by hand. With all three on, Git is the only way to change what runs.
Remember
- Automated sync (
syncPolicy.automated): new commits are applied without waiting for a person. - Prune (
prune: true): objects deleted from Git are deleted from the cluster. Off by default even with auto-sync, so a bad commit cannot silently delete half an app. - Self-heal (
selfHeal: true): live changes made outside Git are put back within seconds. Without it, auto-sync only reacts to new commits. - The loop: Git is re-read every
timeout.reconciliation(60 s here, plus jitter). Live changes are noticed sooner, through watches. A Git webhook makes detection instant. - Sync options worth knowing:
Prune=falseon one resource (volumes, namespaces),PruneLast,ServerSideApply,ApplyOutOfSyncOnly,RespectIgnoreDifferences, andReplace(rare; causes downtime). - Every sync is recorded: what triggered it (
automatedor a person), the revision, the result per resource.
Pitfalls
- "Argo CD keeps reverting my fix" is not a bug. With self-heal on, the only fix that stays is a commit; take away write access and manual fixes become impossible too.
- Auto-sync without prune leaves deleted objects running, shown as OutOfSync. Pruning is a separate decision on purpose.
- Data you must not lose to a deleted line needs
argocd.argoproj.io/sync-options: Prune=falseon the resource.
Commands
argocd app get APP | grep 'Sync Policy' | Expect Automated (Prune) |
kubectl -n NS get deployment NAME -w | Watch self-heal undo a hand change |
git revert --no-edit HEAD && git push | Undo the last change the GitOps way |
argocd app get APP --show-operation | The last sync: trigger, revision, result per resource |
argocd app sync APP --prune --dry-run | Preview a sync, deletions included |
The sync policy
syncPolicy:
automated:
prune: true # delete what was deleted from Git
selfHeal: true # undo changes made outside Git
syncOptions:
- CreateNamespace=true
Test yourself
Answer in your head first, then open the answer.
Auto-sync is on, prune is off. You delete a manifest from Git. What happens?
The object stays, and the app shows OutOfSync.
What does self-heal react to?
Live changes made outside Git. Auto-sync alone only reacts to new commits.
Why set Prune=false on a single resource?
So a deleted line in Git can never delete it, for example a volume claim that holds data.
How do you make a 2 a.m. hotfix stick?
Commit it to the environment repo. Undo it the same way, with a revert, afterwards.
Where module 03 leaves you: checkout-dev syncs by itself: new commits are applied, objects deleted from Git are deleted from the cluster, and hand changes are undone within seconds. Git is now the only way to change what runs.
04 App of apps: everything declarative, Argo CD included
This module only · Full module
The idea
One root Application, applied by hand once, turns every file in apps/ into an Application, including one for Argo CD itself. After that, everything, upgrading Argo CD included, is a reviewed file in Git.
Remember
- App of apps: root's source is the
apps/folder. Add a file, get an Application; delete it, the Application goes. - The one manual step, ever:
kubectl apply -f bootstrap/root.yaml. Root lives inbootstrap/, outsideapps/, so a bad commit cannot delete it. - Argo CD manages itself: the
argocdApplication points at the same chart version and values you installed with, and adopts what Helm created. Neverhelm upgradethat release again;helm liststill shows the old record, harmlessly. - Guardrails first: a
validateCI check plus branch protection (require a pull request and the check) make pull requests the only way intomain. - The resources finalizer
resources-finalizer.argocd.argoproj.io: deleting the Application deletes what it deployed. Without it the workload keeps running, unmanaged. - Adopt, then upgrade: keep the chart at the installed version when Argo CD takes over. Upgrading is a separate pull request.
Pitfalls
- Deleting a file in
apps/is an uninstall. Without the finalizer, the workload keeps running with nothing managing it: no self-heal, and the next real change never arrives. - Running
helm upgradeon the original release after Argo CD adopted it: two owners for one install. - Requiring approvals on a repo you work on alone blocks you, because you cannot approve your own pull request. Require the check instead.
Commands
kubectl apply -n argocd -f bootstrap/root.yaml | The last kubectl apply for Applications |
argocd app list | Root and its children |
argocd app resources root | Everything root owns |
scripts/validate.sh | Run the CI check locally before you push |
gh pr create --fill | Propose the change |
gh pr checks --watch | Wait for validate |
gh pr merge --squash --delete-branch | Merge it |
Test yourself
Answer in your head first, then open the answer.
How do you add a new Application after this module?
A pull request that adds its manifest to apps/.
Why does root.yaml live in bootstrap/ and not in apps/?
If root managed itself, a bad commit to its own file could delete it and everything below it.
How do you upgrade Argo CD from now on?
A pull request that changes targetRevision in apps/argocd.yaml.
What does the resources finalizer change about deleting an Application?
With it, deleting the Application also deletes its workload. Without it, the workload stays, unmanaged.
Where module 04 leaves you: One root Application owns argocd and checkout-dev. Adding or removing a file in apps/ adds or removes an Application. main accepts changes only through a pull request that passes validate. kubectl apply for Applications is over.
05 Helm and Kustomize in practice
This module only · Full module
The idea
Argo CD renders Helm and Kustomize itself and applies plain manifests, so there is no Helm release. Helm packages apps other people install; Kustomize holds the small differences between your own environments. Render locally before you push.
Remember
- How Argo CD picks the tool: only YAML files = a plain directory; a
kustomization.yaml= Kustomize; aChart.yaml= a Helm chart in Git;chart:instead ofpath:= a chart from a chart repository. - Helm in Argo CD: the repo-server runs
helm templateand applies the output.helm listshows nothing, and Helm hooks become Argo CD hooks. Values stay in Git (helm.valuesObjector value files). - Kustomize: the base is ordinary YAML every environment shares; an overlay points at the base and lists only what differs (
images,replicas, patches). The base is never edited. - Helm or Kustomize? Helm for reusable packages (templates plus
values.yaml: vendor add-ons, a chart your team publishes). Kustomize for your own environments (no templates; the base is valid YAML). Many teams use both. - Parameter overrides (
argocd app set -p, the UI's Parameters tab) change the Application, not Git. A debugging tool only: root self-heals them away.
Pitfalls
- An
imagesentry whosenamedoes not match the base (stefanprodan/podinfoinstead ofghcr.io/stefanprodan/podinfo) silently changes nothing. Argo CD is Synced and Healthy on the old version. Render and grep before you push. - Deploying with
argocd app set: the change is not in Git and gets undone. - Expecting
helm historyorhelm rollbackfor charts Argo CD deployed. History and rollback live in Argo CD and Git.
Commands
kubectl kustomize OVERLAY_DIR | Render an overlay exactly as Argo CD will |
helm template NAME CHART_DIR -f values.yaml | Render a chart locally |
argocd app manifests APP | What Argo CD rendered from Git |
helm list -A | Shows nothing for charts Argo CD deployed |
curl -sk -o /dev/null -w '%{http_code}\n' https://localhost:8443/ | Traefik with no routes answers 404: alive |
A Kustomize overlay
# overlays/prod/kustomization.yaml
resources:
- ../../base
replicas:
- name: hello
count: 3
images:
- name: nginx # must match the image name in the base exactly
newTag: "1.28"
Test yourself
Answer in your head first, then open the answer.
You deploy a chart with Argo CD. What does helm list show?
Nothing. Argo CD renders with helm template and applies the output itself.
Where should the differences between dev and prod live?
In overlays (or value files) in the environment repo, changed by pull request.
Why can't an override set with argocd app set survive here?
Root self-heals the Application back to its manifest in Git.
A promotion merged, Argo CD is Synced, prod still runs the old tag. What do you check first?
Render the overlay: the images name probably does not match the base.
Where module 05 leaves you: Three things are now deployed from Git: the traefik ingress controller, checkout-dev (version 6.7.1, 1 replica) and checkout-prod (version 6.7.0, 2 replicas). helm list shows nothing for Traefik, because Argo CD rendered it.
06 Expose Argo CD safely, and upgrade it
This module only · Full module
The idea
A team needs one front door (an ingress with TLS), personal accounts instead of a shared admin, and upgrades that are routine pull requests. All three are lines in Argo CD's values file or its Application.
Remember
- Ingress with TLS: TLS ends at Traefik and Argo CD serves plain HTTP behind it (
server.insecure: true). Safe only because nothing else can reach the server Service. - The CLI through an ingress speaks gRPC-web:
argocd login HOST --grpc-web. - Accounts in Git, passwords not:
accounts.alice: loginandg, alice, role:admininpolicy.csvare config. The password is set once withargocd account update-passwordand stored inargocd-secret. policy.default: "": logged in but not listed means you see nothing.- Switch off
admin(admin.enabled: false) once someone else can administer, and deleteargocd-initial-admin-secret. - An upgrade is one line: read the release notes, render old and new and diff them, change
targetRevision, merge, verify. Patch: routine. Minor: read the notes. Major: test first. Downgrades across minor versions are not supported.
Pitfalls
- Removing the only admin from
policy.csvlocks everyone out of the UI. The application controller needs no login, so reverting the commit still fixes it. Keep a break-glass account and a CI check that one admin remains. connection refusedonlocalhost:8080after the switch: the CLI is still logged in to the old port-forward. Log in again through the ingress.--insecureis only for the self-signed laptop certificate. Production uses real certificates (cert-manager) and SSO.
Commands
argocd login HOST --grpc-web --insecure --username alice --password ... | Log in through the ingress |
argocd account update-password --account NAME --current-password ... --new-password ... | Set a local account's password |
argocd account get-user-info | Who am I logged in as? |
diff <(helm template ... --version OLD) <(helm template ... --version NEW) | See what an upgrade changes before you make it |
argocd version --short | Client and server versions after the upgrade |
The values that matter
configs:
params:
server.insecure: true # TLS ends at the ingress
cm:
admin.enabled: false
accounts.alice: login
rbac:
policy.default: ""
policy.csv: |
g, alice, role:admin
server:
ingress:
enabled: true
ingressClassName: traefik
Test yourself
Answer in your head first, then open the answer.
Why is server.insecure: true acceptable here?
TLS terminates at the ingress, and only the ingress can reach the Argo CD server.
Where does alice's password live?
In the argocd-secret Secret, set with the CLI. Git only declares that the account exists.
What is an Argo CD upgrade, technically?
A pull request changing targetRevision of the argocd Application.
You locked yourself out with an RBAC commit. How do you get back?
Revert the commit. The controller applies Git without needing a login.
Where module 06 leaves you: Argo CD answers at https://argocd.localtest.me:8443 with TLS (no more port-forward), alice is the administrator, admin is disabled, and Argo CD runs the newer chart version, upgraded by a pull request.
07 Three repositories and multiple sources
This module only · Full module
The idea
Code, environment settings and platform config change at different speeds with different risks, so they live in three repos. One Application reads the chart from one repo and the values from another with sources and a ref. A tag is a release; a change to the environment repo is a deploy.
Remember
- Application repo (
checkout-app): code, CI and the chart. A release is a Git tag. Merging here deploys nothing. - Environment repo (
checkout-config): values per environment. Changing a tag or a value here is a deploy. - Platform repo (
argocd-platform): Argo CD, Applications, projects. Few changes, widest blast radius. - Multiple sources:
sources:is a list. A source withref: configonly contributes files; another source reads one as$config/helm/prod.yaml. - One revision per source:
argocd app historyshows only the first source.argocd app get APP --show-operationlists every source's commit underSync Revision:, in source order. - Immutable fields: a Deployment's
spec.selectorcannot change. Switching from Kustomize to the chart needed a delete and recreate; in prod, plan renames (new name, then delete the old). - Not a grouping tool: one Application per thing, grouped by an app of apps. Bundling unrelated things into sources loses separate sync, health and history.
Pitfalls
- Moving a release tag deploys to every environment that pins it, with no change to the environment repo. Protect release tags on GitHub (tag rulesets).
- Reading
historyand concluding nothing changed: it only shows the first source. field is immutableonspec.selectorcomes from Kubernetes, not Argo CD.
Commands
git tag v1.0.0 && git push origin v1.0.0 | Release the chart |
helm lint chart | Check the chart |
helm template checkout CHART_DIR -f helm/prod.yaml | Render with one environment's values |
argocd app get APP --show-operation | grep 'Sync Revision' | The deployed commit of every source |
argocd app get APP --hard-refresh | Re-read Git and drop cached manifests (a tag moved) |
Chart from one repo, values from another
sources:
- repoURL: https://github.com/YOUR_GITHUB_USER/checkout-app.git # source 1: the chart
path: chart
targetRevision: v1.0.0 # a release
helm:
valueFiles:
- $config/helm/prod.yaml # read from the ref source
- repoURL: https://github.com/YOUR_GITHUB_USER/checkout-config.git # source 2: the values
targetRevision: main
ref: config # the nickname behind $config
Test yourself
Answer in your head first, then open the answer.
Which change is a deploy to prod?
A change to helm/prod.yaml in checkout-config.
What does ref: config do?
Names a source so other sources can read files from it as $config/.... It deploys nothing itself.
One Application for checkout, catalog and Traefik: good idea?
No. Unrelated things lose separate sync, health and history. One Application each, grouped by an app of apps.
Why does history keep saying v1.0.0 after a values change?
It shows only the first source, the chart, which did not change.
Where module 07 leaves you: Three repos exist: argocd-platform (Applications), checkout-app (the chart, tagged v1.0.0) and checkout-config (values per environment). checkout-dev and checkout-prod deploy from chart plus values, and prod now runs 6.7.1.
08 Private repositories and secrets
This module only · Full module
The idea
Credentials are the one thing that must not sit in Git. Argo CD reads a private repo with a read-only deploy key stored as a labelled Secret. Values live in a secret store; Git holds only ExternalSecrets, references that say which key to fetch from which store, and External Secrets builds the Secrets in the cluster.
Remember
- Repository credential: a Secret in
argocdlabelledargocd.argoproj.io/secret-type: repository, withtype,urland a key or token. Arepo-credsSecret does the same for every repo under a URL prefix. - Deploy key: an SSH key that can read one repository and belongs to no person. The narrowest credential GitHub offers, and no outage when someone leaves.
- ExternalSecret: names a store (
secretStoreRef), the key in it (remoteRef) and the Secret to build (target).target.templateadds labels, such as the repository label Argo CD looks for. Safe to commit: it holds no value. - ClusterSecretStore: how External Secrets reaches a store and logs in.
conditionslists the only namespaces that may read from it. One store per team. - On kind: each store is a namespace (
store-platform,store-checkout) you fill withkubectl create secret. In production it is a vault reached with workload identity; the ExternalSecrets do not change. - The URL must match: the Application's
repoURLmust be the SSH URL the credential was made for. - The options: External Secrets with a store (this course), SOPS (you have a cloud KMS), encrypt-for-one-cluster controllers (avoid: a rebuild means re-encrypting everything). Avoid the Vault plugin: secrets end up in the repo-server cache.
Pitfalls
- Pointing an ExternalSecret at another team's store:
denied by spec.condition, Degraded, and no Secret. Use your own store. - Forgetting the value: an ExternalSecret whose key is not in the store stays
SecretSyncedError. Git says what; someone still has to put the value in the store. - A changed value reaches the Secret at the next refresh, but a pod reads environment variables only at start. Restart the Deployment after a rotation.
- A repo made private, or a deleted deploy key, shows up as
Unknownplus an authentication error at the next fetch, not at once.--hard-refreshshows it now.
Commands
kubectl -n store-platform create secret generic NAME --from-file=sshPrivateKey=KEY | Put a value in a store, never in Git |
kubectl get clustersecretstore | Is each store Valid and READY True? |
kubectl -n NS get externalsecret,secret NAME | The ExternalSecret from Git (SecretSynced) and the Secret built from it |
kubectl -n NS describe externalsecret NAME | Why a Secret was not built: Conditions and Events |
kubectl -n NS annotate externalsecret NAME force-sync=$(date +%s) --overwrite | Fetch again now, instead of waiting for refreshInterval |
ssh-keygen -q -t ed25519 -N '' -f KEY | A key pair for Argo CD (no passphrase: it runs unattended) |
gh repo deploy-key add KEY.pub -R OWNER/REPO --title argocd | Register the public half, read-only |
argocd repo list | Is each repository connection Successful? |
Test yourself
Answer in your head first, then open the answer.
What makes a Secret a repository credential for Argo CD?
The label argocd.argoproj.io/secret-type: repository, plus url and credentials. Here the ExternalSecret's template sets the label.
Why a deploy key instead of a personal access token?
It reads one repository only and belongs to no person.
Someone reads your ExternalSecret on GitHub. What can they do with it?
Nothing useful. It names a store and a key; only identities you granted can read the store.
What keeps team-checkout from reading the platform's deploy keys?
The conditions on the platform ClusterSecretStore: only argocd may use it.
Where module 08 leaves you: checkout-config is private and Argo CD reads it over SSH with a deploy key from the platform store. Each environment has a checkout-db Secret built by an ExternalSecret, and the checkout pod gets DB_PASSWORD. Git holds only references.
09 AppProjects and RBAC: team boundaries
This module only · Full module
The idea
Two fences per team: an AppProject decides what Argo CD may deploy for them (from which repos, into which namespaces, which kinds), and Argo CD RBAC decides what a person may do in Argo CD. Namespaces belong to the platform.
Remember
- AppProject:
sourceRepos,destinations(cluster plus namespace),clusterResourceWhitelist(cluster-scoped kinds allowed) andnamespaceResourceBlacklist(namespaced kinds denied). Anything outside fails withnot permitted in project. - Argo CD RBAC:
policy.csvlinesp, role, resource, action, project/app, allowandg, account-or-group, role. Failure:permission denied. - Kubernetes RBAC is the third fence, for kubectl. Failure:
forbidden. - Platform-owned namespaces: teams deploy into namespaces they cannot create, label or delete, so
CreateNamespacegoes away for team apps. - The
defaultproject allows everything. In production restrict it (sourceRepos: []) and map SSO groups instead of local accounts. - Also in projects: project roles issue tokens for automation in one project; sync windows allow or deny syncs on a schedule.
Pitfalls
- Changing
bootstrap/root.yamland expecting it to apply: root does not manage itself. Apply it by hand. - A team adding a ClusterRole or a NetworkPolicy in its own repo: the sync fails as not permitted and nothing is applied. Access requests go to the platform repo, under review; grant the narrowest thing (
viewin their namespaces). - "Just admin in dev": the habits a team forms in dev come with it to prod. Give dev a looser project if needed, but keep the fence.
Commands
argocd proj list | Every AppProject |
argocd app list -o wide | Applications with their project |
argocd account can-i sync applications 'PROJECT/*' | Would Argo CD RBAC allow this? |
kubectl auth can-i list pods -n NS --as USER --as-group GROUP | Test Kubernetes RBAC as someone else |
kubectl apply -n argocd -f bootstrap/root.yaml | Root's own file changed: apply it by hand |
A team role in policy.csv
p, role:checkout-team, applications, get, checkout/*, allow
p, role:checkout-team, applications, sync, checkout/*, allow
p, role:checkout-team, applications, action/*, checkout/*, allow
p, role:checkout-team, logs, get, checkout/*, allow
p, role:checkout-team, projects, get, checkout, allow
g, checkout-dev, role:checkout-team
Test yourself
Answer in your head first, then open the answer.
checkout-dev runs argocd app delete checkout-prod. Which fence stops it?
Argo CD RBAC: its role has get, sync, actions and logs only.
team-checkout adds a NetworkPolicy to its namespace through Git. What happens?
Argo CD refuses to sync it: NetworkPolicy is on the project's namespace blacklist.
Why do namespaces belong to the platform team?
Labels, quotas and policies on the namespace are the platform's guarantees. A team must not be able to remove them.
not permitted in project versus permission denied: which fence said each?
The AppProject (what may be deployed) versus Argo CD RBAC (what a person may do).
Where module 09 leaves you: Platform apps sit in the platform project, checkout apps in the checkout project (its two repos and two namespaces only). The platform team creates the namespaces. The account checkout-dev can see, sync and read logs for checkout, and nothing else.
10 Deployment ordering: hooks, phases and waves
This module only · Full module
The idea
Argo CD orders a sync by phase (PreSync, Sync, PostSync), then by wave (lowest first, each healthy before the next), then by kind and name. A PreSync migration that fails stops the release before anything changes.
Remember
- Phases:
PreSync,Sync,PostSync, andSyncFailwhen something fails. (PreDeleteandPostDeleterun around deleting an app.) - Hooks: resources annotated
argocd.argoproj.io/hook: <phase>, usually Jobs. They run when a sync runs and are not part of the compared state, so adding one leaves the app Synced. A delete policy says when to clean them up. - Waves:
argocd.argoproj.io/sync-wave: "-1". Lower waves go first, and Argo CD waits for each wave to be healthy before the next. - Within a wave: by kind (namespaces, CRDs, config, workloads, ...), then alphabetically by name.
- Waves between Applications (app of apps) only wait if Applications report health: add the Lua health check
resource.customizations.health.argoproj.io_Applicationtoargocd-cm. - Migrations must be safe to run twice and backwards compatible: the old pods keep serving while they run.
Pitfalls
- A failed PostSync smoke test marks the sync Failed while the app stays Healthy: the deploy happened, the check after it failed. Read the hook Job's logs.
- Using hooks so an app can wait for its database. Make the app retry; keep hooks for things that run exactly once per release.
- Expecting waves between child Applications to wait without the Application health check.
Commands
argocd app sync APP | Run a sync, and with it the hooks |
argocd app get APP --show-operation | sed -n '/^GROUP/,$p' | The order the last sync applied things in |
kubectl -n NS get jobs | Hook Jobs left behind |
kubectl -n NS logs job/NAME | What a hook did |
grep -H 'sync-wave' apps/*.yaml | Which Applications carry a wave |
The two annotations
# A migration Job: runs before the deploy
metadata:
annotations:
argocd.argoproj.io/hook: PreSync
---
# A ConfigMap: applied before the Deployment that reads it
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
Test yourself
Answer in your head first, then open the answer.
The PreSync migration fails. What happens to the running Deployment?
Nothing. The sync stops before the Sync phase, and the SyncFail hook runs.
Two resources have sync-wave -1 and 0. When does the second start?
Once wave -1 is healthy.
Why does the app of apps need a health check for Applications?
Without it a child Application counts as healthy at once, so waves between Applications do not wait.
You add a hook to the chart. Why does the app stay Synced?
Hooks are not part of the state Argo CD compares. They run on the next sync.
Where module 10 leaves you: Every sync of checkout-dev runs a migration before the deploy and a smoke test after it. You have seen a broken migration leave the old version running, and checkout-dev is back to healthy on 6.7.1.
11 Debugging labs: seven ways Argo CD goes wrong
This module only · Full module
The idea
Start at the Application and go one layer down at a time: status and conditions, the resource tree, the object's events, pod logs, then Argo CD's own logs. Render before you guess, and fix through Git.
Remember
- Read the whole
argocd app getbefore typing anything else. Sync, health, conditions and the last operation answer most questions. - Render or apply? Broken YAML fails at rendering (
Unknown, ComparisonError, nothing applied). A wrong value such asmemory: lotsfails at applying (the sync fails). - The seven failures: a deny sync window holds a merge back; a bad image tag goes Synced, then Degraded; invalid manifests of both kinds; AppProject and RBAC refusals; a deleted deploy key; an Application stuck deleting; an autoscaler and Argo CD fighting over
replicas. - Two owners, one field: pick one owner and make it permanent in Git. Here: declare the autoscaler in the values instead of creating it with kubectl.
- Stuck deletion: find what the deletion waits for (another resource's finalizer) and fix that. Never strip Argo CD's own finalizer first.
- Revert is the default fix. kubectl only restores service while the real fix goes through Git. Write one runbook entry per incident: symptom, where to look, cause, fix, prevention, owner.
Pitfalls
- Removing
resources-finalizer.argocd.argoproj.ioto unstick a deletion leaves everything the app deployed behind, unmanaged. - Switching off self-heal to stop a fight over a field: the flapping stops, but Git no longer describes what runs.
- Guessing before rendering. Nine times out of ten
argocd app manifestsor a local render shows the problem.
Commands
argocd app get APP --refresh | Status, conditions and the last operation |
argocd app manifests APP | Exactly what Argo CD will apply |
argocd app diff APP | What differs, field by field |
argocd proj windows list PROJECT | Sync windows that can block syncs |
argocd repo list | Repository connections and their status |
argocd app terminate-op APP | Cancel a stuck sync |
Test yourself
Answer in your head first, then open the answer.
An Application is Unknown with a ComparisonError. What is true?
Argo CD could not render or read the source. Nothing was applied and the running workload is untouched.
An app goes OutOfSync again right after every sync, always on the same field. Likely cause?
Another controller changes that field. Two owners for one field: pick one.
A change merged, but prod stays OutOfSync and never syncs. Where do you look?
Sync windows on the project: argocd proj windows list, or the Window line in argocd app get.
An Application is stuck deleting. What should you not do first?
Remove Argo CD's resources finalizer from it. Fix the stuck resource instead.
Where module 11 leaves you: All seven labs are cleaned up: the platform is healthy again and every Application is Synced and Healthy. Your platform repo has docs/runbook.md with one real entry.
12 How product teams work with Argo CD
This module only · Full module
The idea
Tools do not make an operating model; agreements do. Teams own their chart, values, secrets, releases and rollbacks inside their project; the platform owns Argo CD, projects, namespaces, RBAC and credentials. Write it down, enforce it with CODEOWNERS and CI, and roll back with a revert.
Remember
- Teams own: their chart (
checkout-app), values and secrets (checkout-config), releases (tags), and syncs and rollbacks in their project. Inside it, they need nobody. - The platform owns: the Argo CD install, upgrades, exposure, SSO and RBAC; AppProjects, namespaces and quotas; repository credentials; add-ons. Teams propose
apps/checkout-*.yaml; the platform reviews. - How teams deploy: a pull request to
checkout-config(versions, values) or toapps/checkout-*.yaml(a chart release), reviewed through CODEOWNERS. - How teams inspect: read-only in Argo CD for their apps: status, diff, history, resources, logs. Write access is Git.
- When to escalate: the fix is outside the team's repos (permissions, credentials, add-ons, Argo CD itself), or every app is Unknown and the UI is down.
- Contracts that matter are CI checks, not trust: for example, chart sources must be release tags like
v1.2.3.
Pitfalls
- Using the rollback button: it refuses while auto-sync is on, and turning auto-sync off leaves Git saying something other than the cluster. Revert in Git.
- Pointing prod at a branch "for an hour": from then on every push to that branch deploys to prod, unreviewed. A CI check that chart sources are release tags stops it.
- Escalating what the team owns (its own failed syncs, Degraded pods), or fixing what the platform owns.
Commands
argocd app get APP | Is it OK? |
argocd app history APP | What was deployed, and when? |
argocd app diff APP; echo $? | Anything waiting to be applied? Exit code 0 means nothing |
argocd app resources APP | What objects does it own? |
argocd app logs APP --tail 5 | What is it saying? No kubectl needed |
git revert --no-edit HEAD && git push | Roll back |
Test yourself
Answer in your head first, then open the answer.
team-checkout wants a new chart release in dev. Which change?
A pull request changing targetRevision in apps/checkout-dev.yaml, which they can approve through CODEOWNERS.
Why is the Argo CD rollback button the wrong rollback here?
It refuses while auto-sync is on, and turning auto-sync off leaves Git disagreeing with the cluster.
team-checkout sees authentication required on its apps. Who acts first?
team-checkout checks its deploy key is still on the repo, and escalates if the credential in Argo CD must change.
What is worth measuring about this agreement?
How often a product team has to ask the platform team for something. Each escalation is a candidate for self-service.
Where module 12 leaves you: The platform repo documents the rules and enforces who reviews what. You have run a full release and rollback as team-checkout, and dev is back on the release it started with.
13 Capstone: onboard and operate catalog
This module only · Full module
The idea
Onboarding a team is a set of reviewed files: an AppProject, platform namespaces, an account and role, a secret store, a repo credential, ExternalSecrets and two Applications. Handling an incident is: health first, then the pods, then a revert, then the five answers.
Remember
- Platform pieces:
projects/catalog.yaml(only its repos, only its namespaces, no cluster-scoped kinds, quotas, limits, network policies and RBAC denied),namespaces/catalog.yaml,secret-store/catalog.yaml(store-catalog, readable only fromcatalog-devandcatalog-prod), andaccounts.catalog-devwithrole:catalog-teaminpolicy.csv. - Team pieces: a private
catalog-configwithhelm/dev.yamlandhelm/prod.yaml(nameOverride: catalog) and an ExternalSecretcatalog-dbper environment, reading from thecatalogstore. Its deploy key sits instore-platform, with an ExternalSecret in the platform repo. - The Applications: three sources each (the shared chart at
v1.0.0, the values repo asref: config, andsecrets/<env>), projectcatalog, the resources finalizer, and automated sync with prune and self-heal. - Done means: both apps Synced and Healthy, the team account limited to its project, a secret from its own store in use, a PreSync migration in dev, and an incident written up.
- The five answers: what failed, how it was detected, who responded, how it was corrected, how it is prevented.
- Rebuild: your repos recreate everything on a fresh cluster. On kind the store values live in the cluster, so put them back; with a real vault there is nothing to redo.
Pitfalls
- Leaving the shared chart repo out of the project's
sourceRepos: the Applications are not permitted. - Copying checkout's ExternalSecrets for catalog without changing the store: the
checkoutstore does not serve catalog's namespaces. Point them atcatalog. - Pushing straight to
main, as the course did from module 07 to stay on Argo CD. A team protectsmainin every repo and works through pull requests with thevalidatecheck.
Commands
argocd app get APP --refresh | grep -E 'Health|Deployment' | Health first |
kubectl -n NS get pods | The reason: OOMKilled, CrashLoopBackOff |
git revert --no-edit HEAD && git push | Recover |
gh issue create -R OWNER/REPO --title "Incident: ..." --body "What failed: ..." | Write the five answers |
kind delete cluster --name argocd | Tear down. Git keeps the setup |
Test yourself
Answer in your head first, then open the answer.
What did onboarding team-catalog require from the platform team?
A project, namespaces, a secret store, an account and role, a repo credential and two Application files, all as reviewed pull requests.
Where do you look first when an app is Degraded?
The resource tree, then the failing object's events and logs. Degraded is about the workload, not Argo CD.
What would you automate when a third team arrives?
Generating the project, namespaces and Applications from one file per team with ApplicationSets: the Fleet GitOps course.
Where module 13 leaves you: team-catalog runs catalog in dev and prod, with the same boundaries as team-checkout, entirely from Git. You handled an incident and wrote the five answers in an issue. You can run Argo CD for a team.