GitOps treats Git as the single source of truth for infrastructure and applications. ArgoCD makes this practical by continuously reconciling your cluster state with your Git repositories. Push to Git, ArgoCD deploys. Drift detected, ArgoCD corrects.

Core GitOps Principles

  1. Declarative: Everything is defined as code
  2. Versioned: Git provides history and audit trail
  3. Automated: Changes are automatically applied
  4. Self-healing: Drift is detected and corrected

Installing ArgoCD

# Create namespace
kubectl create namespace argocd

# Install 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=300s

# Get initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d

# Port forward to access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443

Access at https://localhost:8080 with username admin.

Repository Structure

A GitOps-friendly repository structure:

gitops-repo/
├── apps/                      # Application definitions
│   ├── myapp/
│   │   ├── base/             # Base Kubernetes manifests
│   │   │   ├── deployment.yaml
│   │   │   ├── service.yaml
│   │   │   └── kustomization.yaml
│   │   └── overlays/         # Environment-specific patches
│   │       ├── dev/
│   │       ├── staging/
│   │       └── prod/
│   └── another-app/
├── infrastructure/            # Cluster infrastructure
│   ├── cert-manager/
│   ├── ingress-nginx/
│   └── monitoring/
└── argocd/                    # ArgoCD configuration
    ├── projects/
    └── applications/

Creating Applications

Using CLI

# Install ArgoCD CLI
brew install argocd  # or download from releases

# Login
argocd login localhost:8080 --insecure

# Create application
argocd app create myapp \
    --repo https://github.com/myorg/gitops-repo.git \
    --path apps/myapp/overlays/prod \
    --dest-server https://kubernetes.default.svc \
    --dest-namespace myapp \
    --sync-policy automated

Using Declarative YAML

# argocd/applications/myapp.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: myapp
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: default

  source:
    repoURL: https://github.com/myorg/gitops-repo.git
    targetRevision: main
    path: apps/myapp/overlays/prod

  destination:
    server: https://kubernetes.default.svc
    namespace: myapp

  syncPolicy:
    automated:
      prune: true       # Delete resources not in Git
      selfHeal: true    # Fix drift automatically
      allowEmpty: false # Don't sync empty folders

    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true

    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

Sync Strategies

Automatic Sync

ArgoCD automatically syncs when Git changes:

spec:
  syncPolicy:
    automated:
      prune: true      # Remove resources deleted from Git
      selfHeal: true   # Correct manual changes to cluster

Manual Sync

Require manual intervention (good for production):

spec:
  syncPolicy: {}  # No automated section = manual sync

Trigger sync via CLI:

argocd app sync myapp

Sync Waves and Hooks

Control deployment order:

# Deploy namespace first
apiVersion: v1
kind: Namespace
metadata:
  name: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "-1"  # Deploy before wave 0
---
# Deploy ConfigMap before Deployment
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  annotations:
    argocd.argoproj.io/sync-wave: "0"
---
# Deploy app after ConfigMap
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  annotations:
    argocd.argoproj.io/sync-wave: "1"

Sync Hooks

Run jobs at specific points:

apiVersion: batch/v1
kind: Job
metadata:
  name: db-migrate
  annotations:
    argocd.argoproj.io/hook: PreSync      # Run before sync
    argocd.argoproj.io/hook-delete-policy: HookSucceeded
spec:
  template:
    spec:
      containers:
        - name: migrate
          image: myorg/migrate:latest
          command: ["./migrate.sh"]
      restartPolicy: Never

Hook types: PreSync, Sync, PostSync, SyncFail

Projects for Multi-Tenancy

Projects organize applications and enforce policies:

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: team-platform
  namespace: argocd
