Skip to content

GitOps for Infrastructure: ArgoCD Patterns and Pitfalls

GitOps with ArgoCD for Kubernetes: the application-of-applications pattern, secret handling, drift detection, multi-cluster sync and progressive rollouts.

4 min read
GitOps workflow diagram showing ArgoCD reconciling desired state from Git repositories with actual Kubernetes cluster state

GitOps makes Git the single source of truth for infrastructure state. Instead of running kubectl apply or clicking through dashboards, you commit manifests to a repository and a reconciliation controller—ArgoCD—ensures the cluster matches what's declared in Git. If someone manually changes something in the cluster, ArgoCD detects the drift and corrects it.

This sounds simple, but production GitOps has sharp edges. Secret management, multi-cluster coordination, progressive rollouts, and application dependency ordering all require patterns beyond the basic tutorials.

Application of Applications Pattern

Managing dozens of ArgoCD Applications individually doesn't scale. The App of Apps pattern uses a parent Application that manages child Applications declaratively.

ymlyaml
# ❌ Managing each app manually via CLI
# argocd app create frontend --repo https://... --path frontend
# argocd app create backend --repo https://... --path backend
# argocd app create monitoring --repo https://... --path monitoring
# Doesn't scale, no audit trail, no declarative management
ymlyaml
# ✅ Parent Application that manages all child apps
# root-app/application.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: platform-root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/org/platform-apps
    targetRevision: main
    path: apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
ymlyaml
# apps/frontend.yaml — child Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: frontend
  namespace: argocd
  annotations:
    argocd.argoproj.io/sync-wave: "2"
spec:
  project: default
  source:
    repoURL: https://github.com/org/platform-apps
    targetRevision: main
    path: manifests/frontend
    helm:
      valueFiles:
        - values.yaml
        - values-production.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: frontend
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

Sync waves control ordering: infrastructure (wave: 0) deploys before databases (wave: 1) which deploy before applications (wave: 2). This prevents failures from missing dependencies.

Secret Management without Storing Secrets in Git

The fundamental tension in GitOps: everything should be in Git, but secrets cannot be in Git. Several patterns resolve this.

tstypescript
// Approach 1: Sealed Secrets — encrypt secrets for Git storage
interface SealedSecretWorkflow {
  steps: string[];
}
 
const sealedSecretsApproach: SealedSecretWorkflow = {
  steps: [
    // 1. Create a regular Kubernetes secret
    "kubectl create secret generic db-creds --from-literal=password=s3cure --dry-run=client -o yaml > secret.yaml",
    // 2. Encrypt it with the cluster's public key
    "kubeseal --format yaml < secret.yaml > sealed-secret.yaml",
    // 3. Commit the sealed version to Git
    "git add sealed-secret.yaml && git commit -m 'Add db credentials'",
    // 4. ArgoCD applies it; controller decrypts in-cluster
  ],
};
 
// Approach 2: External Secrets Operator — reference from vault
// external-secret.yaml
const externalSecretManifest = `
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: backend
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: ClusterSecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: production/database
        property: username
    - secretKey: password
      remoteRef:
        key: production/database
        property: password
`;

External Secrets Operator is the better long-term pattern. It decouples secret lifecycle from deployment lifecycle—secrets rotate in the vault without any Git commits.

Drift Detection and Self-Healing

ArgoCD continuously compares the desired state (Git) with the actual state (cluster). When they diverge, it can alert or automatically correct.

tstypescript
interface DriftPolicy {
  autoSync: boolean;
  selfHeal: boolean;
  prune: boolean;
  allowedDrift: DriftException[];
}
 
interface DriftException {
  group: string;
  kind: string;
  jsonPointers: string[];
}
 
// Configure which fields are allowed to drift
// (managed by controllers, not humans)
const driftPolicy: DriftPolicy = {
  autoSync: true,
  selfHeal: true,
  prune: true,
  allowedDrift: [
    {
      // HPA modifies replica count
      group: "apps",
      kind: "Deployment",
      jsonPointers: ["/spec/replicas"],
    },
    {
      // Cert-manager updates TLS secrets
      group: "",
      kind: "Secret",
      jsonPointers: ["/data"],
    },
  ],
};
 
// Corresponding ArgoCD Application config
const ignoreDifferences = `
spec:
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
    - group: ""
      kind: Secret
      jsonPointers:
        - /data/tls.crt
        - /data/tls.key
`;

