
Introduction
In modern DevOps, Git isn’t just for version control—it’s the single source of truth for your entire infrastructure. GitOps takes this concept further by automating deployments through Git repositories. Instead of manually pushing updates with kubectl, your Kubernetes environment automatically syncs with the desired state defined in Git. Tools like Argo CD and Flux make this process seamless. In this comprehensive guide, you’ll learn how GitOps works, implement both tools step-by-step, and understand when to choose each approach.
What Is GitOps?
GitOps is a DevOps practice where Git acts as the central control plane for infrastructure and application deployment. The desired state of your system—configurations, manifests, and policies—lives in Git. Whenever someone pushes a change, a GitOps controller automatically reconciles that state to your cluster.
Core Principles
- Declarative – Everything described as code (YAML, HCL, etc.)
- Versioned and Immutable – Git history provides complete audit trail
- Pulled Automatically – Agents pull desired state from Git
- Continuously Reconciled – Drift is automatically corrected
Push vs Pull Deployment
Traditional CI/CD uses push deployments—the pipeline pushes changes to the cluster. GitOps uses pull deployments—an in-cluster agent pulls the desired state from Git. This is more secure because cluster credentials never leave the cluster.
Repository Structure
A well-organized GitOps repository is crucial for maintainability:
gitops-repo/
├── apps/
│ ├── base/
│ │ ├── deployment.yaml
│ │ ├── service.yaml
│ │ └── kustomization.yaml
│ ├── staging/
│ │ ├── kustomization.yaml
│ │ └── patches/
│ │ └── replicas.yaml
│ └── production/
│ ├── kustomization.yaml
│ └── patches/
│ ├── replicas.yaml
│ └── resources.yaml
├── infrastructure/
│ ├── controllers/
│ │ ├── cert-manager/
│ │ ├── ingress-nginx/
│ │ └── monitoring/
│ └── crds/
├── clusters/
│ ├── staging/
│ │ ├── apps.yaml
│ │ └── infrastructure.yaml
│ └── production/
│ ├── apps.yaml
│ └── infrastructure.yaml
└── README.md
Setting Up Argo CD
Argo CD is a Kubernetes-native GitOps tool with a powerful web UI. It continuously monitors Git repositories and syncs application manifests to clusters.
Install Argo CD
# Create namespace
kubectl create namespace argocd
# Install Argo CD
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods to be ready
kubectl wait --for=condition=available --timeout=300s deployment/argocd-server -n argocd
# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
# Access the UI (port-forward)
kubectl port-forward svc/argocd-server -n argocd 8080:443
Install Argo CD CLI
# macOS
brew install argocd
# Linux
curl -sSL -o argocd https://github.com/argoproj/argo-cd/releases/latest/download/argocd-linux-amd64
chmod +x argocd
sudo mv argocd /usr/local/bin/
# Login
argocd login localhost:8080 --username admin --password <password> --insecure
Create an Application
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
targetRevision: main
path: apps/production
destination:
server: https://kubernetes.default.svc
namespace: my-app
syncPolicy:
automated:
prune: true
selfHeal: true
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# Apply the application
kubectl apply -f application.yaml
# Or use CLI
argocd app create my-app \
--repo https://github.com/myorg/gitops-repo.git \
--path apps/production \
--dest-server https://kubernetes.default.svc \
--dest-namespace my-app \
--sync-policy automated \
--auto-prune \
--self-heal
App of Apps Pattern
Manage multiple applications with a single root application:
# apps/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: root-app
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
targetRevision: main
path: clusters/production
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
# clusters/production/apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: frontend
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
path: apps/frontend/production
destination:
server: https://kubernetes.default.svc
namespace: frontend
syncPolicy:
automated:
prune: true
selfHeal: true
---
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: backend
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/gitops-repo.git
path: apps/backend/production
destination:
server: https://kubernetes.default.svc
namespace: backend
syncPolicy:
automated:
prune: true
selfHeal: true
Projects for Multi-Tenancy
# project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: team-alpha
namespace: argocd
spec:
description: Project for Team Alpha
# Restrict source repos
sourceRepos:
- 'https://github.com/myorg/team-alpha-*'
# Restrict destination clusters/namespaces
destinations:
- namespace: 'alpha-*'
server: https://kubernetes.default.svc
# Allowed cluster resources
clusterResourceWhitelist:
- group: ''
kind: Namespace
# Namespace resources (all by default)
namespaceResourceWhitelist:
- group: '*'
kind: '*'
# Role-based access
roles:
- name: developer
description: Developer access
policies:
- p, proj:team-alpha:developer, applications, get, team-alpha/*, allow
- p, proj:team-alpha:developer, applications, sync, team-alpha/*, allow
groups:
- team-alpha-developers
Setting Up Flux
Flux by Weaveworks is a lightweight, Git-native GitOps tool. It works directly with your Git workflow without requiring a separate UI.
Install Flux CLI
# macOS
brew install fluxcd/tap/flux
# Linux
curl -s https://fluxcd.io/install.sh | sudo bash
# Verify installation
flux check --pre
Bootstrap Flux
# GitHub bootstrap
export GITHUB_TOKEN=<your-token>
flux bootstrap github \
--owner=myorg \
--repository=gitops-repo \
--branch=main \
--path=clusters/production \
--personal
# GitLab bootstrap
export GITLAB_TOKEN=<your-token>
flux bootstrap gitlab \
--owner=myorg \
--repository=gitops-repo \
--branch=main \
--path=clusters/production
Configure GitRepository Source
# flux-system/gitrepository.yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: app-source
namespace: flux-system
spec:
interval: 1m
url: https://github.com/myorg/app-manifests.git
ref:
branch: main
secretRef:
name: git-credentials # If private repo
Create Kustomization
# clusters/production/apps.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps
namespace: flux-system
spec:
interval: 10m
targetNamespace: default
sourceRef:
kind: GitRepository
name: flux-system
path: ./apps/production
prune: true
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: frontend
namespace: frontend
timeout: 2m
Helm Releases with Flux
# infrastructure/monitoring/prometheus.yaml
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: prometheus-community
namespace: flux-system
spec:
interval: 1h
url: https://prometheus-community.github.io/helm-charts
---
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: prometheus
namespace: monitoring
spec:
interval: 1h
chart:
spec:
chart: kube-prometheus-stack
version: "55.x"
sourceRef:
kind: HelmRepository
name: prometheus-community
namespace: flux-system
values:
prometheus:
prometheusSpec:
retention: 15d
resources:
requests:
memory: 1Gi
cpu: 500m
grafana:
enabled: true
adminPassword: ${GRAFANA_ADMIN_PASSWORD}
valuesFrom:
- kind: Secret
name: prometheus-values
valuesKey: values.yaml
Image Automation
Automatically update container images when new versions are pushed:
# Install image automation controllers
flux bootstrap ... --components-extra=image-reflector-controller,image-automation-controller
# image-repository.yaml
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImageRepository
metadata:
name: myapp
namespace: flux-system
spec:
image: ghcr.io/myorg/myapp
interval: 5m
---
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
name: myapp
namespace: flux-system
spec:
imageRepositoryRef:
name: myapp
policy:
semver:
range: 1.x.x
---
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: flux-system
git:
checkout:
ref:
branch: main
commit:
author:
email: flux@myorg.com
name: Flux
messageTemplate: 'chore: update {{.AutomationObject.Name}} to {{.NewTag}}'
push:
branch: main
update:
path: ./apps
strategy: Setters
# deployment.yaml - Add image policy marker
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
spec:
containers:
- name: myapp
image: ghcr.io/myorg/myapp:1.0.0 # {"$imagepolicy": "flux-system:myapp"}
Argo CD vs Flux: Comparison
| Feature | Argo CD | Flux |
|---|---|---|
| UI | Rich web UI | CLI only (use Weave GitOps UI) |
| Architecture | Single component | Modular controllers |
| Multi-tenancy | AppProjects | Kustomization namespacing |
| Helm Support | Native | HelmRelease CRD |
| Image Automation | Argo CD Image Updater | Built-in controllers |
| Learning Curve | Lower (UI helps) | Steeper (Git-native) |
| Best For | Teams wanting visibility | Teams preferring Git workflows |
Common Mistakes to Avoid
1. Not Using Separate Repos for Config
# Wrong - app code and config in same repo
myapp/
├── src/
├── k8s/
└── Dockerfile
# Correct - separate config repo
myapp/ # Source code
gitops-config/ # K8s manifests
2. Storing Secrets in Git
# Wrong - plain secrets in Git
apiVersion: v1
kind: Secret
data:
password: cGFzc3dvcmQxMjM= # Never do this!
# Correct - use Sealed Secrets or External Secrets
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
spec:
encryptedData:
password: AgBy3i4OJSWK+...
3. No Health Checks
# Flux Kustomization with health checks
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
spec:
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: frontend
- apiVersion: apps/v1
kind: Deployment
name: backend
timeout: 5m
4. Missing Notifications
# Flux notification provider
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Provider
metadata:
name: slack
namespace: flux-system
spec:
type: slack
channel: deployments
secretRef:
name: slack-webhook
---
apiVersion: notification.toolkit.fluxcd.io/v1beta2
kind: Alert
metadata:
name: on-call
namespace: flux-system
spec:
providerRef:
name: slack
eventSeverity: error
eventSources:
- kind: Kustomization
name: '*'
- kind: HelmRelease
name: '*'
Final Thoughts
GitOps brings reliability, auditability, and automation to Kubernetes deployments. Argo CD excels when you need a rich UI, RBAC, and enterprise features. Flux shines when you prefer a lightweight, Git-native approach with powerful image automation. Both tools follow the same core principles—choose based on your team’s workflow preferences. Start with a single application, implement proper secrets management, and gradually expand as your GitOps practice matures.
To extend your automation, read Infrastructure as Code with Terraform and Blue/Green vs Canary Deployments. For official documentation, visit the Argo CD Documentation and the Flux Documentation.