Hands-On Labs · The Capstone · Part 2 of 5

Capstone Part 2 — Progressive Delivery

This is the second of five parts building one continuous project: cert-tracker, the exam-tracking service Part 1 got running under Argo CD, self-healing and prune both proven live. Every release since Part 1 has meant one thing: edit an image tag in Git, let the reconciler apply it, and let a plain Deployment's RollingUpdate cycle through every pod at once — whether or not the new build was actually safe. Today that changes. This page swaps cert-tracker's Deployment for an Argo Rollout, wires a real weighted traffic split through the NGINX ingress controller cert-tracker already needs, and gates every step on an AnalysisTemplate reading a real metric — not a guess, not a fixed pause. By the end you will have watched a safe release ramp itself from 20% to 100% on its own, and watched a broken one get caught and reversed automatically, with nobody pressing a button either time.

☺ Explain it like I'm 10

Picture Mission Control certifying a redesigned heat shield for the capsule. Nobody straps a full crew onto the new shield's very first flight — they send an uncrewed test capsule through a partial re-entry first, and watch the one sensor reading that actually matters, shield temperature, not just "did the capsule power on." Good numbers for a few minutes, and the next test goes further, then further, until the shield is proven and every future crew rides it. But one bad temperature reading, even for a few seconds, and the test aborts immediately — the capsule reverts to the old, already-proven shield, and nobody waits around hoping the number recovers on its own. Mission Control isn't watching a screen at 2 a.m. for this; a sensor threshold makes that call by itself, instantly, every time.

🦫🐘Your hosts for this part: Benny the Beaver & Ellie the Elephant — Benny writes the Rollout and wires the traffic split, and Ellie makes sure the AnalysisTemplate's query is reading a real, meaningful number before anyone trusts it to gate a release.
⚠ Where you are arriving from, and where you are headed

Arriving: cert-tracker running as a plain Deployment in the cert-tracker namespace, reconciled by the cert-tracker Argo CD Application Part 1 built — prune and selfHeal both on, watching cert-tracker-config at apps/cert-tracker/overlays/dev. Leaving this page: cert-tracker running as a Rollout instead, a real weighted split between a stable and a canary Service driven by the NGINX ingress controller's own built-in canary routing, a namespaced AnalysisTemplate gating every promotion on a real metric read straight from that same ingress controller, and two new ignoreDifferences entries so Argo CD's selfHeal stops fighting the Rollout controller over the one field it now owns. Part 3 picks up here and puts cert-tracker's traffic inside a real mesh; today's ingress split is deliberately the narrowest thing that could work.

What this part assumes, and what it produces

☺ Like you're 10: Just the cluster, the two repos, and the app Part 1 already got Synced and Healthy — nothing about cert-tracker's own code changes today, except two small version bumps at the very end.

This part assumes Part 1's cert-tracker Application reports Synced and Healthy — pods Running in the cert-tracker namespace, prune and selfHeal both proven with your own hands, not just configured. You need everything Part 1 needed — kubectl, Docker, git, the argocd CLI — plus the kubectl argo rollouts plugin. Nothing about the real mesh or the real observability stack gets built yet: the ingress controller and the narrow Prometheus this page installs are deliberately the smallest slice of each that a working canary actually needs, not Part 3's mesh or Part 4's dashboards.

ThingChanges toIntroduced
Workload kindDeploymentRolloutPart 1 → Part 2 — this page
Traffic routingnone (single Service) → NGINX ingress canary split, cert-tracker-stable / cert-tracker-canaryPart 2 — this page
Promotion gatereadiness probe only → AnalysisTemplate cert-tracker-success-ratePart 2 — this page
Metrics sourcenone → a narrow, single-scrape Prometheus reading the ingress controller's own request metricsPart 2 — this page (Part 4 replaces this with real app-level OpenTelemetry instrumentation)
Mesh & policynot yet installedstill Part 3
Full observabilitynot yet wiredstill Part 4
◆ A few commands here are still run by hand — here's why that's consistent with Part 1

Part 1 called applying project.yaml and application.yaml "the one and only manual apply in this whole capstone" — and that claim was about cert-tracker's own desired state, the thing cert-tracker-config's apps/cert-tracker path governs. Installing the Argo Rollouts controller, the NGINX ingress controller, and a narrow Prometheus sits in a different category: platform infrastructure, applied by hand once, exactly like Argo CD's own install in Part 1. Later, adding an ignoreDifferences entry means editing the Application object's own spec — and nothing reconciles bootstrap/ the way the Application reconciles apps/cert-tracker, so that's a second by-hand moment in the same bootstrap category, not a new one. Everything about cert-tracker's actual desired state — the Rollout, both Services, the Ingress, the AnalysisTemplate — still only ever moves through a git push to cert-tracker-config, exactly as promised.

