Two repositories — the app repo with a Helm chart and a CI job that builds, pushes and opens a pull request against the config repo, and the config repo with Kustomize overlays for dev and prod that Argo CD syncs to a kind cluster. Argo CD's UI shows the app, the monitoring stack and their health; a manual kubectl edit is reverted automatically; a bad release is rolled back by reverting a commit; prod only changes after dev has been synced and healthy.
- The zero-to-production project (its image on GHCR is what you deploy), or any container image with a
/healthand/metricsendpoint - The DevOps track's Kubernetes, Helm and GitOps modules
- Docker Desktop, kind, kubectl, Helm and the GitHub CLI installed; 6 GB RAM free for the cluster
- kind — a local Kubernetes cluster in Docker, rebuilt in a minute ↗
- Helm — packages the app with values per environment ↗
- Kustomize — environment overlays in the config repo without templating everything ↗
- Argo CD — continuous reconciliation of the cluster to git, with a UI that shows drift and health ↗
- kube-prometheus-stack — Prometheus, Alertmanager and Grafana as one chart, deployed by Argo CD like everything else ↗
zero-to-prod/ # app repo (existing)
├── chart/
│ ├── Chart.yaml
│ ├── values.yaml
│ └── templates/ (deployment, service, ingress, servicemonitor, hpa)
└── .github/workflows/release.yml # + bump the digest in the config repo via PR
zero-to-prod-config/ # config repo (new): the desired state
├── apps/
│ └── orders-api/
│ ├── base/kustomization.yaml # HelmChart inflation from the app repo's chart
│ ├── dev/kustomization.yaml # image digest + dev values
│ └── prod/kustomization.yaml # image digest + prod values
├── platform/
│ ├── monitoring/ # kube-prometheus-stack Application
│ └── ingress-nginx/
├── argocd/
│ ├── project.yaml # AppProject with allowed repos and namespaces
│ ├── app-of-apps.yaml # root Application pointing at apps/ and platform/
│ └── applications/*.yaml
└── README.mdTick each step as you finish it — your progress is saved in this browser only (back up or restore on the hub). Every code block has a copy button. If something goes wrong, the troubleshooting section at the end covers the usual suspects.
Cluster and Argo CD
A kind cluster with ingress, Argo CD installed and reachable, and the CLI logged in.
- Create a kind cluster with port mappings for the ingress controller, so services are reachable at
localhost.bashcat > kind.yaml <<'EOF' kind: Cluster apiVersion: kind.x-k8s.io/v1alpha4 nodes: - role: control-plane kubeadmConfigPatches: - | kind: InitConfiguration nodeRegistration: kubeletExtraArgs: node-labels: "ingress-ready=true" extraPortMappings: - containerPort: 80 hostPort: 80 - containerPort: 443 hostPort: 443 - role: worker EOF kind create cluster --name gitops --config kind.yaml kubectl cluster-info --context kind-gitopsCheck:kubectl get nodesshows two Ready nodes. - Install the ingress-nginx controller for kind (this one is installed by hand; Argo CD will manage everything after it exists), then Argo CD.bash
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.12.0/deploy/static/provider/kind/deploy.yaml kubectl -n ingress-nginx wait --for=condition=ready pod -l app.kubernetes.io/component=controller --timeout=180s kubectl create namespace argocd kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v2.13.3/manifests/install.yaml kubectl -n argocd wait --for=condition=available deploy/argocd-server --timeout=300sArgo CD publishes a pinned install manifest per release; use the current stable tag if v2.13 has moved on. The rest of the guide uses features that have existed since 2.x. - Expose the Argo CD UI through an Ingress (insecure mode behind the ingress, which is fine for a local lab) and log in with the CLI.bash
kubectl -n argocd patch configmap argocd-cmd-params-cm --type merge -p '{"data":{"server.insecure":"true"}}' kubectl -n argocd rollout restart deploy/argocd-server cat <<'EOF' | kubectl apply -f - apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: argocd namespace: argocd annotations: nginx.ingress.kubernetes.io/backend-protocol: "HTTP" spec: ingressClassName: nginx rules: - host: argocd.localtest.me http: paths: - path: / pathType: Prefix backend: service: name: argocd-server port: number: 80 EOF brew install argocd # or the release binary on Linux/Windows PASS=$(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath='{.data.password}' | base64 -d) argocd login argocd.localtest.me --username admin --password "$PASS" --plaintext argocd account update-password --current-password "$PASS" --new-password 'pick-a-long-one'Check: http://argocd.localtest.me opens the UI (localtest.meresolves to 127.0.0.1);argocd app listreturns an empty list.localtest.meand its subdomains resolve to localhost without any hosts-file edits.
A Helm chart for the app
The zero-to-production API packaged as a chart with sensible defaults, probes, resources, a ServiceMonitor and per-environment values, tested locally with Helm before Argo CD ever sees it.
- In the app repository, scaffold a chart and replace the generated templates with a minimal, explicit set.bash
cd zero-to-prod && git switch -c feat/helm-chart helm create chart && rm -rf chart/templates/* chart/charts chart/values.yaml - Write
Chart.yamlandvalues.yaml. The image is referenced by digest, and the values file is what the config repo overrides per environment.yaml# chart/Chart.yaml apiVersion: v2 name: orders-api description: The zero-to-production FastAPI service type: application version: 0.1.0 appVersion: "0.1.0" # chart/values.yaml image: repository: ghcr.io/YOUR_GITHUB_USER/zero-to-prod digest: "" # set per environment by the config repo; empty means :latest for local testing tag: latest replicaCount: 2 service: port: 80 targetPort: 8000 ingress: enabled: true className: nginx host: orders.localtest.me resources: requests: {cpu: 100m, memory: 128Mi} limits: {memory: 256Mi} env: GREETING: "hello from kubernetes" metrics: serviceMonitor: true autoscaling: enabled: false minReplicas: 2 maxReplicas: 6 targetCPU: 70 - Write the Deployment template with probes, the restricted security context and the image reference logic.helm
# chart/templates/deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: {{ .Release.Name }} labels: {app.kubernetes.io/name: {{ .Chart.Name }}, app.kubernetes.io/instance: {{ .Release.Name }}} spec: replicas: {{ .Values.replicaCount }} selector: matchLabels: {app.kubernetes.io/instance: {{ .Release.Name }}} template: metadata: labels: {app.kubernetes.io/name: {{ .Chart.Name }}, app.kubernetes.io/instance: {{ .Release.Name }}} spec: securityContext: runAsNonRoot: true runAsUser: 10001 seccompProfile: {type: RuntimeDefault} containers: - name: app image: "{{ .Values.image.repository }}{{ if .Values.image.digest }}@{{ .Values.image.digest }}{{ else }}:{{ .Values.image.tag }}{{ end }}" ports: [{name: http, containerPort: 8000}] env: {{- range $k, $v := .Values.env }} - name: {{ $k }} value: {{ $v | quote }} {{- end }} readinessProbe: httpGet: {path: /health, port: http} periodSeconds: 5 livenessProbe: httpGet: {path: /health, port: http} periodSeconds: 15 resources: {{- toYaml .Values.resources | nindent 12 }} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: {drop: [ALL]}Addservice.yaml,ingress.yaml(host from values, path/),hpa.yaml(guarded by.Values.autoscaling.enabled) andservicemonitor.yaml(guarded by.Values.metrics.serviceMonitor, selecting the Service, endpointhttp, path/metrics). The ServiceMonitor's API group ismonitoring.coreos.com/v1, which exists once the monitoring stack is installed in Phase 4; keep it disabled in dev values until then. - Add the remaining templates: the Service, the Ingress and the ServiceMonitor. Each is small; the guard on the ServiceMonitor lets the chart install before the monitoring CRDs exist.helm
# chart/templates/service.yaml apiVersion: v1 kind: Service metadata: name: {{ .Release.Name }} labels: {app.kubernetes.io/instance: {{ .Release.Name }}} spec: selector: {app.kubernetes.io/instance: {{ .Release.Name }}} ports: - name: http port: {{ .Values.service.port }} targetPort: http --- # chart/templates/ingress.yaml {{- if .Values.ingress.enabled }} apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: {{ .Release.Name }} spec: ingressClassName: {{ .Values.ingress.className }} rules: - host: {{ .Values.ingress.host }} http: paths: - path: / pathType: Prefix backend: service: name: {{ .Release.Name }} port: {name: http} {{- end }} --- # chart/templates/servicemonitor.yaml {{- if .Values.metrics.serviceMonitor }} apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: name: {{ .Release.Name }} labels: {release: monitoring} spec: selector: matchLabels: {app.kubernetes.io/instance: {{ .Release.Name }}} endpoints: - port: http path: /metrics interval: 15s {{- end }}Check:helm template orders-api chartrenders four documents (Deployment, Service, Ingress, ServiceMonitor) with no template errors. - Lint and render the chart, then install it into the cluster by hand once to prove it works — the last manual install you will do.bash
helm lint chart helm template orders-api chart --set metrics.serviceMonitor=false | kubectl apply --dry-run=server -f - helm upgrade --install orders-api chart -n orders --create-namespace --set metrics.serviceMonitor=false kubectl -n orders rollout status deploy/orders-api curl -s http://orders.localtest.me/healthCheck:{"status":"ok"}. Uninstall afterwards (helm uninstall orders-api -n orders) so Argo CD owns it from here. - Add a chart test job in CI (
helm lintand a server-side dry run against a kind cluster in the workflow) and merge the chart through a pull request.bashgit add chart && git commit -m "feat: Helm chart for orders-api" && git push -u origin feat/helm-chart gh pr create --fill && gh pr merge --squash --delete-branch && git switch main && git pullFor the CI job usehelm/kind-action@v1to spin up a cluster, then the same two commands as above.
The config repository and Argo CD Applications
A separate repository describing dev and prod as Kustomize overlays of the chart, and Argo CD Applications that sync them, created through an app-of-apps so even the Applications are in git.
- Create the config repository. Separating configuration from application code is the GitOps convention: the app repo changes when code changes, the config repo changes when what runs changes, and the second is what Argo CD watches.bash
cd .. && gh repo create zero-to-prod-config --public --clone && cd zero-to-prod-config mkdir -p apps/orders-api/{base,dev,prod} platform/{monitoring,ingress-nginx} argocd/applications - Write the base as a Kustomize HelmChart inflation pointing at the chart in the app repository, and the environment overlays that pin the image digest and set environment-specific values.yaml
# apps/orders-api/base/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization helmCharts: - name: orders-api repo: oci://ghcr.io/YOUR_GITHUB_USER/charts # publish the chart as an OCI artifact from CI (Phase 5) version: 0.1.0 releaseName: orders-api namespace: orders valuesFile: values.yaml # apps/orders-api/dev/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: orders-dev resources: [../base] patches: - target: {kind: Deployment, name: orders-api} patch: | - op: replace path: /spec/replicas value: 1 images: - name: ghcr.io/YOUR_GITHUB_USER/zero-to-prod digest: sha256:REPLACE_WITH_A_REAL_DIGEST # apps/orders-api/prod/kustomization.yaml apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization namespace: orders-prod resources: [../base] images: - name: ghcr.io/YOUR_GITHUB_USER/zero-to-prod digest: sha256:REPLACE_WITH_A_REAL_DIGESTUntil the chart is published to an OCI registry in Phase 5, use a local path instead: copychart/intoapps/orders-api/base/chartand reference it withhelmCharts[0].repoomitted andname: chart. Get a real digest withdocker buildx imagetools inspect ghcr.io/YOUR_GITHUB_USER/zero-to-prod:latest. Set the ingress host per environment in each overlay's values (orders-dev.localtest.me,orders.localtest.me). - Define the Argo CD project (what may be deployed where) and the Applications for both environments. Dev syncs automatically with pruning and self-heal; prod syncs automatically too but only after a change lands in its own directory, which the release flow gates.yaml
# argocd/project.yaml apiVersion: argoproj.io/v1alpha1 kind: AppProject metadata: name: zero-to-prod namespace: argocd spec: sourceRepos: - https://github.com/YOUR_GITHUB_USER/zero-to-prod-config - https://prometheus-community.github.io/helm-charts - oci://ghcr.io/YOUR_GITHUB_USER/charts destinations: - {namespace: "orders-*", server: https://kubernetes.default.svc} - {namespace: monitoring, server: https://kubernetes.default.svc} clusterResourceWhitelist: - {group: "", kind: Namespace} - {group: apiextensions.k8s.io, kind: CustomResourceDefinition} - {group: rbac.authorization.k8s.io, kind: ClusterRole} - {group: rbac.authorization.k8s.io, kind: ClusterRoleBinding} # argocd/applications/orders-api-dev.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: orders-api-dev namespace: argocd spec: project: zero-to-prod source: repoURL: https://github.com/YOUR_GITHUB_USER/zero-to-prod-config targetRevision: main path: apps/orders-api/dev destination: {server: https://kubernetes.default.svc, namespace: orders-dev} syncPolicy: automated: {prune: true, selfHeal: true} syncOptions: [CreateNamespace=true] # argocd/applications/orders-api-prod.yaml: same with name orders-api-prod, path apps/orders-api/prod, # namespace orders-prod, and syncPolicy.automated.selfHeal: true, prune: false (prod deletions are manual)Argo CD needs Kustomize's Helm support enabled: patchargocd-cmwithkustomize.buildOptions: --enable-helm, then restart the repo server. - Write the app-of-apps root Application, apply only that one manually, and watch Argo CD create the rest from the repository.yaml
# argocd/app-of-apps.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: root namespace: argocd spec: project: default source: repoURL: https://github.com/YOUR_GITHUB_USER/zero-to-prod-config targetRevision: main path: argocd directory: {recurse: true, exclude: app-of-apps.yaml} destination: {server: https://kubernetes.default.svc, namespace: argocd} syncPolicy: automated: {prune: true, selfHeal: true} - Before committing, render each overlay locally exactly as Argo CD will, so a Kustomize or Helm mistake is caught on your laptop rather than as a red Application.bash
kustomize build --enable-helm apps/orders-api/dev | kubectl apply --dry-run=server -f - kustomize build --enable-helm apps/orders-api/prod | grep -A1 'image:'Check: Both overlays render; the prod output shows the image with the@sha256:digest you pinned. - Commit the config repository, enable Kustomize's Helm support, and bootstrap.bash
git add . && git commit -m "feat: orders-api dev/prod overlays, AppProject, Applications, app-of-apps" && git push -u origin main kubectl -n argocd patch configmap argocd-cm --type merge -p '{"data":{"kustomize.buildOptions":"--enable-helm"}}' kubectl -n argocd rollout restart deploy/argocd-repo-server kubectl apply -f argocd/app-of-apps.yaml argocd app list argocd app wait orders-api-dev --health --timeout 300Check:argocd app listshowsroot,orders-api-devandorders-api-prodas Synced and Healthy;curl http://orders-dev.localtest.me/healthworks; the UI shows the tree of Deployment, ReplicaSet, Pods, Service and Ingress under each app.
The monitoring stack, the same way
Prometheus, Alertmanager and Grafana installed and upgraded by Argo CD from a chart reference in git, scraping the app through its ServiceMonitor.
- Add a multi-source Application for kube-prometheus-stack: the chart from the community repository, the values from your config repository.yaml
# argocd/applications/monitoring.yaml apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: monitoring namespace: argocd spec: project: zero-to-prod sources: - repoURL: https://prometheus-community.github.io/helm-charts chart: kube-prometheus-stack targetRevision: 67.9.0 helm: releaseName: monitoring valueFiles: [$values/platform/monitoring/values.yaml] - repoURL: https://github.com/YOUR_GITHUB_USER/zero-to-prod-config targetRevision: main ref: values destination: {server: https://kubernetes.default.svc, namespace: monitoring} syncPolicy: automated: {prune: true, selfHeal: true} syncOptions: [CreateNamespace=true, ServerSideApply=true] # platform/monitoring/values.yaml grafana: adminPassword: change-me-in-a-secret # lab only; use a SecretRef or an external secrets operator for real ingress: enabled: true ingressClassName: nginx hosts: [grafana.localtest.me] prometheus: prometheusSpec: serviceMonitorSelectorNilUsesHelmValues: false # scrape ServiceMonitors from every namespace retention: 7d alertmanager: enabled: trueServerSideApply=trueavoids the annotation-size limit that the stack's large CRDs otherwise hit. Pin the chart version and bump it via pull requests like any dependency. - Commit, let the root app pick it up, and wait for the stack to become healthy. Then enable the ServiceMonitor in both overlays' values and confirm Prometheus scrapes the app.bash
git add . && git commit -m "feat: kube-prometheus-stack via Argo CD" && git push argocd app wait monitoring --health --timeout 600 # now set metrics.serviceMonitor: true in apps/orders-api/{dev,prod} values and push argocd app wait orders-api-dev --syncCheck: http://grafana.localtest.me logs in; Prometheus (port-forwardkubectl -n monitoring port-forward svc/monitoring-kube-prometheus-prometheus 9090) showsorders-dev/orders-apiandorders-prod/orders-apitargets as UP. - Add the SLO dashboard from the SRE project as a ConfigMap with the
grafana_dashboard: "1"label inplatform/monitoring/, and the burn-rate rules as aPrometheusRuleresource. Both are just files in git now; Argo CD applies them and the operator loads them.yaml# platform/monitoring/orders-slo-rules.yaml apiVersion: monitoring.coreos.com/v1 kind: PrometheusRule metadata: name: orders-api-slo namespace: monitoring labels: {release: monitoring} spec: groups: - name: orders_slo rules: - record: sli:availability_error_ratio:rate5m expr: | sum(rate(http_requests_total{namespace="orders-prod", status=~"5.."}[5m])) / sum(rate(http_requests_total{namespace="orders-prod"}[5m])) - alert: OrdersApiAvailabilityFastBurn expr: sli:availability_error_ratio:rate5m > 0.0144 for: 2m labels: {severity: page} annotations: {summary: "orders-api (prod) burning availability budget fast"}Reference the platform directory from an Application (platform-monitoring-extras, pathplatform/monitoring, excludingvalues.yaml) so these manifests sync. The full rule set from the SLO project drops in unchanged apart from the namespace selector.
The release flow, drift and rollback
CI publishes the chart and the image, then opens a pull request in the config repo that bumps the dev digest; a second pull request promotes to prod; and you see Argo CD revert drift and roll back a bad release.
- Protect the config repository's
mainbranch the same way the app repo is protected: pull requests only, and a required review for anything touchingapps/orders-api/prod/. A CODEOWNERS file makes the prod directory require a named reviewer.bashprintf 'apps/orders-api/prod/ @YOUR_GITHUB_USER\nargocd/ @YOUR_GITHUB_USER\n' > CODEOWNERS git add CODEOWNERS && git commit -m "chore: code owners for prod and argocd" && git push gh api -X PUT "repos/{owner}/zero-to-prod-config/branches/main/protection" --input - <<'JSON' {"required_status_checks": null, "enforce_admins": false, "required_pull_request_reviews": {"require_code_owner_reviews": true, "required_approving_review_count": 1}, "restrictions": null, "allow_force_pushes": false, "allow_deletions": false} JSONWorking alone, you can approve your own bot's dev bumps but GitHub will not let you approve your own prod pull requests; that is the point of the rule. For a solo lab, droprequired_approving_review_countto 0 for dev-only changes if it gets in the way. - In the app repository's
release.yml, after the image is pushed: package and push the chart to GHCR as an OCI artifact, then update the dev overlay's digest in the config repo through a pull request. A fine-grained token for the config repo lives in the app repo's secrets asCONFIG_REPO_TOKEN.yamlpromote-dev: needs: build runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Publish the chart as an OCI artifact run: | echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io -u "${{ github.actor }}" --password-stdin helm package chart --version "0.1.${{ github.run_number }}" --app-version "${{ needs.build.outputs.tag }}" helm push orders-api-0.1.${{ github.run_number }}.tgz oci://ghcr.io/${{ github.repository_owner }}/charts - name: Bump the dev digest in the config repo env: GH_TOKEN: ${{ secrets.CONFIG_REPO_TOKEN }} DIGEST: ${{ needs.build.outputs.digest }} run: | gh repo clone ${{ github.repository_owner }}/zero-to-prod-config cfg cd cfg && git switch -c "bump/dev-${DIGEST:7:12}" sed -i "s#digest: sha256:.*#digest: ${DIGEST}#" apps/orders-api/dev/kustomization.yaml sed -i "s#version: 0.1.*#version: 0.1.${{ github.run_number }}#" apps/orders-api/base/kustomization.yaml git -c user.name=release-bot -c user.email=bot@users.noreply.github.com commit -am "dev: ${{ github.sha }} (${DIGEST:7:12})" git push -u origin HEAD gh pr create --fill --body "Image ${DIGEST} from ${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}"docker/build-push-actionexposesoutputs.digest; add it to thebuildjob's outputs. The bot opens a pull request rather than pushing tomainso that the config repo's branch protection applies to machines too. For dev you may auto-merge it; for prod a human merges. - Make a small code change in the app, merge it, and follow the chain: the image is built, the chart is pushed, a bump pull request appears in the config repo. Merge it and watch Argo CD sync dev.bash
cd ../zero-to-prod && git switch -c feat/greeting-k8s sed -i.bak 's/hello from zero-to-prod/hello from gitops/' app/main.py && rm app/main.py.bak git commit -am "feat: gitops greeting" && git push -u origin HEAD && gh pr create --fill && gh pr merge --squash --delete-branch gh run watch cd ../zero-to-prod-config && gh pr list && gh pr merge --squash --delete-branch $(gh pr list --json number --jq '.[0].number') argocd app wait orders-api-dev --sync --health curl -s http://orders-dev.localtest.me/Check: The response changes in dev only. Prod still serves the old digest — the point of separate overlays. - Promote to prod as a git operation: copy the digest from dev to prod in a pull request that a human merges.bash
git switch -c promote/prod DIG=$(grep -o 'sha256:[0-9a-f]*' apps/orders-api/dev/kustomization.yaml) sed -i "s#digest: sha256:.*#digest: ${DIG}#" apps/orders-api/prod/kustomization.yaml git commit -am "prod: promote ${DIG:7:12}" && git push -u origin HEAD && gh pr create --fill # a reviewer merges; then: argocd app wait orders-api-prod --sync --health && curl -s http://orders.localtest.me/Check: Prod now serves the new greeting. The config repo's history is the deployment log: who promoted what, when, and from which app commit. - Drift: change the cluster by hand and watch Argo CD put it back.bash
kubectl -n orders-prod scale deploy/orders-api --replicas=5 sleep 20 && kubectl -n orders-prod get deploy orders-api argocd app history orders-api-prod | tail -3Check: Replicas return to the value in git within a few seconds (self-heal); the UI briefly showed OutOfSync. Manual changes to production are now impossible to keep, which is the feature. - Rollback: ship a bad release (make
/healthreturn 500 in a branch, merge, promote), watch prod's health degrade in Argo CD, then revert the promotion commit.bash# after promoting the bad digest: argocd app get orders-api-prod | grep -E 'Health|Sync' # Degraded: readiness fails, rollout stuck git revert --no-edit HEAD && git push argocd app wait orders-api-prod --sync --health curl -s http://orders.localtest.me/healthCheck: Health returns to Healthy and the previous digest is running.argocd app rollbackexists too, but the git revert is the GitOps answer because the repository stays the truth. - Write the README for the config repo: the promotion flow, who may merge prod, how to roll back, how to add a new app (a directory plus an Application file), and the one rule — nobody runs
kubectl applyagainst this cluster. Then delete the kind cluster when you are done.bashkind delete cluster --name gitopsRebuilding iskind create cluster, the ingress install, the Argo CD install, andkubectl apply -f argocd/app-of-apps.yaml: everything else comes back from git. That is the recoverability test.
Troubleshooting
- Argo CD shows
rpc error: ... helm chart inflationor ignores helmCharts - Kustomize's Helm support is off by default in Argo CD; patch
argocd-cmwithkustomize.buildOptions: --enable-helmand restartargocd-repo-server. - The kube-prometheus-stack Application fails with
metadata.annotations: Too long - Enable
ServerSideApply=truein the Application's sync options (the CRDs exceed the client-side apply annotation limit). - Prometheus does not scrape the app although the ServiceMonitor exists
- Set
serviceMonitorSelectorNilUsesHelmValues: falsein the stack's values, or label the ServiceMonitor withrelease: monitoring. Checkkubectl -n monitoring get servicemonitors -Aand the Prometheus UI's Service Discovery page. orders.localtest.medoes not resolve- Some corporate DNS setups block wildcard public DNS to localhost. Add
127.0.0.1 orders.localtest.me argocd.localtest.me grafana.localtest.meto/etc/hostsinstead. - The bump pull request is not created
- The
CONFIG_REPO_TOKENmust be a fine-grained token with Contents and Pull requests write on the config repo;ghalso needsGH_TOKENset in the step's env, as shown. - Argo CD reports OutOfSync forever on a resource it did not create
- Mutating webhooks and defaulted fields cause diffs; add an
ignoreDifferencesentry for the field, orRespectIgnoreDifferences=truein sync options. Check the diff in the UI to see what changes. - The prod Application pruned something it should not have
- Prod was configured with
prune: falsefor this reason; if you enabled it, a resource removed from git is deleted from the cluster. Restore the file in git; that is the recovery path.
Where to go from here
- Add an ApplicationSet that generates one Application per directory under
apps/, so a new service is only a directory. - Gate prod promotion on the SLO: the Progressive delivery project in the SRE track adds Argo Rollouts with canary analysis against the Prometheus metrics you already scrape.
- Replace the plain-text Grafana password with the External Secrets Operator reading from a secrets manager, and sign the chart and the config commits (the supply chain project).
- Point the same config repo at a real cluster (EKS from the Terraform an AWS environment project) by adding a second destination; the overlays already separate environments.
- Enable Argo CD notifications so a failed sync or a Degraded app posts to a channel with the diff.
Did a step fail or feel unclear? Tell me which one →