GitOps β Flux & ArgoCD
GitOps uses Git repositories as the single source of truth for declarative infrastructure and applications, enabling automated synchronization and drift detection. This guide covers both ArgoCD and Flux with production patterns from large-scale Kubernetes deployments.
GitOps Principles
GitOps, coined by Weaveworks in 2017, applies Git workflows to operations. The core principles:
- Declarative Configuration: All infrastructure and application state is described declaratively (Kubernetes manifests, Helm charts, Kustomize overlays) and stored in Git.
- Git as Single Source of Truth: The Git repository contains the desired state. The running system continuously converges to match Git.
- Automated Synchronization: An agent (ArgoCD or Flux) watches Git and automatically applies changes to the cluster.
- Drift Detection and Reconciliation: Any manual changes to the cluster are detected and reverted to match Git. The desired state always wins.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β GitOps Flow β
β β
β ββββββββββββ PR/Merge ββββββββββββ Sync β
β β DeveloperβββββββββββββββββΆβ Git ββββββββββββ β
β β (kubectl)β β (Source β β β
β ββββββββββββ β of Truthβ βΌ β
β ββββββββββββ ββββββββββββ β
β β ArgoCD / β β
β β Flux β β
β β (Agent) β β
β ββββββ¬ββββββ β
β β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββ β
β βΌ β
β ββββββββββββββββ Drift Detection ββββββββββββ β
β β Kubernetes ββββββββββββββββββββββββ Reconcileβ β
β β Cluster β (auto-heal) β Loop β β
β ββββββββββββββββ ββββββββββββ β
β β
β Rule: NEVER use kubectl apply manually in production. β
β All changes go through Git β PR β Merge β Automated Sync β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Flux vs. ArgoCD Comparison
| Capability | Flux | ArgoCD |
|---|---|---|
| GitOps Engine | CNCF Graduated (v2) | CNCF Incubating |
| Architecture | Native Kubernetes operators (in-cluster) | Application controller + API server + UI |
| Web UI | Weave GitOps (separate) or CLI only | Built-in, excellent UI |
| Multi-cluster | Cluster API + Bootstrap | Native multi-cluster management |
| Multi-source | OCI + Git + S3 + Helm repos | Git + Helm + S3 + GCS + OCI |
| Helm support | Native Helm Controller | Native Helm support |
| Image automation | ImageUpdateAutomation + ImagePolicy | ArgoCD Image Updater |
| Progressive delivery | Flagger (separate) | Argo Rollouts (separate) |
| Secrets management | SOPS + External Secrets | Sealed Secrets + External Secrets + SOPS |
| Notification | Notification Controller | Notifications + Webhooks |
| App of Apps pattern | Supports via Kustomize | Native (Applications in Application) |
| RBAC | Kubernetes RBAC | Built-in RBAC + SSO integration |
| Drift detection | Continuous reconciliation | Auto-sync + Visual diff in UI |
| Resource health assessment | Health checks in events | Visual health indicators + custom health checks |
ArgoCD Installation and Setup
Install ArgoCD
# Create namespace and install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Or install with Helm
helm repo add argo https://argoproj.github.io/argo-helm
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--set server.service.type=LoadBalancer \
--set server.ingress.enabled=true \
--set server.ingress.hostname=argocd.example.com
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=120s
# Get initial admin password
kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d
# Port-forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Access: https://localhost:8080 (login: admin / password from above)
# CLI login
argocd login localhost:8080 --username admin --password $(kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath="{.data.password}" | base64 -d) --insecure
Configure ArgoCD
# ββ Configure SSO (GitHub OAuth) βββββββββββββββββββββββββ
# Edit argocd-cm ConfigMap
kubectl patch configmap argocd-cm -n argocd --type merge -p '{
"data": {
"url": "https://argocd.example.com",
"dex.config": "connectors:\n- type: github\n id: github\n name: GitHub\n config:\n clientID: $GITHUB_CLIENT_ID\n clientSecret: $GITHUB_CLIENT_SECRET\n organizations:\n - my-org"
}
}'
# ββ Configure RBAC βββββββββββββββββββββββββββββββββββββββ
kubectl patch configmap argocd-rbac-cm -n argocd --type merge -p '{
"data": {
"policy.default": "role:readonly",
"policy.csv": "p, role:admin, applications, *, *, allow\np, role:admin, clusters, *, *, allow\np, role:admin, repositories, *, *, allow\ng, my-org:platform-team, role:admin\ng, my-org:developers, role:readonly"
}
}'
# ββ Add a cluster ββββββββββββββββββββββββββββββββββββββββ
# ArgoCD auto-detects the local cluster
# Add external EKS cluster:
argocd cluster add arn:aws:eks:us-east-1:123456789012:cluster/production-cluster \
--name production \
--yes
Complete Application Manifest Example
# argocd/application.yaml β ArgoCD Application manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io # Cascade delete on app removal
labels:
app.kubernetes.io/name: payment-service
environment: production
team: payments
spec:
project: production
# ββ Source βββββββββββββββββββββββββββββββββββββββββββββ
source:
repoURL: https://github.com/my-org/gitops-apps.git
targetRevision: main
path: apps/payment-service/overlays/production
# Helm-specific configuration
helm:
valueFiles:
- values-production.yaml
parameters:
- name: replicaCount
value: "5"
- name: autoscaling.maxReplicas
value: "20"
# Or Kustomize
# kustomize:
# namePrefix: prod-
# commonLabels:
# environment: production
# ββ Destination ββββββββββββββββββββββββββββββββββββββββ
destination:
server: https://kubernetes.default.svc # In-cluster
namespace: production
# ββ Sync Policy ββββββββββββββββββββββββββββββββββββββββ
syncPolicy:
automated:
prune: true # Delete resources not in Git
selfHeal: true # Revert manual changes to cluster
allowEmpty: false
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
# ββ Ignore Differences βββββββββββββββββββββββββββββββββ
# Fields that ArgoCD should ignore when comparing
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore HPA-managed replicas
- group: ""
kind: Secret
managedFieldsManagers:
- external-secrets # Ignore fields managed by ESO
# ββ Revision History βββββββββββββββββββββββββββββββββββ
revisionHistoryLimit: 10
App of Apps Pattern
The App of Apps pattern creates a single ArgoCD Application that manages other Applications:
# argocd/apps/production-apps.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: production-apps
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/my-org/gitops-apps.git
targetRevision: main
directory:
recurse: true
jsonnet: {}
path: argocd/apps/production
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
# File structure in Git:
# argocd/apps/production/
# βββ payment-service.yaml # Individual Application manifests
# βββ user-service.yaml
# βββ notification-service.yaml
# βββ ingress-controller.yaml
Auto-sync and Prune Settings
| Setting | Description | Recommended |
|---|---|---|
automated.prune | Delete resources removed from Git | true (with PruneLast sync option) |
automated.selfHeal | Revert manual changes in cluster | true for production |
syncOptions.CreateNamespace | Auto-create namespace if missing | true |
syncOptions.Validate=true | Run kubectl validation before apply | true |
syncOptions.ApplyOutOfSyncOnly | Only apply changed resources | true (faster syncs) |
syncOptions.PruneLast | Delete resources after all creates | true (safer rollouts) |
Multi-Source Applications
ArgoCD 2.6+ supports multiple sources for a single Application, useful for combining a base Helm chart with environment-specific values:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
spec:
project: production
sources:
# Source 1: Helm chart from a Helm repository
- repoURL: https://charts.mycompany.io
chart: payment-service
targetRevision: 1.2.3
helm:
valueFiles:
- $values/apps/payment-service/overlays/production/values.yaml
# Source 2: Environment-specific values from Git
- repoURL: https://github.com/my-org/gitops-values.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ArgoCD with Helm
# ArgoCD Application for a Helm chart stored in Git
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: ingress-nginx
namespace: argocd
spec:
project: infrastructure
source:
repoURL: https://kubernetes.github.io/ingress-nginx
chart: ingress-nginx
targetRevision: 4.8.0
helm:
releaseName: ingress-nginx
values: |
controller:
replicaCount: 2
service:
annotations:
service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 1000m
memory: 512Mi
destination:
server: https://kubernetes.default.svc
namespace: ingress-nginx
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
ArgoCD Image Updater for Automated Deployments
ArgoCD Image Updater automatically updates container image tags in Git when new images are pushed:
# Install ArgoCD Image Updater
helm install argocd-image-updater argo/argocd-image-updater \
--namespace argocd \
--set config.registries[0].name=ECR \
--set config.registries[0].api_url=https://123456789012.dkr.ecr.us-east-1.amazonaws.com \
--set config.registries[0].prefix=123456789012.dkr.ecr.us-east-1.amazonaws.com \
--set config.registries[0].credentials=ext:/scripts/ecr-login.sh
# Annotate the Application to enable image updates
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: payment-service
namespace: argocd
annotations:
# Enable image updater for this application
argocd-image-updater.argoproj.io/image-list: "payment=123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-service"
# Update strategy: semver (semantic versioning)
argocd-image-updater.argoproj.io/payment.update-strategy: semver
argocd-image-updater.argoproj.io/payment.allow-tags: regexp:^v[0-9]+\.[0-9]+\.[0-9]+$
# Write updates back to Git (write-back method)
argocd-image-updater.argoproj.io/write-back-method: git:secret:argocd/git-creds
argocd-image-updater.argoproj.io/git-branch: main
# Alternatively: update the image tag in the Application spec (Kustomize)
# argocd-image-updater.argoproj.io/write-back-method: kustomize
spec:
source:
repoURL: https://github.com/my-org/gitops-apps.git
targetRevision: main
path: apps/payment-service/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
Progressive Delivery with Argo Rollouts
Argo Rollouts extends Deployment capabilities with progressive delivery strategies:
# Install Argo Rollouts
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# Canary deployment with automated analysis
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: payment-service
namespace: production
spec:
replicas: 10
strategy:
canary:
maxSurge: "25%"
maxUnavailable: 0
steps:
# Step 1: Deploy 10% of traffic to canary
- setWeight: 10
# Step 2: Wait 10 minutes and run analysis
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
# Step 3: Increase to 25%
- setWeight: 25
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
- templateName: latency-check
# Step 4: Increase to 50%
- setWeight: 50
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
# Step 5: Full rollout (100%)
- setWeight: 100
# Traffic routing with Istio or ALB Ingress
canaryService: payment-service-canary
stableService: payment-service-stable
trafficRouting:
alb:
ingress:
ingressName: payment-service
servicePort: 80
selector:
matchLabels:
app: payment-service
template:
metadata:
labels:
app: payment-service
spec:
containers:
- name: payment-service
image: "123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-service:v1.2.3"
ports:
- containerPort: 8080
---
# Automated analysis template
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: production
spec:
metrics:
- name: success-rate
interval: 5m
successCondition: result[0] >= 0.99
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
sum(rate(http_requests_total{service="payment-service",status!~"5.."}[5m]))
/
sum(rate(http_requests_total{service="payment-service"}[5m]))
Secrets Management in GitOps
Secrets must not be stored in plain text in Git. Three approaches for GitOps environments:
| Solution | Encryption | Rotation | Setup Complexity | Best For |
|---|---|---|---|---|
| Sealed Secrets | Client-side (asymmetric) | Manual | Low | Simple clusters, few secrets |
| External Secrets Operator | External vault | Automatic | Medium | Enterprise, many secrets |
| SOPS | Cloud KMS (AWS/GCP/Azure) | Manual | Medium | Flux-native, multi-key access |
Sealed Secrets
# Install Sealed Secrets controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system
# Install kubeseal CLI
brew install kubeseal # macOS
# wget https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/kubeseal-0.24.0-linux-amd64.tar.gz
# Create a secret and seal it
cat <<EOF > secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: payment-service-secrets
namespace: production
type: Opaque
stringData:
db_password: "SuperSecret123!"
api_key: "sk-live-abcdef123456"
EOF
kubeseal --controller-namespace=kube-system \
--controller-name=sealed-secrets \
--format yaml < secret.yaml > sealed-secret.yaml
# sealed-secret.yaml is safe to commit to Git!
kubectl apply -f sealed-secret.yaml
# The controller automatically decrypts to a regular Secret
External Secrets Operator
# Install External Secrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
--namespace external-secrets \
--create-namespace \
--set installCRDs=true
# Create a SecretStore (namespace-scoped) or ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
---
# Create an ExternalSecret that syncs from AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: payment-service-secrets
namespace: production
spec:
refreshInterval: 1h # Sync every hour
secretStoreRef:
kind: ClusterSecretStore
name: aws-secrets-manager
target:
name: payment-service-secrets
creationPolicy: Owner
template:
type: Opaque
data:
- secretKey: db_password
remoteRef:
key: production/payment-service/db
property: password
- secretKey: db_host
remoteRef:
key: production/payment-service/db
property: host
- secretKey: api_key
remoteRef:
key: production/payment-service/api
property: key
End-to-End Example: Git Repo to ArgoCD to Kubernetes
This complete example shows the full GitOps workflow from code commit to production deployment:
Git Repository Structure
gitops-platform/ # Infrastructure (managed by platform team)
βββ argocd/
β βββ projects/ # ArgoCD projects
β β βββ production.yaml
β β βββ staging.yaml
β βββ clusters/ # Cluster registrations
β βββ production.yaml
β βββ staging.yaml
βββ system/ # Cluster-level components
β βββ ingress-nginx/
β β βββ application.yaml
β β βββ values.yaml
β βββ cert-manager/
β βββ external-secrets/
β βββ monitoring/
β βββ kyverno/
βββ policies/
βββ kyverno-policies.yaml
gitops-apps/ # Application manifests (managed by dev teams)
βββ apps/
β βββ payment-service/
β β βββ base/ # Base Kustomize layer
β β β βββ deployment.yaml
β β β βββ service.yaml
β β β βββ ingress.yaml
β β β βββ kustomization.yaml
β β βββ overlays/
β β βββ production/
β β β βββ replicas.yaml
β β β βββ resource-limits.yaml
β β β βββ ingress-patch.yaml
β β β βββ kustomization.yaml
β β βββ staging/
β β βββ ...
β βββ user-service/
β βββ notification-service/
βββ argocd/
βββ apps/
βββ production/ # App of Apps
β βββ payment-service.yaml
β βββ user-service.yaml
β βββ kustomization.yaml
βββ staging/
βββ ...
Kustomize Configuration
# apps/payment-service/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: payment-service
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
commonLabels:
app.kubernetes.io/name: payment-service
app.kubernetes.io/part-of: payments-platform
images:
- name: payment-service
newName: 123456789012.dkr.ecr.us-east-1.amazonaws.com/payment-service
# apps/payment-service/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: production
namePrefix: prod-
resources:
- ../../base
patches:
- path: replicas.yaml
- path: resource-limits.yaml
- path: ingress-patch.yaml
replicas:
- name: payment-service
count: 5
images:
- name: payment-service
newTag: v1.2.3
Complete Workflow
- Developer commits code change to
payment-servicerepository - CI pipeline triggers: GitHub Actions builds Docker image, runs tests, pushes to ECR with tag
v1.2.4 - Git commit: Developer updates image tag in
gitops-apps/apps/payment-service/overlays/production/kustomization.yamlfromv1.2.3tov1.2.4 - Pull Request created: Changes reviewed by team; GitOps diff shown in PR via ArgoCD integration
- PR merged to main: ArgoCD detects change within 3 minutes (default poll interval)
- ArgoCD syncs: Application shows OutOfSync β Syncing β Synced β Healthy
- Rolling update: Kubernetes gradually replaces pods with new version; HPA maintains desired replicas
- Monitoring confirms: Grafana dashboards show healthy deployment; Prometheus alerts verify SLOs
# Verify sync in ArgoCD UI or CLI
argocd app get payment-service
# Name: payment-service
# Project: production
# Server: https://kubernetes.default.svc
# Namespace: production
# URL: https://argocd.example.com/applications/payment-service
# Repo: https://github.com/my-org/gitops-apps
# Target: main
# Path: apps/payment-service/overlays/production
# SyncWindow: Sync Allowed
# Sync Policy: Automated (Prune Resources, Self Heal)
# Sync Status: Synced to main (abc1234)
# Health Status: Healthy
# Last sync details
argocd app history payment-service
# ID DATE REVISION
# 0 2024-01-15 10:30:00 UTC main (abc1234)
# 1 2024-01-15 08:15:00 UTC main (def5678)
# Force a sync (if needed)
argocd app sync payment-service