spec:
  description: Platform team applications

  # Allowed source repos
  sourceRepos:
    - https://github.com/myorg/platform-gitops.git
    - https://charts.jetstack.io

  # Allowed destinations
  destinations:
    - namespace: '*'
      server: https://kubernetes.default.svc

  # Allowed cluster resources
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRole
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRoleBinding

  # Deny certain namespaces
  namespaceResourceBlacklist:
    - group: ''
      kind: Secret
      namespace: kube-system

  # Roles for team access
  roles:
    - name: developer
      description: Platform team developers
      policies:
        - p, proj:team-platform:developer, applications, get, team-platform/*, allow
        - p, proj:team-platform:developer, applications, sync, team-platform/*, allow
      groups:
        - platform-team

Kustomize Integration

ArgoCD natively supports Kustomize:

# apps/myapp/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - deployment.yaml
  - service.yaml
  - configmap.yaml

commonLabels:
  app: myapp
# apps/myapp/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  - ../../base

namespace: myapp-prod

replicas:
  - name: myapp
    count: 5

images:
  - name: myorg/myapp
    newTag: v1.2.3

patches:
  - path: deployment-patch.yaml
# apps/myapp/overlays/prod/deployment-patch.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  template:
    spec:
      containers:
        - name: myapp
          resources:
            requests:
              cpu: 500m
              memory: 512Mi
            limits:
              cpu: 1000m
              memory: 1Gi

Helm Integration

Deploy Helm charts with ArgoCD:

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: ingress-nginx
  namespace: argocd
spec:
  project: default

  source:
    repoURL: https://kubernetes.github.io/ingress-nginx
    chart: ingress-nginx
    targetRevision: 4.9.0
    helm:
      releaseName: ingress-nginx
      values: |
        controller:
          replicaCount: 2
          metrics:
            enabled: true
          resources:
            requests:
              cpu: 100m
              memory: 128Mi

  destination:
    server: https://kubernetes.default.svc
    namespace: ingress-nginx

  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

App of Apps Pattern

Manage multiple applications with a single application:

# argocd/applications/root.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/gitops-repo.git
    targetRevision: main
    path: argocd/applications  # Contains all app definitions
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Now every Application YAML in argocd/applications/ is managed by ArgoCD!

ApplicationSet for Dynamic Apps

Generate applications automatically:

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: myapp-environments
  namespace: argocd
spec:
  generators:
    # Generate app for each environment
    - list:
        elements:
          - env: dev
            cluster: https://kubernetes.default.svc
            replicas: 1
          - env: staging
            cluster: https://kubernetes.default.svc
            replicas: 2
          - env: prod
            cluster: https://prod-cluster.example.com
            replicas: 5

  template:
    metadata:
      name: 'myapp-{{env}}'
    spec:
      project: default
      source:
        repoURL: https://github.com/myorg/gitops-repo.git
        targetRevision: main
        path: 'apps/myapp/overlays/{{env}}'
      destination:
        server: '{{cluster}}'
        namespace: 'myapp-{{env}}'
      syncPolicy:
        automated:
          prune: true
          selfHeal: true

Secrets Management

ArgoCD works with various secrets solutions:

Sealed Secrets

# Encrypt secrets before committing
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: myapp-secrets
  namespace: myapp
spec:
  encryptedData:
    api-key: AgBy8hqN...encrypted...

External Secrets

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-secrets
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: myapp-secrets
  data:
    - secretKey: api-key
      remoteRef:
        key: prod/myapp/api-key

Health Checks

Custom health checks for CRDs:

# argocd-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  resource.customizations.health.cert-manager.io_Certificate: |
    hs = {}
    if obj.status ~= nil then
      if obj.status.conditions ~= nil then
        for i, condition in ipairs(obj.status.conditions) do
          if condition.type == "Ready" and condition.status == "True" then
            hs.status = "Healthy"
            hs.message = "Certificate is ready"
            return hs
          end
        end
      end
    end
    hs.status = "Progressing"
    hs.message = "Waiting for certificate"
    return hs

Notifications

Configure notifications for sync events:

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-notifications-cm
  namespace: argocd
data:
  service.slack: |
    token: $slack-token

  template.app-sync-succeeded: |
    message: |
      Application {{.app.metadata.name}} sync succeeded
      Revision: {{.app.status.sync.revision}}

  trigger.on-sync-succeeded: |
    - when: app.status.operationState.phase in ['Succeeded']
      send: [app-sync-succeeded]

Key Takeaways

  1. Git is the source of truth — never kubectl apply manually
  2. Use automated sync with selfHeal — let ArgoCD fix drift
  3. Organize with projects — separate teams and environments
  4. App of Apps — manage applications declaratively
  5. Sync waves — control deployment order
  6. External secrets — never commit secrets to Git
  7. ApplicationSets — generate apps dynamically

“GitOps is not just about automation. It’s about having a complete audit trail, easy rollbacks, and the confidence that your cluster state matches your Git state.”