Platform bootstrap: the ingress controller, Argo Rollouts, and a narrow Prometheus

☺ Like you're 10: Three pieces of building infrastructure go in once, by hand, before the app itself ever notices anything changed.

Install the NGINX ingress controller first — cert-tracker's canary split rides entirely on its built-in canary annotations, and it is not a service mesh, so installing it doesn't front-run Part 3. The kind-flavored manifest below matches Part 1's assumed local cluster; swap it for minikube addons enable ingress if that's what you're running instead:

kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/kind/deploy.yaml
kubectl -n ingress-nginx rollout status deploy/ingress-nginx-controller --timeout=180s

kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
kubectl -n argo-rollouts rollout status deploy/argo-rollouts --timeout=180s

curl -fSL -o kubectl-argo-rollouts \
  https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts && sudo mv kubectl-argo-rollouts /usr/local/bin/

Then a Prometheus deliberately narrow enough to answer one query: it scrapes only the ingress controller's own :10254/metrics endpoint, nothing app-level yet.

# platform/prometheus.yaml — applied by hand, not tracked in cert-tracker-config
apiVersion: v1
kind: ConfigMap
metadata:
  name: prometheus-config
  namespace: monitoring
data:
  prometheus.yml: |
    scrape_configs:
      - job_name: ingress-nginx
        static_configs:
          - targets: ["ingress-nginx-controller-metrics.ingress-nginx.svc.cluster.local:10254"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: prometheus
  namespace: monitoring
spec:
  replicas: 1
  selector: { matchLabels: { app: prometheus } }
  template:
    metadata: { labels: { app: prometheus } }
    spec:
      containers:
        - name: prometheus
          image: prom/prometheus:v2.54.1
          args: ["--config.file=/etc/prometheus/prometheus.yml"]
          volumeMounts: [{ name: config, mountPath: /etc/prometheus }]
      volumes:
        - name: config
          configMap: { name: prometheus-config }
---
apiVersion: v1
kind: Service
metadata:
  name: prometheus
  namespace: monitoring
spec:
  selector: { app: prometheus }
  ports: [{ port: 9090, targetPort: 9090 }]
kubectl create namespace monitoring
kubectl apply -f platform/prometheus.yaml
kubectl -n monitoring rollout status deploy/prometheus --timeout=120s

The Rollout: swapping the workload kind without touching the pod template

☺ Like you're 10: Swapping the label on the box from Deployment to Rollout — everything already inside stays exactly the same, only what happens on the next release changes.

In cert-tracker-config, replace apps/cert-tracker/base/deployment.yaml with rollout.yaml. The pod template is copied verbatim from Part 1 — same image, same probes — and everything new lives in spec.strategy:

# apps/cert-tracker/base/rollout.yaml — replaces deployment.yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: cert-tracker
  labels: { app: cert-tracker }
spec:
  replicas: 2
  selector:
    matchLabels: { app: cert-tracker }
  template:                              # identical to Part 1's Deployment — nothing here changes
    metadata:
      labels: { app: cert-tracker }
    spec:
      containers:
        - name: cert-tracker
          image: ghcr.io/kubestronaut/cert-tracker:v0.1.0
          ports: [{ containerPort: 8080 }]
          readinessProbe: { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 3 }
          livenessProbe: { httpGet: { path: /healthz, port: 8080 }, initialDelaySeconds: 10 }
  strategy:
    canary:
      canaryService: cert-tracker-canary   # selectors Argo Rollouts manages for you — see below
      stableService: cert-tracker-stable
      trafficRouting:
        nginx:
          stableIngress: cert-tracker       # the Ingress it clones into a canary variant
      analysis:
        templates:
          - templateName: cert-tracker-success-rate   # namespaced — see why, below
        startingStep: 1
        args:
          - name: canary-service
            value: cert-tracker-canary
      steps:
        - setWeight: 20
        - pause: { duration: 2m }
        - setWeight: 50
        - pause: { duration: 2m }
        - setWeight: 100
◆ Namespaced AnalysisTemplate, on purpose

Argo Rollouts' own reference page shows a ClusterAnalysisTemplate — the cluster-scoped variant, published once and referenced from any namespace. This page uses the namespaced AnalysisTemplate kind instead, deliberately: Part 1's kubestronaut AppProject set clusterResourceWhitelist: [], which blocks the project from creating any cluster-scoped resource at all. A ClusterAnalysisTemplate would trip that whitelist the moment Argo CD tried to apply it. There's no reason cert-tracker's own analysis needs to be cluster-wide yet — a plain, namespaced AnalysisTemplate living in cert-tracker alongside everything else does the identical job without asking for a permission this app doesn't need.

The rest of the base directory grows by four files — two Services Argo Rollouts will manage the selectors of, the stable Ingress its NGINX integration clones, and the AnalysisTemplate itself:

# apps/cert-tracker/base/service-stable.yaml
apiVersion: v1
kind: Service
metadata:
  name: cert-tracker-stable
spec:
  selector: { app: cert-tracker }   # Argo Rollouts narrows this with a pod-template-hash label at sync time
  ports: [{ port: 80, targetPort: 8080 }]
---
# apps/cert-tracker/base/service-canary.yaml — identical shape, different name
apiVersion: v1
kind: Service
metadata:
  name: cert-tracker-canary
spec:
  selector: { app: cert-tracker }
  ports: [{ port: 80, targetPort: 8080 }]
---
# apps/cert-tracker/base/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: cert-tracker
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: cert-tracker.kubestronaut.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: cert-tracker-stable, port: { number: 80 } }
---
# apps/cert-tracker/base/analysistemplate.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: cert-tracker-success-rate
spec:
  args:
    - name: canary-service
  metrics:
    - name: success-rate
      interval: 30s
      count: 6
      initialDelay: 30s                  # let the canary warm up before judging it
      successCondition: result[0] >= 0.98
      failureLimit: 2                    # tolerate 2 bad readings; the 3rd fails the run
      provider:
        prometheus:
          address: http://prometheus.monitoring.svc.cluster.local:9090
          query: |
            sum(rate(nginx_ingress_controller_requests{
                  service="{{args.canary-service}}", status!~"5.."}[2m]))
            /
            sum(rate(nginx_ingress_controller_requests{
                  service="{{args.canary-service}}"}[2m]))

