Files
atelier/examples/good/gitops-pr.md
T
Jon Chery 9ebc9c8868 docs(milestone): complete v0.3 — GitOps+Operators/AI-ML/i18n/Compliance
---ci---
project: atelier
phase: 6
milestone: v0.3
status: complete
requirements:
  covered: [ATELIER-60..91]
  partial: []
---/ci---
2026-08-05 03:45:38 +00:00

197 lines
7.4 KiB
Markdown

# Good Example: GitOps Pull Request
> A pull request that changes ArgoCD Application manifests following
> Atelier's GitOps + Operators principles. Each aspect cites the
> principle it satisfies.
## The PR
A PR titled `promote payments-api 1.2.3 to prod` opened against the
GitOps repo `platform/gitops`. It changes the `targetRevision` of the
payments-api Application from `1.2.2` to `1.2.3`, adds a sync-wave
annotation to a new migration Job, and tightens the AppProject's
destination allow-list. CI runs plan/diff; nothing pushes to the
cluster.
### The Commit
```yaml
# manifests/prod/payments-api.yaml — the only file changed
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payments-api
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
source:
repoURL: https://git.example.com/platform/payments
targetRevision: 1.2.3 # was 1.2.2 — pinned, not latest
path: manifests/prod
destination:
server: https://kubernetes.default.svc
namespace: payments
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=false
- PrunePropagationPolicy=foreground
```
```yaml
# manifests/prod/payments-db-migration.yaml — new file, wave-ordered
apiVersion: batch/v1
kind: Job
metadata:
name: payments-db-migrate-1.2.3
namespace: payments
annotations:
argocd.argoproj.io/sync-wave: "-1" # PreSync: run before the app
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: OnFailure
serviceAccountName: payments-migrator
containers:
- name: migrate
image: registry.example.com/payments/migrate:1.2.3
args: ["up", "--to", "1.2.3"]
```
```yaml
# projects/payments.yaml — AppProject tightened in the same PR
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: payments
namespace: argocd
spec:
sourceRepos:
- https://git.example.com/platform/payments
destinations:
- server: https://kubernetes.default.svc
namespace: payments # prod-app can only deploy to payments ns
clusterResourceWhitelist:
- group: ""
kind: Namespace # allowed to create its own namespace
roles:
- name: payments-team
policies:
- p, proj:payments:payments-team, applications, sync, payments/*, allow
```
### The CI Pipeline (runs on the PR, before merge)
```text
# .github/workflows/gitops-plan.yml (illustrative steps)
- name: validate manifests
run: argocd app manifests manifests/prod/ | kubeconform -strict
- name: diff against live cluster (read-only, no apply)
run: argocd app diff payments-api --server $ARGOCD_SERVER --auth-token $READ_ONLY_TOKEN
# CI holds a READ-ONLY ArgoCD token. It never holds kubectl rights.
# A non-empty diff is the PR's proposed change, rendered for review.
- name: opa gate (admission policy pre-check)
run: opa eval -i manifests/prod/ -d policies/ "data.k8s.admission.deny"
# Policy violations fail the PR before merge, not after deploy.
```
## What Makes It Good
### Git is the Source of Truth (GitOps P1, C1 Correctness)
- The promotion is a commit. The cluster's desired state is a
derivative of this repo; the repo is the authority. If the change is
wrong, `git revert` is the rollback — the recovery path is the
history.
- See `domains/gitops-operators/first-principles.md` P1 and
`domains/gitops-operators/argocd.md` (Application CRD).
### Pull, Don't Push (GitOps P3, C4 Locality)
- CI holds a **read-only** ArgoCD token for `app diff`. It holds no
`kubectl` rights against the production cluster. The cluster's
ArgoCD controller pulls the merged commit; nothing pushes to the
cluster. A compromised CI token can read, not deploy.
- See `domains/gitops-operators/argocd.md` (RBAC and SSO) and
`domains/gitops-operators/flux.md` for the same pull boundary from
the Flux side.
### State is Immutable and Versioned (GitOps P5, C5 Reversibility)
- `targetRevision: 1.2.3` — the Application pins a specific chart
revision, not `latest`. The commit that changed it is a permanent
record; `git revert` restores `1.2.2` and ArgoCD's `selfHeal`
converges the cluster back. No force-push; history is the audit
trail.
- See `domains/gitops-operators/first-principles.md` P5 and
`domains/infrastructure-as-code/state.md` (State is Truth).
### Sync Waves Order Correctness (GitOps P4, C1)
- The migration Job carries `argocd.argoproj.io/sync-wave: "-1"` so
it runs in `PreSync` before the payments-api Deployment that
depends on the new schema. Wave ordering is a correctness
mechanism, not performance — the app starting before its migration
is a correctness bug.
- See `domains/gitops-operators/argocd.md` (Sync Waves and Hooks).
### Reconcile, Don't Mutate by Hand (GitOps P8)
- `selfHeal: true` + `prune: true` means a hand-edited drift on a
managed resource is overwritten on the next loop. The fix for drift
is a new commit, not `kubectl edit`. The PR author does not SSH into
the cluster to "fix" anything.
- See `domains/gitops-operators/argocd.md` (Diff and Drift) and
`domains/gitops-operators/first-principles.md` P8.
### Least Privilege Reconciliation (GitOps P10, C8 Economy)
- The AppProject `payments` restricts the Application to the
`payments` namespace and the `payments` repo. The controller's
ServiceAccount (not shown) is bound to a namespace-scoped Role, not
`cluster-admin`. The PR *tightens* the allow-list — least privilege
is a direction, not a one-time setting.
- See `domains/gitops-operators/argocd.md` (RBAC and SSO) and
`domains/kubernetes/rbac.md`.
### Policy is a Gate (Compliance P5, cross-link)
- The `opa eval` step runs the admission policy against the proposed
manifests before merge. A violation fails the PR; the non-compliant
state is never realized. Detection is not enforcement; this is
enforcement.
- See `domains/compliance/policy-as-code.md` and
`domains/devops/ci-cd.md`.
### Failure is Observable (GitOps P9)
- A sync failure or health degradation on `payments-api` emits
ArgoCD status (`Degraded` / `OutOfSync`) and a notification. Silent
drift is the bug; this PR does not disable notifications.
- See `domains/gitops-operators/argocd.md` (Health and Status) and
`domains/observability/metrics.md`.
## What This PR Does NOT Do (And Why That's Good)
- Does **not** run `kubectl apply` from CI — that is the push pattern,
a P3 violation (see `examples/bad/` for the anti-pattern).
- Does **not** use `argocd app set` as the steady state — the change
is in git, not in an imperative command's history.
- Does **not** store raw Secrets in the GitOps repo — secrets arrive
via Sealed Secrets / SOPS / External Secrets, encrypted in git.
- Does **not** float `targetRevision: latest` — the Application pins
a version; "latest" is an unknown model of the system.
## Cross-Domain Links
- `domains/gitops-operators/argocd.md` — the Application CRD, sync
waves, RBAC/AppProjects, and the pull model.
- `domains/gitops-operators/flux.md` — the same PR pattern from the
Flux side (Kustomization CRD, per-cluster autonomy).
- `domains/kubernetes/workloads.md` — the Deployment/Job the
Application reconciles.
- `domains/kubernetes/rbac.md` — the ServiceAccount + Role the
controller and the migration Job run as.
- `domains/compliance/policy-as-code.md` — the OPA gate is a
compliance-as-a-gate enforcement point.
- `domains/devops/P4 Rollback First``git revert` is the rollback;
`selfHeal` is the convergence.