GitOps and Continuous Deployment with ArgoCD and Flux
GitOps and Continuous Deployment with ArgoCD and Flux
GitOps is one of those ideas that sounds simple -- "use Git as the source of truth for infrastructure" -- but has real depth once you start implementing it. Done well, it gives you auditable, repeatable deployments with built-in rollback. Done poorly, it adds a layer of indirection that slows everything down.
This guide covers what GitOps actually means in practice, compares the two dominant tools (ArgoCD and Flux), and gives honest guidance on when it's worth adopting.
What GitOps Actually Means
GitOps has a specific definition beyond "we store YAML in Git." The core principles:
- Declarative configuration. The entire desired state of your system is described declaratively (Kubernetes manifests, Helm charts, Kustomize overlays).
- Versioned and immutable. That desired state lives in Git, giving you a full audit trail and the ability to roll back to any previous state.
- Pulled automatically. A software agent in the cluster continuously reconciles the actual state with the desired state from Git. You push to Git; the agent deploys.
- Continuously reconciled. If someone manually changes the cluster (kubectl edit, dashboard changes), the agent detects the drift and reverts it.
The key difference from traditional CI/CD: in a traditional pipeline, the CI system pushes changes to the cluster. In GitOps, the cluster pulls changes from Git. This distinction matters for security -- the cluster needs read access to Git, not the other way around. Your CI system never needs cluster credentials.
GitOps vs Traditional CI/CD
Traditional CI/CD:
Developer -> Git Push -> CI Build -> CI Deploy -> Cluster
(CI needs cluster credentials)
GitOps:
Developer -> Git Push -> CI Build -> Push manifest to Git
Agent in Cluster -> Watches Git -> Applies changes -> Cluster
(Cluster needs Git read access only)
The security improvement is significant. In traditional CI/CD, a compromised CI runner can deploy arbitrary code to production. In GitOps, a compromised CI runner can only modify a Git repository, and any changes are visible, reviewable, and revertible.
ArgoCD
ArgoCD is the most popular GitOps tool. It's a CNCF graduated project with a polished web UI, granular RBAC, and support for Helm, Kustomize, Jsonnet, and plain YAML.
Installation
# Create namespace and install
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Wait for pods
kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=argocd-server -n argocd --timeout=120s
# Get initial admin password
argocd admin initial-password -n argocd
# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Login via CLI
argocd login localhost:8080 --insecure
Creating an Application
ArgoCD applications define the mapping between a Git repository and a Kubernetes cluster/namespace:
# application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: myapp
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/myorg/myapp-deploy.git
targetRevision: main
path: overlays/production
destination:
server: https://kubernetes.default.svc
namespace: myapp
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Revert manual cluster changes
syncOptions:
- CreateNamespace=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
kubectl apply -f application.yaml
With automated.selfHeal: true, if someone runs kubectl edit to change a deployment, ArgoCD will detect the drift and revert it within seconds. This is GitOps enforcement -- the Git repository is the single source of truth.
Application Sets
For managing many applications (microservices, multi-environment), ApplicationSets generate Application resources from templates:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: microservices
namespace: argocd
spec:
generators:
- git:
repoURL: https://github.com/myorg/deploy.git
revision: main
directories:
- path: "apps/*"
template:
metadata:
name: "{{path.basename}}"
spec:
project: default
source:
repoURL: https://github.com/myorg/deploy.git
targetRevision: main
path: "{{path}}"
destination:
server: https://kubernetes.default.svc
namespace: "{{path.basename}}"
This creates one ArgoCD Application for every directory under apps/ in your deploy repo. Add a new service by adding a directory -- no ArgoCD configuration changes needed.
ArgoCD Strengths
- Web UI. The dashboard shows sync status, resource health, and a live view of your Kubernetes objects. Useful for debugging and giving non-CLI users visibility.
- RBAC. Fine-grained access control. Restrict which teams can sync which applications.
- Multi-cluster. Manage deployments across multiple Kubernetes clusters from a single ArgoCD instance.
- Diff preview. See exactly what will change before syncing.
Flux
Flux (v2) is the other major GitOps tool, also a CNCF graduated project. Where ArgoCD takes an application-centric approach with a UI, Flux takes a Kubernetes-native approach -- everything is a Custom Resource, and there is no built-in UI.
Installation
# Install Flux CLI
brew install fluxcd/tap/flux # macOS
curl -s https://fluxcd.io/install.sh | bash # Linux
# Bootstrap Flux into your cluster (creates repo structure)
flux bootstrap github \
--owner=myorg \
--repository=fleet-infra \
--branch=main \
--path=clusters/production \
--personal
The bootstrap command does several things: installs Flux controllers into the cluster, creates the Git repository if it doesn't exist, and commits the Flux configuration to the repository. From this point, Flux manages itself through GitOps -- including its own upgrades.
Defining Sources and Kustomizations
Flux separates "where to get manifests" (Sources) from "how to apply them" (Kustomizations):
# source.yaml - GitRepository source
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: myapp
namespace: flux-system
spec:
interval: 1m
url: https://github.com/myorg/myapp-deploy.git
ref:
branch: main
secretRef:
name: myapp-git-credentials
---
# kustomization.yaml - What to deploy from the source
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: myapp
namespace: flux-system
spec:
interval: 5m
targetNamespace: myapp
sourceRef:
kind: GitRepository
name: myapp
path: ./overlays/production
prune: true
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: myapp
namespace: myapp
timeout: 3m
Flux with Helm
Flux has first-class Helm support through HelmRepository and HelmRelease resources:
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: ingress-nginx
namespace: flux-system
spec:
interval: 1h
url: https://kubernetes.github.io/ingress-nginx
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: ingress-nginx
namespace: ingress-nginx
spec:
interval: 30m
chart:
spec:
chart: ingress-nginx
version: "4.x"
sourceRef:
kind: HelmRepository
name: ingress-nginx
namespace: flux-system
values:
controller:
replicaCount: 2
metrics:
enabled: true
Flux Strengths
- Kubernetes-native. Everything is a CRD. If you think in kubectl and YAML, Flux feels natural.
- Self-bootstrapping. Flux manages its own installation through GitOps.
- Notification system. Built-in alerts to Slack, Teams, Discord, and generic webhooks.
- Image automation. Flux can watch container registries and automatically update image tags in Git when new versions are pushed.
ArgoCD vs Flux: Honest Comparison
| Aspect | ArgoCD | Flux |
|---|---|---|
| UI | Built-in, polished | None (use Weave GitOps or Capacitor) |
| Architecture | Centralized server | Distributed controllers |
| Multi-cluster | Native (managed centrally) | Per-cluster install (use Flux with remote sources) |
| Helm support | Renders in cluster | HelmRelease CRD (more Kubernetes-native) |
| Image automation | Separate ArgoCD Image Updater | Built-in (Image Reflector + Automation controllers) |
| RBAC | Built-in, project-based | Delegates to Kubernetes RBAC |
| Learning curve | Moderate (UI helps) | Steeper (CRD-heavy, no UI) |
| Resource usage | Higher (UI, API server, repo server) | Lower (lightweight controllers) |
| Community | Larger (more stars, more contributors) | Smaller but active |
Choose ArgoCD when: You want a UI for visibility, manage multiple clusters centrally, or have teams that prefer visual tools over CLI-only workflows.
Choose Flux when: You want a lightweight, Kubernetes-native approach, need image automation built-in, or prefer managing everything through YAML and kubectl.
Both tools are CNCF graduated and production-ready. The choice is more about workflow preference than capability.
Handling Secrets in GitOps
The hardest problem in GitOps is secrets. You cannot commit plaintext secrets to Git, but your applications need them. There are several approaches:
Sealed Secrets
Bitnami's Sealed Secrets encrypts secrets with a cluster-side key. Only the cluster can decrypt them.
# Install
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.27.0/controller.yaml
# Encrypt a secret
kubectl create secret generic myapp-db \
--from-literal=password=supersecret \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-secret.yaml
The encrypted SealedSecret resource is safe to commit to Git. The controller in the cluster decrypts it into a regular Kubernetes Secret.
SOPS (Mozilla)
SOPS encrypts values within YAML files, leaving keys readable. Works with AWS KMS, GCP KMS, Azure Key Vault, and age.
# Encrypt
sops --encrypt --age age1... secret.yaml > secret.enc.yaml
# Flux decrypts automatically with SOPS provider
Flux has native SOPS integration -- add a decryption field to your Kustomization and Flux decrypts on the fly.
External Secrets Operator
External Secrets Operator syncs secrets from external stores (AWS Secrets Manager, HashiCorp Vault, 1Password) into Kubernetes Secrets:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: myapp-db
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: myapp-db
data:
- secretKey: password
remoteRef:
key: production/myapp/database
property: password
This is the most operationally sound approach for teams already using a secrets manager. The secret values never touch Git.
Rollback Strategies
Git Revert
The simplest rollback in GitOps: revert the commit that introduced the bad change.
git revert HEAD
git push origin main
The GitOps agent detects the new commit and applies the previous state. This is the canonical GitOps rollback -- it creates an audit trail and is the same process as any other change.
ArgoCD Rollback
ArgoCD keeps a history of synced revisions:
# List sync history
argocd app history myapp
# Rollback to a previous revision
argocd app rollback myapp <revision-number>
Note that this is a temporary fix -- ArgoCD will eventually re-sync from Git. For a permanent rollback, revert in Git.
Flux Rollback
Flux with Helm supports automatic rollback on failed deployments:
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
spec:
upgrade:
remediation:
remediateLastFailure: true
retries: 3
rollback:
cleanupOnFail: true
recreate: true
If a Helm upgrade fails health checks, Flux automatically rolls back to the last successful release.
When GitOps Is Overkill
GitOps adds real complexity: a separate deployment repository (or directory), a reconciliation agent, secrets management, and a new mental model. It is not always worth it.
GitOps is overkill when:
- You deploy to a single server or PaaS (Heroku, Render, Railway)
- Your team is small (1-5 developers) and deploys infrequently
- You don't use Kubernetes
- Your deployment is a simple
docker compose upor a static site push - You can't afford the operational overhead of running and monitoring the GitOps agent
GitOps is worth it when:
- You run Kubernetes in production with multiple services
- Multiple teams deploy independently and you need audit trails
- You want drift detection (prevent ad-hoc kubectl changes)
- You need multi-environment promotion (dev -> staging -> production)
- Compliance requires you to show who changed what and when
The honest truth: if you're not on Kubernetes, GitOps tools like ArgoCD and Flux don't apply. And if you are on Kubernetes but deploy one or two services, a simple GitHub Actions workflow that runs kubectl apply is fine. GitOps shines at scale -- many services, many environments, many teams.
Getting Started
If you're adopting GitOps for the first time:
- Start with one non-critical service. Don't migrate your entire platform at once.
- Use a separate deployment repo (or a clearly separated directory in your mono-repo). Keep application code and deployment manifests on different commit cadences.
- Choose Sealed Secrets or SOPS for secrets initially. External Secrets Operator is better long-term but adds another component to manage.
- Enable automated sync with self-heal from the start. Manual sync mode defeats the purpose of GitOps.
- Set up notifications. Both ArgoCD and Flux support Slack/webhook alerts. You need to know when syncs fail.
GitOps is a workflow, not a product. The tools enforce the workflow, but the real value comes from treating Git as the single source of truth for your infrastructure state. Once that mental model clicks, deployments become predictable and auditable -- and that is worth the setup cost.