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-appwith the Helm chart and release it asv1.0.0. Put per-environment values incheckout-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, taggedv1.0.0) andcheckout-config(values per environment).checkout-devandcheckout-proddeploy 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. Withsources(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: configgives 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.$configis not a variable you define anywhere else: it is just therefname 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 repo | Environment repo | Platform repo | |
|---|---|---|---|
| Example | checkout-app | checkout-config | argocd-platform |
| Owner | team-checkout | team-checkout | Platform team |
| Holds | Code, CI, Helm chart | Values per environment, sealed secrets | Argo CD config, Applications, projects |
| Argo CD reads | A tag (v1.0.0) | main | main |
| A merge here | Releases, deploys nothing | Deploys | Changes the platform for everyone |
Files to know
Open them in your editor or on GitHub; the bullets say what to look at.
platform/apps/checkout-dev.yaml(reference): The finished Application: three sources (the third arrives in module 08).checkout-app/chart/values.yaml(reference): The chart's defaults, shared by every environment.checkout-config/helm/dev.yaml(reference): Everything specific to dev, in one short file.- Promoting a version is this one line L2–3
docs/repositories.md(reference): The comparison, and the rule about multiple sources.- The argument in one paragraph L13–19
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-appbuilds the image, pushes it, and opens the pull request that bumps the tag incheckout-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
- 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
- What does
ref: configdo?- 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
- 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
- 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.
- Names the source so other sources can use files from it as
$config/...A ref source contributes files, not manifests. - 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.