Update base/kustomization.yaml's resources list to swap deployment.yaml for the five files above, and update overlays/dev/patch-replicas.yaml's target.kind from Deployment to Rollout — the patch still targets the same resource name, only the kind changed. Commit both, plus every new file, and push:

cd cert-tracker-config
git rm apps/cert-tracker/base/deployment.yaml
# add rollout.yaml, service-stable.yaml, service-canary.yaml, ingress.yaml, analysistemplate.yaml
# edit base/kustomization.yaml and overlays/dev/patch-replicas.yaml as described above
git add .
git commit -m "feat: cert-tracker as a canary Rollout, gated on a real ingress metric"
git push

argocd app sync cert-tracker      # or wait for the next ~3min poll
argocd app get cert-tracker       # project: kubestronaut, still — nothing cluster-scoped got added
⚠ The very first Rollout skips the canary entirely

Watch kubectl argo rollouts get rollout cert-tracker -n cert-tracker --watch right after this sync and the weight jumps straight to 100% — no 20%, no pause, no analysis run. This is expected, not broken: there is no stable version yet to canary against on a brand-new Rollout object, so the very first revision always goes straight to full traffic. The canary machinery only engages starting with the next pod-template change, which is exactly what the next section ships.

Git cert-tracker-config Argo CD Application applies Rollout, Ingress, both Services ignores: Service .spec.selector 🦫 Rollout controller argo-rollouts ns walks the canary steps NGINX ingress stable Ingress (Git) canary Ingress Rollout-owned, not in Git :10254/metrics cert-tracker-stable selector: live-managed cert-tracker-canary selector: live-managed AnalysisRun every 30s · count 6 Prometheus scrapes ingress metrics push apply owns + patches weight, live selector managed live — ignored by Argo CD query fail → abort fail → weight snaps to 0, the Rollout-owned canary Ingress is removed, stable serves 100% — Argo CD never touches this reversal, because it never owned the canary Ingress in the first place.

Keeping Argo CD out of the Rollout controller's own field

☺ Like you're 10: Two robots reaching for the same dial — one has to be told "not this one, that one's the other robot's job."

