Step 6 of 7 in the DevOps journey · see the full journey
GitOps Galaxy
A Kubernetes cluster reconciled from Git by ArgoCD rather than changed by hand.
Overview
I extended my Kubernetes lab with Git-backed deployments, Helm packaging and Vault-based database secrets. The goal was to make deployment changes traceable and exercise drift correction, rollback and resource recovery.
- context
- Coursework
- status
- Archived learning project
- role
- Solo
- started
- Aug 2025
- setting
- Local Kubernetes lab for GitOps reconciliation, secrets delivery and deliberately triggered recovery exercises.
- ArgoCD
- ArgoCD Image Updater
- HashiCorp Vault
- Vault Secrets Operator
- Helm
- Jenkins
- +2
Key decisions
- Keep deployment updates in Git
- Jenkins and Image Updater both write image-tag changes to Git. ArgoCD then reconciles the declared application configuration.
- Deliver database credentials through Vault
- Kubernetes authentication and Vault Secrets Operator deliver the database password as a Kubernetes Secret, keeping it out of versioned manifests.
- Install dependencies before their configuration
- I separated the monitoring stack and its custom resources with sync waves, so Prometheus CRDs exist before configuration that depends on them.
Demonstrated results
- Manual replica drift was corrected automatically
- After I scaled the frontend from two replicas to three, ArgoCD detected the difference and restored the declared count of two.
- A broken deployment triggered rollback
- An injected startup failure produced CrashLoopBackOff. Jenkins restored the previous image tag in Git, and the application returned to Healthy and Synced.
- Read access allowed, sync denied
- A permission test using the image-updater user's ArgoCD token confirmed that reading the application succeeded while a sync request was refused.
Known limitations
- Rollback required coordination between controllers
- The experiments needed Image Updater to be paused and root self-heal coordinated so automation would not undo the recovery action.
- TLS recovery remained a restart workaround
- Missing or corrupted TLS material after a Minikube restart was regenerated through Helm. The archived helper restores the lab, rather than eliminating the restart failure.
- Vault used developer-mode storage
- The Vault manifest enables in-memory developer mode and disables high availability. It demonstrates secrets delivery, not durable Vault storage or failover.
Git-backed cluster configuration
The repository declares twelve Application manifests, including the root, with destinations across seven namespaces:
argocd database monitoring jenkins dev vault vault-operator
I used an App-of-Apps to manage the child Applications, with automated sync, pruning and self-heal. The root manages Application manifests; the children manage their respective workloads. Normal deployment changes go through Git.
Self-heal, demonstrated
Git says two frontend replicas:
kubectl scale deployment infrastructure-insight-dev-frontend -n dev --replicas=3
Within seconds the ArgoCD application goes OutOfSync, and the UI shows a diff
between live replicas: 3 and desired replicas: 2. Doing nothing:
kubectl get deployment infrastructure-insight-dev-frontend -n dev -w
The count returns to 2, one pod enters Terminating, and the application goes
back to Healthy and Synced. This checks the controller's response to a deliberate
manual change in the lab.
Two triggers, both ending in Git
Code-triggered. Jenkins polls the source repository, builds and pushes images
with a 1.0.x tag, then rewrites the chart values and commits the change back:
yq -i '.backend.image.tag = "1.0.4"' infrastructure/app-chart/values.yaml
git commit -m "ci: Update image tags to 1.0.4"
git push origin main
ArgoCD sees the commit and syncs.
Image-triggered. ArgoCD Image Updater watches the same chart for new tags
matching regexp:^1\.0\.[0-9]+$ and writes the change to Git itself. The filter
was tightened once, deliberately, so that 1.1.0 and 2.0.0 could not be
picked up. The journal shows the debug output confirming one surviving tag.
Deployment ordering and stateful resources
The monitoring configuration initially tried to create ServiceMonitors before the
Prometheus Operator had installed their CRDs. I assigned sync wave 0 to
monitoring-stack and 1 to monitoring-custom-config so the dependencies were
installed first.
Large Prometheus CRDs also exceeded the client-side apply annotation limit. The
monitoring Application uses ServerSideApply=true to avoid that annotation and
excludes the PVC field /spec/volumeName from drift comparison. These are separate
adjustments for apply size and stateful-resource differences.
Automated rollback in the lab
A breaking change was committed to trigger a build, which produced a
CrashLoopBackOff. The pipeline's failure detector, three consecutive checks of
restartCount > 2 with a 30-second grace, moved the build to its catch block:
yq -i '.backend.image.tag = "1.0.3"' infrastructure/app-chart/values.yaml
git commit -m "ci: ROLLBACK to 1.0.3 [skip ci]"
sh "argocd app sync ${appName} --force --prune --timeout 180"
It also pauses the image updater, because a rollback that immediately re-updates is not a rollback.
The failure was injected deliberately, and the pipeline executed the rollback.
The development record reports the application returning to Healthy and Synced
on the previous image. Repeated rollback commits record the iterations needed to
get that workflow working.
Rollback coordination
Image Updater could otherwise select the failed image again after rollback. The history records attempts to pause or disable it, along with a temporary change to root self-heal while recovery was being tested.
The final manifests enable root self-heal and exclude the Image Updater deployment's
/spec/replicas field from drift comparison. That lets the pipeline pause the updater
without ArgoCD immediately restoring its replica count. The constraint is coordinating
these control loops; reverting an image tag in Git is compatible with reconciliation.
Recovering a deleted Application
kubectl delete application.argoproj.io infrastructure-insight-dev -n argocd
With the root app's self-heal temporarily disabled so the test could observe the window. The next pipeline run produces:
application.argoproj.io/infrastructure-insight-dev created
The created output verifies that the pipeline recreated the missing Application
resource. The root app's auto-sync was then re-enabled. This exercise checks resource
recreation, not restoration of database contents or recovery of an entire lost cluster.
Database secrets from Vault
The Banzai Cloud Vault Operator was the first choice. It was dropped after research found the Helm repository deprecated following a Cisco acquisition, and HashiCorp's own Vault Secrets Operator was used instead.
I supplied the database password from a git-ignored bootstrap script, wrote it to
Vault via vault kv put, and synced it to a Kubernetes Secret. The
policy restricts read access to secret/data/database/* for one service account
in one namespace.
Vault developer mode
The archived argocd/app-vault.yaml explicitly sets server.dev.enabled: true
and server.ha.enabled: false. This is a single-pod, in-memory Vault setup.
It makes the Kubernetes authentication and secret-delivery path convenient to
exercise, but does not establish durable secret storage or high availability.
Scoped access, tested
RBAC was tightened from cluster-admin to four Roles across three namespaces, and
one rule is restricted by name:
- apiGroups: [""]
resourceNames: ["argocd-initial-admin-secret"]
resources: ["secrets"]
verbs: ["get"]
Separately, scripts/rbac-permission-test.sh uses the image-updater-user ArgoCD
token to check both sides of its policy. The recorded output shows argocd app get
succeeding and argocd app sync being denied. This verifies those actions for that
identity, rather than asserting that every permission in the cluster was exhaustively tested.
TLS recovery after a restart
The restart investigation recorded missing or corrupted TLS material in
argocd-secret. The server started with tls: false, so clients expecting HTTPS
could not complete their handshakes.
The archived scripts/fix-argocd.sh scales down the ArgoCD server, deletes the
problem secret and runs a Helm upgrade with --reuse-values to regenerate it.
It then scales the server back up and waits for readiness. That restores this lab
after the failure; the record does not establish a durable prevention of the
restart problem.
Interactive models
Explore the model below to inspect the implementation. This is a reconstruction from the project, not a live service.
Deployment history and rollback experiments
GitOpsReplay
Selected setup, sync and rollback commits, ordered by author date.
89e1e1afixDisable root selfHeal during rollback
58125a4Journal the Jenkins pipeline troubleshooting236d474Declaratively define the Argo CD repo, added it to .gitignorebf09a6bAdd the Argo CD repository secret to the setup scriptc9adc9fArgo CD applications for kube-prometheus-stack, monitoring config and alerts7139c36Embed Helm values directly into monitoring-application.yaml914cc59Add sync waves to control monitoring deployment order4ee8ecbRevert to the last almost-working commitaf29eb0Try a fix, force sync4cc7ab8Add ignoreDifferences for PersistentVolumeClaim to stop sync conflictsa015d15Revert the crash command to the original working version (rollback test successful)3bd8109Test the RBAC strategy: sync should be denied3b81ec2Correct the RBAC test annotation7720022Change the Argo CD sync interval to 30s (default was 3min)89e1e1aDisable root selfHeal during rollback
2 reverts · 6 fixes · 3 probes
This excerpt follows setup, deployment ordering and rollback experiments. A PersistentVolumeClaim that differs from Git on every sync needed ignoreDifferences; custom monitoring resources needed the Prometheus CRDs installed first, using sync-wave ordering.
monitoring sync waves: stack 0, custom configuration 1
client:visible
Stack and source figures
how it fits together
- reconciliation
-
-
Argo CDReconciles the cluster from Git. Rollback experiments required coordinating root self-heal with image updates; the archived root manifest enables self-heal. -
Argo CD Image UpdaterWatches accepted image tags and writes changes to Git. Its deployment's replica count is excluded from drift comparison so the updater can be paused during rollback.
-
- secrets
-
-
HashiCorp Vault + Vault Secrets OperatorSecrets sourced rather than committed.
-
- repository layout
-
-
valueFiles scopingA relative path that climbs outside the repository root is rejected outright, so the values files live inside the root Argo CD reads.
-
- packaging
-
-
HelmCharts and values package the application and supporting services. The monitoring Application uses ServerSideApply because the Prometheus CRDs exceeded the annotation limit.
-
- stateful
-
-
PersistentVolumes + ClaimsStateful storage that Argo CD would otherwise fight, hence the ignoreDifferences rule.
-
- commits
- 693 The largest repository in the collection by a wide margin.
- Application manifests
- 12 Eleven child Application manifests plus the root, declaring destinations across seven namespaces. Counted from source, not a current live-cluster inventory.
- chart templates
- 7
- automated rollbacks
- 9+ The CI rollback path fired repeatedly in the commit history.
Full issue record · 8 entries
what broke, and what happened
- 5 fixed / 8
- 2 workaround / 8
- 1 unresolved / 8
Every entry below says what broke and what became of it. An unresolved one is listed as unresolved rather than dropped.
- fixed
Application resources raced each other for CRDs that did not exist yet
`monitoring-custom-config` tried to create ServiceMonitors before `monitoring-stack` had created the CRDs. Fixed with sync waves: 0 for the stack, 1 for the config.
then: Sync waves: `0` for the monitoring stack and `1` for its configuration, declared in `argocd/app-monitoring-custom.yaml`.
- fixed
The Prometheus CRDs exceeded the annotation size limit
The `last-applied-configuration` annotation was too large. `ServerSideApply=true` fixed it; `Replace=true` then proved too aggressive, so the final answer was a precise `ignoreDifferences` rule for PVC volumeName.
then: Server-side apply avoids the annotation ceiling. The monitoring Application in `argocd/app-monitoring-stack.yaml` excludes `/spec/volumeName` on PVCs from drift comparison.
- fixed
valueFiles cannot reference a path outside the repository root
`ComparisonError: file '../infrastructure/database/postgres-values.yaml' resolved to outside repository root`
then: The values files moved inside the repository root Argo CD reads, since a relative path that climbs out of it is rejected outright rather than resolved.
- fixed
Helm whitespace control merged a comment into the apiVersion line
`{{- if }}` after a comment consumed the trailing newline, producing `# commentapiVersion: ...` and `groupVersion shouldn't be empty`.
then: The template no longer places a chomping `{{-` immediately after a comment. That is what ate the newline.
- fixed
An Ingress hardcoded a service name that did not exist
`kubectl describe ingress` reported `services "frontend-svc" not found`.
then: The Ingress references the service the chart actually renders in `infrastructure/app-chart/templates/frontend-service.yaml`.
- workaround
ArgoCD TLS material was missing or corrupted after a Minikube restart
The restart investigation recorded missing or corrupted `tls.crt` and `tls.key` data in `argocd-secret`, with the server starting in `tls: false` mode.
then: The helper regenerates TLS material through a Helm upgrade and restarts the server. This remains a lab recovery workaround rather than a demonstrated prevention of the restart failure.
- workaround
Rollback experiments conflicted with automatic reconciliation
The history contains `ci: Disable root selfHeal during rollback` and repeated ROLLBACK commits that pause or disable Image Updater.
then: The experiments coordinated root self-heal with pausing Image Updater to prevent recovery actions from being undone. The archived root manifest enables self-heal; this is a workflow constraint in this setup, not an inherent incompatibility between GitOps and rollback.
- unresolved
Vault ran in developer mode with in-memory storage
`argocd/app-vault.yaml` sets `server.dev.enabled: true` and `server.ha.enabled: false`.
then: Retained as a lab configuration in the archived source. The delivery path was exercised, but persistent Vault storage and high availability were not demonstrated.