Skip to content

Step 6 of 7 in the DevOps journey · see the full journey

2025

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.
built with
  • 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.

14/14
89e1e1afix

Disable root selfHeal during rollback

  1. 58125a4Journal the Jenkins pipeline troubleshooting
  2. 236d474Declaratively define the Argo CD repo, added it to .gitignore
  3. bf09a6bAdd the Argo CD repository secret to the setup script
  4. c9adc9fArgo CD applications for kube-prometheus-stack, monitoring config and alerts
  5. 7139c36Embed Helm values directly into monitoring-application.yaml
  6. 914cc59Add sync waves to control monitoring deployment order
  7. 4ee8ecbRevert to the last almost-working commit
  8. af29eb0Try a fix, force sync
  9. 4cc7ab8Add ignoreDifferences for PersistentVolumeClaim to stop sync conflicts
  10. a015d15Revert the crash command to the original working version (rollback test successful)
  11. 3bd8109Test the RBAC strategy: sync should be denied
  12. 3b81ec2Correct the RBAC test annotation
  13. 7720022Change the Argo CD sync interval to 30s (default was 3min)
  14. 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

kubernetes-gitops-platform · selected git history

client:visible

Selected setup, sync and rollback commits from the project's development history. A reconstruction, not a live cluster.
Stack and source figures

how it fits together

reconciliation
  • Argo CD Reconciles the cluster from Git. Rollback experiments required coordinating root self-heal with image updates; the archived root manifest enables self-heal.
  • Argo CD Image Updater Watches 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 Operator Secrets sourced rather than committed.
repository layout
  • valueFiles scoping A relative path that climbs outside the repository root is rejected outright, so the values files live inside the root Argo CD reads.
packaging
  • Helm Charts and values package the application and supporting services. The monitoring Application uses ServerSideApply because the Prometheus CRDs exceeded the annotation limit.
stateful
  • PersistentVolumes + Claims Stateful 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.

  1. 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`.

  2. 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.

  3. 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.

  4. 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.

  5. 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`.

  6. 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.

  7. 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.

  8. 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.