cert-tracker-stable and cert-tracker-canary are plain manifests under Git, applied and continuously reconciled by the same cert-tracker Application as everything else. The moment a rollout starts, the Rollout controller narrows each Service's spec.selector with its own rollouts-pod-template-hash label, live, so each one points at exactly the right ReplicaSet — a runtime edit, not a Git commit. With selfHeal: true still on from Part 1, the very next reconcile pass sees a live selector that no longer matches Git's checked-in { app: cert-tracker } and — this is external-owner drift, the exact shape that page already named, not a mistake to revert — would fight the Rollout controller for it, forever, unless told otherwise. Add the carve-out to bootstrap/application.yaml:

# cert-tracker-config/bootstrap/application.yaml — spec.ignoreDifferences added
spec:
  ignoreDifferences:
    - group: ''
      kind: Service
      name: cert-tracker-stable
      jsonPointers: [/spec/selector]
    - group: ''
      kind: Service
      name: cert-tracker-canary
      jsonPointers: [/spec/selector]
cd cert-tracker-config
git commit -am "fix: ignoreDifferences on the two Services Rollouts now owns"
git push                                          # keep the change in history, same as everything else

kubectl apply -n argocd -f bootstrap/application.yaml   # nothing reconciles bootstrap/ automatically — apply it once, by hand

The cert-tracker-canary Ingress that NGINX-mode Rollouts creates needs no such carve-out at all — it is a resource Argo CD never applied in the first place, generated and owned entirely by the Rollout controller, and it never appears anywhere in cert-tracker-config's rendered manifests. Argo CD can only prune or revert resources it tracks; an object it never created is invisible to its diff, full stop. That's a structural difference from the Istio-based routing Argo Rollouts also documents, where the controller patches weight fields directly onto a single VirtualService Argo CD does track — a genuine two-controller conflict over one object, needing its own ignoreDifferences entry on the weight fields themselves. The NGINX router sidesteps that particular fight structurally; the Service selectors are the one field this setup still has to hand off explicitly.

Proving it: a safe release promotes itself, a broken one catches itself

☺ Like you're 10: Send the safe test capsule through first and watch it get waved further each time it reports clean numbers — then send a flawed one and watch the exact same watchdog say no.

Ship a small, safe change first — v0.1.1 adds a version field to the /healthz response, nothing that touches /exams or /status at all:

docker build -t ghcr.io/kubestronaut/cert-tracker:v0.1.1 .
docker push ghcr.io/kubestronaut/cert-tracker:v0.1.1
# edit the image tag in apps/cert-tracker/base/rollout.yaml
git commit -am "feat: report version in /healthz response (v0.1.1)"
git push

kubectl argo rollouts get rollout cert-tracker -n cert-tracker --watch
# ...a couple minutes in:
# SetWeight  20    Steps  1/5
# AnalysisRun cert-tracker-abc12  Running  (success-rate: measuring...)
# SetWeight  50    Steps  3/5
# AnalysisRun cert-tracker-abc12  Successful  (success-rate: 1.0)
# SetWeight  100   Steps  5/5
# Rollout    cert-tracker  Healthy

Now ship the build worth proving against — not one that crashes outright, since a crashed process fails its readiness probe and never receives a single request, canary or not. v0.2.0 adds pagination to GET /exams behind a new query-parsing helper that throws on a malformed parameter, outside Express's own error handling, and takes the whole process down with it. /healthz never touches that code path, so kubelet keeps calling the pod ready between crash-restarts — but a meaningful slice of real /exams traffic during any given window lands mid-crash and gets a connection reset, which the ingress controller's own metrics record honestly as a 5xx:

kubectl argo rollouts set image cert-tracker cert-tracker=ghcr.io/kubestronaut/cert-tracker:v0.2.0 -n cert-tracker
kubectl argo rollouts get rollout cert-tracker -n cert-tracker --watch

# SetWeight  20    Steps  1/5
# AnalysisRun cert-tracker-def45  Running  (success-rate: measuring...)
# AnalysisRun cert-tracker-def45  Failed   (success-rate: 0.91, want >= 0.98)
# Rollout    cert-tracker  Degraded

kubectl get analysisrun -n cert-tracker
kubectl describe analysisrun cert-tracker-def45 -n cert-tracker
# Message: Metric "success-rate" assessed Failed due to failed (2) > failureLimit (2)

argocd app get cert-tracker
# Sync Status: Synced   -- still. The spec Argo CD applied still matches Git; the abort lives in the Rollout's status.

