PlatformEngr® Argo CD course

Module 07 · Real-world delivery · 55 min · You are: Platform and product

Three repositories and multiple sources

In this module

  • You learn: Why the application, its release and its environment settings live in separate repositories, how one Application can read from two repos (multiple sources), and why a Git tag is a release but not a deploy.
  • You do: Create checkout-app with the Helm chart and release it as v1.0.0. Put per-environment values in checkout-config. Switch both Applications to read the chart from one repo and the values from the other. Promote a new version to prod with a one-line change.
  • You end with: 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.

Code changes many times a day; deploys are deliberate; platform changes are rare and affect everyone. Putting all three in one repo means one set of permissions, reviewers and CI for very different risks. Splitting them is common, and Argo CD's multiple sources let one Application take the chart from one repo and the values from another.

Open this module in the interactive course to track progress and switch tracks.

GitOps loop practised: Committed → Reviewed → Detected → Rendered → Compared → Synced → Healthy → Observed

Concepts

Application repository
checkout-app: code, tests, CI, and the chart. A release is a Git tag. Changes here deploy nothing by themselves.
Environment repository
checkout-config: what runs where. Changing an image tag or a value here is a deploy.
Shared platform repository
argocd-platform: Argo CD, Applications, projects. Few changes, widest blast radius.
Multiple sources
Until now an Application had one source. With sources (a list) it can read from several repos. Each entry can give the chart, the values, or both.
ref and $config
A source with ref: config gives that repo a nickname and contributes only files. Another source then writes $config/helm/prod.yaml, and Argo CD reads that file from the nicknamed repo. $config is not a variable you define anywhere else: it is just the ref name with a $ in front.
Not a grouping tool
The Argo CD docs warn against using sources to bundle unrelated things: you lose separate sync, health and history. Group Applications with an app of apps.

The three repositories

Application repoEnvironment repoPlatform repo
Examplecheckout-appcheckout-configargocd-platform
Ownerteam-checkoutteam-checkoutPlatform team
HoldsCode, CI, Helm chartValues per environment, sealed secretsArgo CD config, Applications, projects
Argo CD readsA tag (v1.0.0)mainmain
A merge hereReleases, deploys nothingDeploysChanges the platform for everyone

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. team-checkout: create the application repository and release v1.0.0

The chart moves into its own repo. A release is a tag; Argo CD will deploy the tag, never a moving branch.

cd ~/work && gh repo create YOUR_GITHUB_USER/checkout-app --public --clone

Copy the Helm chart for checkout from the reference:

cd ~/work/checkout-app && git switch -C main
cp -r ~/work/argocd-gitops/checkout-app/chart ~/work/argocd-gitops/checkout-app/README.md .

Check the chart for mistakes. Expect 1 chart(s) linted, 0 chart(s) failed:

helm lint chart

Commit and push it:

git add -A && git commit -qm "checkout chart 1.0.0" && git push -qu origin main

Release it. A release is a Git tag: a fixed name for this exact commit. Create v1.0.0 and push it.

git tag v1.0.0 && git push -q origin v1.0.0

2. team-checkout: values per environment

Short files with only what differs. Render locally with the released chart exactly as Argo CD will. (Secrets and hooks come in modules 08 and 10.)

cd ~/work/checkout-config && git switch -q main && git pull -q

Create ~/work/checkout-config/helm/dev.yaml

# checkout in dev. Values for the chart in checkout-app.
image:
  tag: "6.7.1"                 # promote by changing this line
message: checkout (dev)
replicas: 1

Create ~/work/checkout-config/helm/prod.yaml

# checkout in prod.
image:
  tag: "6.7.0"
message: checkout (prod)
replicas: 2
resources:
  requests: { cpu: 100m, memory: 96Mi }
  limits: { memory: 192Mi }

Render the chart with the prod values, the way Argo CD will. Expect replicas: 2 and an image ending in 6.7.0:

helm template checkout ~/work/checkout-app/chart -f helm/prod.yaml | grep -E 'replicas|image:'
git add -A && git commit -qm "checkout: Helm values per environment" && git push -q

3. Platform: switch the Applications to multiple sources