Without ignoreDifferences, ArgoCD will constantly fight with the Horizontal Pod Autoscaler—ArgoCD sets replicas to the Git value, HPA immediately changes it, ArgoCD detects drift and resets it. This loop is one of the most common ArgoCD pitfalls.

Multi-Cluster Sync Strategy

Production GitOps usually involves multiple clusters: staging, production, and sometimes regional clusters. The Git repository structure must support environment-specific configuration.

tstypescript
interface MultiClusterConfig {
  structure: "directory-per-env" | "branch-per-env" | "overlay";
}
 
// Recommended: Kustomize overlays
const repoStructure = {
  "base/": {
    "deployment.yaml": "Base deployment manifest",
    "service.yaml": "Base service manifest",
    "kustomization.yaml": "Base kustomization",
  },
  "overlays/staging/": {
    "kustomization.yaml": `
      bases:
        - ../../base
      patches:
        - replica-count.yaml
      configMapGenerator:
        - name: app-config
          literals:
            - LOG_LEVEL=debug
            - API_URL=https://api.staging.example.com
    `,
  },
  "overlays/production/": {
    "kustomization.yaml": `
      bases:
        - ../../base
      patches:
        - replica-count.yaml
        - resource-limits.yaml
      configMapGenerator:
        - name: app-config
          literals:
            - LOG_LEVEL=warn
            - API_URL=https://api.example.com
    `,
  },
};
 
// ArgoCD ApplicationSet for multi-cluster
const applicationSet = `
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: backend-service
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - cluster: staging
            url: https://staging-k8s.internal
            overlay: staging
          - cluster: production
            url: https://prod-k8s.internal
            overlay: production
  template:
    metadata:
      name: 'backend-{{cluster}}'
    spec:
      source:
        repoURL: https://github.com/org/platform-apps
        targetRevision: main
        path: 'manifests/backend/overlays/{{overlay}}'
      destination:
        server: '{{url}}'
        namespace: backend
`;

ApplicationSets generate Applications from templates, reducing duplication and ensuring consistency across clusters. Each environment gets the same base manifests with environment-specific overlays.

Progressive Rollout Integration

GitOps with ArgoCD pairs well with Argo Rollouts for progressive delivery. Instead of replacing all pods at once, traffic shifts gradually with automated analysis.

ymlyaml
# rollout.yaml — replaces Deployment
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: backend-api
  namespace: backend
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 5
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate-check
        - setWeight: 25
        - pause: { duration: 10m }
        - analysis:
            templates:
              - templateName: success-rate-check
        - setWeight: 50
        - pause: { duration: 10m }
        - setWeight: 100
      canaryService: backend-api-canary
      stableService: backend-api-stable
  selector:
    matchLabels:
      app: backend-api
  template:
    metadata:
      labels:
        app: backend-api
    spec:
      containers:
        - name: api
          image: registry.example.com/backend:v2.1.0
ymlyaml
# analysis-template.yaml
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate-check
spec:
  metrics:
    - name: success-rate
      interval: 60s
      successCondition: result[0] > 0.99
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{
              service="backend-api",
              status=~"2.."
            }[5m])) /
            sum(rate(http_requests_total{
              service="backend-api"
            }[5m]))

A Git commit updates the image tag, ArgoCD syncs the Rollout, and Argo Rollouts progressively shifts traffic. If the analysis template detects degraded success rates, it automatically rolls back—no human intervention needed.

Key Takeaways

GitOps with ArgoCD makes Git the single source of truth for infrastructure, providing audit trails, rollback through git revert, and declarative cluster management. Use the App of Apps pattern with sync waves to manage dozens of applications with proper dependency ordering. Handle secrets through External Secrets Operator or Sealed Secrets—never store plaintext credentials in Git. Configure ignoreDifferences for fields managed by cluster controllers like HPA and cert-manager to prevent reconciliation loops. Structure multi-cluster configurations using Kustomize overlays with ApplicationSets to generate environment-specific applications from templates. Integrate Argo Rollouts for progressive delivery that automatically analyzes canary metrics and rolls back on degradation. The goal is a system where a git revert is your disaster recovery plan—if the current state is broken, reverting the commit restores the last known good state automatically.

Wilfredo Rujel

Wilfredo Rujel

Full Stack Software Engineer

Share this postX