That last command is the entire point of this section, not a side note. cert-tracker's traffic never left the 20% ceiling this test held it to, the abort happened with nobody watching a dashboard, and the Argo CD Application shows exactly the same Synced it always does — proof that Rollout health and Application sync status are answering two different questions, exactly as Argo Rollouts already warned. Once a real fix ships as v0.2.1, kubectl argo rollouts retry rollout cert-tracker -n cert-tracker clears the abort and re-runs the same steps against it.

⚠ Background analysis doesn't stop when the steps do

cert-tracker-success-rate is declared at strategy.canary.analysis, outside steps — a background analysis, not an inline one. It starts the moment the canary receives any traffic at all and keeps sampling for its full count of measurements independent of which step the rollout is currently on. That cuts both ways: a regression that only shows up once the weight reaches 50% still gets caught, not just whatever the first step happened to check — but it also means an analysis can still be running, and can still trigger an abort, even after the rollout has already reached setWeight: 100 and the steps list is exhausted. "Fully promoted" and "fully judged" are not the same moment.

What "done" looks like for Part 2, and where Part 3 picks up

☺ Like you're 10: A capsule design that tests itself in small, growing steps and pulls itself back the instant a real reading looks wrong — proven twice, not assumed once.

At the end of this part: cert-tracker runs as a Rollout, gated by a namespaced AnalysisTemplate reading a real metric off the NGINX ingress controller; a weighted split between cert-tracker-stable and cert-tracker-canary moves real traffic in steps; the Application carries the two ignoreDifferences entries that keep selfHeal from fighting the Rollout controller; and you've watched, with your own commands, both a safe release promote itself and a broken one get caught and reversed. Nothing here gets thrown away:

PartWhat it does with Part 2's artifacts
3 — Mesh & PolicyPuts this same Rollout's traffic inside a real mesh, and adds Kyverno policies gating what cert-tracker-config is even allowed to declare
4 — ObservabilityReplaces today's one narrow, ingress-only Prometheus with real OpenTelemetry traces and app-level metrics across every route
5 — The PortalCatalogs cert-tracker's Rollout status in Backstage, right alongside the repo pair Part 1 built

Milestones

☺ Like you're 10: Tick each box only once you've actually watched it happen on your own screen, not because the step "sounds right."

Work these in order — each depends on the cluster and repo state from the one before. Progress saves in this browser.

0 / 11 milestones complete
1Confirm Part 1's state: cert-tracker Synced and Healthy
argocd app get cert-tracker — both prune and selfHeal already proven with your own hands.
Done when: the Application reports Synced / Healthy before you change anything.
2Platform bootstrap: install the NGINX ingress controller, Argo Rollouts, and the narrow Prometheus
The three kubectl apply sequences above, by hand — same category as installing Argo CD itself.
Done when: ingress-nginx-controller, argo-rollouts, and prometheus all report Ready.
Concept: Argo Rollouts
3Write rollout.yaml, replacing deployment.yaml
The pod template stays identical; the new strategy.canary block is what's new.
Done when: the file is committed locally, pod template unchanged from Part 1's Deployment.
4Add service-stable.yaml, service-canary.yaml, ingress.yaml
All three exactly as shown above.
Done when: all three files exist and validate with kubectl apply --dry-run=client -f.
Concept: Traffic routers — who actually moves the packets
5Add the namespaced AnalysisTemplate cert-tracker-success-rate
The exact manifest above — no ClusterAnalysisTemplate, given Part 1's empty clusterResourceWhitelist.
Done when: the file exists and its provider.prometheus.address matches the Prometheus Service from step 2.
Concept: AnalysisTemplate vs. AnalysisRun
6Update kustomization.yaml and the overlay's patch target
Swap deployment.yaml for the five new files in base/kustomization.yaml; change patch-replicas.yaml's target.kind to Rollout.
Done when: kustomize build apps/cert-tracker/overlays/dev renders clean YAML with no leftover Deployment.
Concept: State store scope
7Commit, push, and watch the first Rollout skip straight to 100%
git push, then argocd app sync cert-tracker, then kubectl argo rollouts get rollout cert-tracker -n cert-tracker --watch.
Done when: you've watched it — no canary steps ran, because there was no stable version yet.
Concept: The very first rollout skips the steps entirely
8Add the two ignoreDifferences entries to bootstrap/application.yaml
Commit and push for history, then kubectl apply -n argocd -f bootstrap/application.yaml by hand.
Done when: argocd app get cert-tracker -o yaml shows both entries under ignoreDifferences.
9Ship v0.1.1 and watch a real canary promote itself: 20 → 50 → 100
Build, push, bump the tag in rollout.yaml, commit, push, then watch it.
Done when: the Rollout reaches Healthy at setWeight: 100 with no manual promote.
Concept: Automated analysis & rollback
10Ship v0.2.0 and watch the analysis catch it and abort
kubectl argo rollouts set image, then watch the AnalysisRun fail and confirm argocd app get cert-tracker still shows Synced.
Done when: the Rollout sits Degraded, traffic never exceeded 20%, and the Application sync status never moved.
Concept: Aborting a bad rollout
11Say out loud what state you're leaving for Part 3
Confirm: cert-tracker is a Rollout, the ingress split and analysis both proven live, both ignoreDifferences entries in place.
Done when: you can describe this state without looking anything up — it's the exact starting point Part 3 assumes.
🎬 At Mission Control
🦫