The Application keeps its name but now has two sources: the chart from checkout-app (pinned to the tag v1.0.0), and the values from checkout-config. In the file you are about to write, find ref: config on the second source and $config/helm/dev.yaml under the first: that pair is how the chart finds its values. Expect one failure on purpose. The old Deployment was made from Kustomize, the new one from the chart, and they use different labels; Kubernetes does not allow a Deployment's label selector to change, so the sync fails. You fix it in the next step.

cd ~/work/argocd-platform && git switch -q main && git pull -q

Replace the whole file ~/work/argocd-platform/apps/checkout-dev.yaml

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: checkout-dev
  namespace: argocd
  labels:
    team: checkout
    env: dev
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  sources:                            # a list: this Application reads from two repos
    - repoURL: https://github.com/YOUR_GITHUB_USER/checkout-app.git              # source 1: the chart
      path: chart
      targetRevision: v1.0.0          # a release of the chart
      helm:
        valueFiles:
          - $config/helm/dev.yaml  # $config = the repo whose source says "ref: config" (source 2)
    - repoURL: https://github.com/YOUR_GITHUB_USER/checkout-config.git            # source 2: the values. It deploys nothing itself
      targetRevision: main
      ref: config                     # gives this repo the nickname "config", so $config works above
  destination:
    server: https://kubernetes.default.svc
    namespace: checkout-dev
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Replace the whole file ~/work/argocd-platform/apps/checkout-prod.yaml
The same, with prod in the name, labels, value file and namespace.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: checkout-prod
  namespace: argocd
  labels:
    team: checkout
    env: prod
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default
  sources:                            # a list: this Application reads from two repos
    - repoURL: https://github.com/YOUR_GITHUB_USER/checkout-app.git              # source 1: the chart
      path: chart
      targetRevision: v1.0.0          # a release of the chart
      helm:
        valueFiles:
          - $config/helm/prod.yaml  # $config = the repo whose source says "ref: config" (source 2)
    - repoURL: https://github.com/YOUR_GITHUB_USER/checkout-config.git            # source 2: the values. It deploys nothing itself
      targetRevision: main
      ref: config                     # gives this repo the nickname "config", so $config works above
  destination:
    server: https://kubernetes.default.svc
    namespace: checkout-prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
git add -A && git commit -qm "checkout: chart from checkout-app, values from checkout-config" && git push -q
argocd app get root --refresh >/dev/null; sleep 20

This command shows the failure. It prints the result of the last sync and keeps only the lines that matter. Expect Phase: Failed, then a Message: that contains field is immutable and mentions spec.selector. That is Kubernetes saying: "this Deployment already exists with a different selector, and a selector cannot be changed after creation." It is not an Argo CD bug, and nothing is broken beyond this one Deployment. If you see Phase: Succeeded, wait 20 seconds and run it again.

argocd app get checkout-dev --show-operation | grep -iE 'immutable|Phase|Message' | head -5

4. Your first immutable field

Kubernetes refuses to change a Deployment's selector. Delete the old Deployment and let auto-sync create the new one. In dev that is fine; in prod it is a short outage, which is why renames like this are planned (a new name, then delete the old one).

Delete the old Deployment in both environments. Auto-sync recreates each one from the new source within seconds:

kubectl -n checkout-dev delete deployment checkout
kubectl -n checkout-prod delete deployment checkout

Wait until both are healthy again:

argocd app wait checkout-dev checkout-prod --sync --health --timeout 240

Check prod. Expect two Source lines (the chart in checkout-app at v1.0.0, the values in checkout-config at main), then Synced and Healthy:

argocd app get checkout-prod | grep -E 'Source|Sync Status|Health'

5. Promote 6.7.1 to prod: a one-line deploy

team-checkout changes one line in the environment repo. No platform change, no chart change.

cd ~/work/checkout-config && git switch -q main && git pull -q

Change in ~/work/checkout-config/helm/prod.yaml

-   tag: "6.7.0"
+   tag: "6.7.1"
git add -A && git commit -qm "checkout: promote 6.7.1 to prod" && git push -q

Let Argo CD see the merge and deploy it:

argocd app get checkout-prod --refresh >/dev/null
argocd app wait checkout-prod --sync --health

The history now ends with the deploy of your one-line change. Expect the newest row to have a different revision from the one above it:

argocd app history checkout-prod | tail -3

Simplified for the course

