GitOps works best when repositories, environments, and promotion paths are intentionally structured. The core idea is simple: Git is the single source of truth for declarative infrastructure and application configuration. Every change goes through a pull request, gets reviewed, and is applied by an automated reconciliation controller. According to the CNCF 2025 survey, teams practicing GitOps report 45% fewer deployment failures and 60% faster mean time to recovery compared to imperative deployment workflows.
---
Core GitOps Principles
Before optimizing a GitOps workflow, align on what GitOps actually requires:
Repository Model
Choose your repository structure based on team boundaries, not trends.
Mono-Repo vs Multi-Repo
| Approach | Pros | Cons | Best For |
| Mono-repo | Single PR for cross-cutting changes, simpler CI | Permissions harder, CI complexity grows | < 5 services, single team |
| App repo + config repo | Separation of concerns, clear access control | Two PRs for some changes | 5-20 services, 2-5 teams |
| Repo per service + shared config | Maximum isolation, independent release cycles | Coordination overhead | > 20 services, many teams |
Recommended Structure: App Repo + Config Repo
app-repo/
src/
Dockerfile
.github/workflows/build.yaml
gitops-config/
base/
deployment.yaml
service.yaml
configmap.yaml
overlays/
dev/
kustomization.yaml
patches/
staging/
kustomization.yaml
patches/
production/
kustomization.yaml
patches/
Environment Strategy
Use a clear promotion flow with auditable change history at every stage.
Promotion Pipeline
dev (auto-deploy on PR merge)
|
v
staging (auto-deploy, smoke tests gate promotion)
|
v
production (manual approval required)
Kustomize Overlay for Environment Promotion
<h1>gitops-config/overlays/staging/kustomization.yaml</h1>
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: staging
resources:
- ../../base
patches:
- target:
kind: Deployment
name: api-server
patch: |
- op: replace
path: /spec/replicas
value: 2
images:
- name: api-server
newTag: abc1234f # Updated by CI pipeline
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=debug
- ENVIRONMENT=staging
<h1>gitops-config/overlays/production/kustomization.yaml</h1>
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
resources:
- ../../base
patches:
- target:
kind: Deployment
name: api-server
patch: |
- op: replace
path: /spec/replicas
value: 5
images:
- name: api-server
newTag: abc1234f # Same tag as staging after validation
configMapGenerator:
- name: app-config
behavior: merge
literals:
- LOG_LEVEL=info
- ENVIRONMENT=production
Automated Image Tag Updates
<h1>.github/workflows/promote.yaml</h1>
name: Promote to Environment
on:
workflow_dispatch:
inputs:
environment:
description: Target environment
required: true
type: choice
options: [staging, production]
image_tag:
description: Image tag to promote
required: true
jobs:
promote:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
repository: org/gitops-config
token: ${{ secrets.GITOPS_TOKEN }}
- name: Update image tag
run: |
cd overlays/${{ inputs.environment }}
kustomize edit set image \
api-server=ghcr.io/org/api-server:${{ inputs.image_tag }}
- name: Commit and push
run: |
git config user.name "CI Bot"
git config user.email "[email protected]"
git add .
git commit -m "Promote api-server:${{ inputs.image_tag }} to ${{ inputs.environment }}"
git push
Policy and Review
Automate policy checks and keep human review focused on business-impacting changes.
Automated Policy Checks
<h1>OPA/Gatekeeper constraint: require resource limits</h1>
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: require-resource-limits
spec:
match:
kinds:
- apiGroups: ["apps"]
kinds: ["Deployment"]
parameters:
requiredResources:
- limits
- requests
PR Review Checklist
<h2>GitOps PR Review Checklist</h2>
<ul><li>[ ] Image tag is a specific SHA, not "latest" or "main"</li>
<li>[ ] Resource requests and limits are set</li>
<li>[ ] Liveness and readiness probes are defined</li>
<li>[ ] Secrets reference external manager, not inline values</li>
<li>[ ] Change was tested in staging before production PR</li>
<li>[ ] Rollback plan documented in PR description</li>
</ul>
Rollout and Rollback Discipline
Every rollout needs a known rollback path and verification checklist.
Rollback via Git Revert
<h1>GitOps rollback is a git revert</h1>
git revert HEAD
git push
<h1>The reconciliation controller (ArgoCD/Flux) automatically</h1>
<h1>applies the reverted state to the cluster</h1>
<h1>Verify rollback</h1>
kubectl get deployment api-server -n production -o jsonpath='{.spec.template.spec.containers[0].image}'
Drift Detection
<h1>ArgoCD: check sync status</h1>
argocd app get api-server --output json | jq '.status.sync.status'
<h1>Expected: "Synced"</h1>
<h1>If "OutOfSync": someone changed cluster state outside GitOps</h1>
Review deployment tooling on the DevOps Tools page and infrastructure context on Cloud Computing. For architecture guidance, visit Contact.
Frequently Asked Questions
Should I use ArgoCD or Flux?
Both are mature CNCF projects. ArgoCD provides a web UI, application-centric model, and SSO integration out of the box. Flux is more lightweight, CLI-driven, and integrates tightly with Kustomize and Helm. For teams that want a visual dashboard, choose ArgoCD. For teams that prefer pure Git workflows, choose Flux. Both handle the core reconciliation loop equally well.
How do I handle secrets in GitOps?
Never store plaintext secrets in Git. Use Sealed Secrets (encrypts secrets that only the cluster can decrypt), External Secrets Operator (syncs secrets from external managers like Vault or AWS Secrets Manager), or SOPS (encrypts values in-place with age or PGP keys). External Secrets Operator is the most flexible for production use.
What if someone makes a manual change to the cluster?
The reconciliation controller detects drift and can either alert or auto-remediate. Configure auto-remediation for production namespaces so manual changes are automatically reverted to the Git-declared state. This enforces the "Git is the source of truth" principle and prevents configuration drift.
How do I handle database migrations in GitOps?
Database migrations are imperative operations that do not fit the declarative GitOps model cleanly. Run migrations as Kubernetes Jobs triggered by CI before the application deployment is updated. Ensure migrations are backward-compatible so both old and new application versions can run during the rollout window.
How often should I sync?
Configure the reconciliation controller to poll every 1-3 minutes or use webhooks for immediate sync on Git push. Polling provides a safety net if webhooks fail. The sync interval is a tradeoff between deployment speed and cluster API load. For most teams, 1-minute polling with webhook acceleration is the right balance.
Final Takeaway
GitOps succeeds when process simplicity and operational safety are balanced. Start with a clear repository structure, define promotion paths between environments, automate policy enforcement, and ensure every change is reversible with a git revert. The goal is predictable, auditable, and recoverable deployments.