Benny the Beaver: Rollout's live. First deploy went straight to 100%, no canary — a little anticlimactic.

🐘

Ellie the Elephant: Because there was nothing to compare it against yet. Ship the next version and watch the steps actually run — I want to see the AnalysisRun read a real number, not just exist.

🦊

Foxy: And if the number's bad?

🐘

Ellie: Then it aborts, on its own, before more than 20% of real traffic ever touched the broken build. I don't page anyone for that — the query already knows.

👺

Gizmo: Or — hot take — just skip straight to setWeight: 100 every time. It passed /healthz, right? 😈

🦫

Benny: /healthz means the process is breathing, Gizmo. It says nothing about whether /exams actually works under real load. That's the whole reason the query watches real requests, not a heartbeat.

🤖

Recon the Robot: BEEP. And don't come looking to my sync status for the alarm either. I'll show Synced the entire time the Rollout's Degraded — that word's his, not mine.

🐢 Timmy's checkpoint

1. Why does the very first Rollout apply on this page skip the canary steps entirely, even though strategy.canary is already fully wired? 2. Why does this page use the namespaced AnalysisTemplate kind instead of the ClusterAnalysisTemplate the Argo Rollouts reference page shows? 3. Why doesn't the Rollout-owned canary Ingress need an ignoreDifferences entry, while the two split Services do? 4. Why does v0.2.0 still pass its readiness probe even while the analysis is aborting the rollout? 5. After the abort, what does argocd app get cert-tracker show, and why doesn't that contradict the Rollout sitting Degraded? 6. Name the platform-level things this page installed by hand, and why that doesn't break Part 1's "one and only manual apply" claim.

Check your answers
  1. Because there is no existing stable version to canary against on a brand-new Rollout object — the very first revision always ships straight to 100%. The steps only engage starting with the next change to the pod template.
  2. Part 1's kubestronaut AppProject set clusterResourceWhitelist: [], which blocks the project from creating any cluster-scoped resource — and ClusterAnalysisTemplate is cluster-scoped. A namespaced AnalysisTemplate living in cert-tracker does the identical job without needing a permission this app was never granted.
  3. Argo CD can only prune or revert resources it actually applied and tracks. The canary Ingress is created and owned entirely by the Rollout controller and never appears in cert-tracker-config's rendered manifests, so it's invisible to Argo CD's diff by construction — there's no live/Git mismatch to fight over in the first place. The two Services, by contrast, are Git-tracked and get their selector patched live, which is exactly the kind of field two controllers can fight over without a carve-out.
  4. The bug lives in a new query-parsing helper on GET /exams that throws outside Express's own error handling — a code path /healthz never touches. A readiness probe only proves the process is alive and listening; it says nothing about whether a specific, newer request path actually works.
  5. Sync Status: Synced — unchanged. The spec Argo CD applied still matches Git exactly; the abort is recorded in the Rollout's own status, not its spec, so sync status (does live match Git?) and Rollout health (is it actually working?) are simply answering two different questions.
  6. The NGINX ingress controller, the Argo Rollouts controller, and the narrow Prometheus — plus, later, the one kubectl apply needed to update the Application's own ignoreDifferences. All of these are platform infrastructure or edits to the Application object itself, the same category Part 1 already put its own two bootstrap manifests in — not part of cert-tracker's desired state, which still only ever moves through cert-tracker-config.

Part 2 turned a plain Deployment into a release that promotes itself on good numbers and reverses itself on bad ones, with nobody watching a dashboard either way. Continue to Capstone Part 3 — Mesh & Policy, where this same Rollout's traffic moves inside a real mesh. Or step back to Build Your Cert Tracker — Start Here to see how this capstone's five parts fit together, and revisit Argo Rollouts and GitOps Philosophy for the concepts behind what you just built.