Here the "application repository" has no code and no CI; the image is the public podinfo. In a real team, CI in checkout-app builds the image, pushes it, and opens the pull request that bumps the tag in checkout-config.

Break it on purpose: Move a release tag

A developer spots a small mistake in release v1.0.0 and "fixes" it in place: a new commit, then the v1.0.0 tag moved onto it. Nobody touches checkout-config.

You practise: tracing a deploy back to a commit when the revision name did not change, and why a tag is only a pointer.

1. Break it

cd ~/work/checkout-app && git switch -q main && git pull -q

Change in ~/work/checkout-app/chart/values.yaml

- progressDeadlineSeconds: 120   # report a failed rollout after 2 minutes
+ progressDeadlineSeconds: 90    # report a failed rollout after 2 minutes
git commit -qam "tweak" && git push -q

Move the v1.0.0 tag onto the new commit and force-push it:

git tag -f v1.0.0 && git push -qf origin v1.0.0

Make Argo CD forget what it cached for the tag:

argocd app get checkout-prod --hard-refresh >/dev/null; sleep 20

2. What you see

kubectl -n checkout-prod get deploy checkout -o jsonpath='{.spec.progressDeadlineSeconds}' prints 90. Prod changed (and so did dev). Expected: nothing reaches prod without a pull request to checkout-config. argocd app history checkout-prod shows a new sync, still of v1.0.0.

3. Fix it yourself

Put v1.0.0 back on the commit that was actually released, so prod runs what was approved again, and take the tweak off main.

Hint 1

Which commit did v1.0.0 point at before? git log --oneline -3 in checkout-app: the tweak is on top, the release is the commit below it.

Hint 2

Point the tag back with git tag -f, force-push the tag, then revert the tweak. Argo CD caches what a tag resolves to: hard-refresh.

The fix

Moving a tag back is the one time you move a tag: to undo a move. The real way to fix a mistake in a release is a new release, v1.0.1, promoted through checkout-config.

Point the tag back at the commit before the tweak, and force-push it:

cd ~/work/checkout-app
git tag -f v1.0.0 HEAD~1 && git push -qf origin v1.0.0

Take the tweak off main too:

git revert --no-edit HEAD && git push -q
argocd app get checkout-prod --hard-refresh >/dev/null; sleep 20

Expect 120 again:

kubectl -n checkout-prod get deploy checkout -o jsonpath='{.spec.progressDeadlineSeconds}'; echo
After you fixed it: the five questions
What failed?
A release changed after it shipped: prod ran code nobody approved for prod.
How was it detected?
Only by noticing a new sync with no matching change in checkout-config. That is the danger.
Who responds?
team-checkout.
How is it corrected?
Point the tag back at the released commit. Ship the change as a new release.
How is it prevented?
Protect release tags on GitHub (tag rulesets) so they cannot be moved or deleted.

Quiz

  1. Which change is a deploy to prod?
    • A change to helm/prod.yaml in checkout-config
    • A merge to main in checkout-app, since that is where the code for the new release lands
    • A new image tag pushed to the registry by the app pipeline, whatever repo it came from
  2. What does ref: config do?
    • It deploys the config repo as an extra Application next to the chart source
    • Names the source so other sources can use files from it as $config/...
    • It pins the chart to a branch named config, so every sync follows that branch
  3. You want one Application for checkout, catalog and Traefik. Good idea?
    • Yes: one Application means fewer objects to manage and a single sync button for everything, so it is the simpler design for a small team
    • Only if all three use Helm: mixed sources need separate Applications, and since they all use charts here one object is enough
    • No: unrelated things lose separate sync, health and history. Use one Application each, grouped by an app of apps
Answers
  1. A change to helm/prod.yaml in checkout-config Only the environment repo (or the Application) decides what runs where. A tag is a release, not a deploy.
  2. Names the source so other sources can use files from it as $config/... A ref source contributes files, not manifests.
  3. No: unrelated things lose separate sync, health and history. Use one Application each, grouped by an app of apps The Argo CD docs warn explicitly against multiple sources as a grouping mechanism.

Field note

Split repos when ownership or cadence differs, not because a blog said so. A single team with one service is fine with one repo. The moment a second team or a compliance review appears, you will be glad the boundaries already exist.

Report a problem with this module