Published on

GitOps with ArgoCD: Modern Kubernetes Deployment Patterns

30 min read

Authors

GitOps with ArgoCD: Modern Kubernetes Deployment Patterns

After years of wrestling with complex deployment pipelines, manual kubectl commands, and inconsistent environments, I discovered GitOpsβ€”and it fundamentally changed how I approach Kubernetes deployments. GitOps isn't just a deployment strategy; it's a paradigm shift toward declarative, auditable, and reliable infrastructure management.

In this comprehensive guide, I'll share how I've implemented GitOps with ArgoCD in production environments, the patterns that work, and the pitfalls to avoid.

Why GitOps? The Problems It Solves

Before diving into ArgoCD specifics, let me share the pain points that led me to embrace GitOps:

The Traditional CI/CD Pain Points

πŸ”₯ The Problems I Faced:

  • Deployment Drift: Production environments diverging from expected state
  • Security Concerns: CI/CD systems needing cluster admin access
  • Audit Challenges: Difficulty tracking who deployed what and when
  • Environment Inconsistency: Different deployment processes across environments
  • Rollback Complexity: Manual and error-prone rollback procedures

βœ… How GitOps Solved Them:

  • Single Source of Truth: Git repositories define the desired state
  • Pull-based Deployment: ArgoCD pulls changes, eliminating the need for external access
  • Automatic Drift Detection: Continuous reconciliation ensures actual state matches desired state
  • Complete Audit Trail: Every change is tracked through Git commits
  • Declarative Rollbacks: Simply revert a Git commit to roll back

ArgoCD Architecture and Core Concepts

Understanding ArgoCD Components

# ArgoCD installation using official manifests
apiVersion: v1
kind: Namespace
metadata:
  name: argocd
---
# Install ArgoCD
# kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml

# Custom ArgoCD configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  # Enable Git webhook support
  webhook.github.secret: 'your-webhook-secret'

  # Repository credentials template
  repository.credentials: |
    - url: https://github.com/your-org
      passwordSecret:
        name: github-token
        key: token
      usernameSecret:
        name: github-token
        key: username

  # OIDC configuration for SSO
  oidc.config: |
    name: GitHub
    issuer: https://github.com
    clientId: your-github-oauth-app-id
    clientSecret: $oidc.github.clientSecret
    requestedScopes: ["user:email"]
    requestedIDTokenClaims: {"groups": {"essential": true}}

  # Policy configuration
  policy.default: role:readonly
  policy.csv: |
    p, role:admin, applications, *, */*, allow
    p, role:admin, clusters, *, *, allow
    p, role:admin, repositories, *, *, allow

    p, role:developer, applications, *, default/*, allow
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, default/*, allow

    g, your-org:platform-team, role:admin
    g, your-org:developers, role:developer

ArgoCD Application Pattern

Here's my standard ArgoCD application structure:

# applications/web-app-production.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-app-production
  namespace: argocd
  # Finalizers ensure proper cleanup
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production

  # Source configuration
  source:
    repoURL: https://github.com/your-org/k8s-manifests
    targetRevision: main
    path: applications/web-app/overlays/production

    # Kustomize configuration
    kustomize:
      images:
        - name: web-app
          newTag: 'v1.2.3'
      patchesStrategicMerge:
        - |-
          apiVersion: apps/v1
          kind: Deployment
          metadata:
            name: web-app
          spec:
            replicas: 6

  # Destination configuration
  destination:
    server: https://kubernetes.default.svc
    namespace: production

  # Sync policy
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true
    retry:
      limit: 5
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  # Health checks
  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas
    - group: ''
      kind: Secret
      jsonPointers:
        - /data

  # Sync waves for ordered deployment
  syncWaves:
    - name: databases
      wave: 0
    - name: applications
      wave: 1
    - name: ingress
      wave: 2

Repository Structure: The Foundation

A well-organized repository structure is crucial for successful GitOps. Here's the pattern I've refined:

k8s-manifests/
β”œβ”€β”€ applications/
β”‚   β”œβ”€β”€ web-app/
β”‚   β”‚   β”œβ”€β”€ base/
β”‚   β”‚   β”‚   β”œβ”€β”€ kustomization.yaml
β”‚   β”‚   β”‚   β”œβ”€β”€ deployment.yaml
β”‚   β”‚   β”‚   β”œβ”€β”€ service.yaml
β”‚   β”‚   β”‚   └── configmap.yaml
β”‚   β”‚   └── overlays/
β”‚   β”‚       β”œβ”€β”€ development/
β”‚   β”‚       β”‚   β”œβ”€β”€ kustomization.yaml
β”‚   β”‚       β”‚   β”œβ”€β”€ config.env
β”‚   β”‚       β”‚   └── replica-patch.yaml
β”‚   β”‚       β”œβ”€β”€ staging/
β”‚   β”‚       └── production/
β”‚   β”‚           β”œβ”€β”€ kustomization.yaml
β”‚   β”‚           β”œβ”€β”€ config.env
β”‚   β”‚           β”œβ”€β”€ hpa.yaml
β”‚   β”‚           └── monitoring.yaml
β”œβ”€β”€ infrastructure/
β”‚   β”œβ”€β”€ monitoring/
β”‚   β”‚   β”œβ”€β”€ prometheus/
β”‚   β”‚   β”œβ”€β”€ grafana/
β”‚   β”‚   └── alertmanager/
β”‚   β”œβ”€β”€ ingress/
β”‚   └── security/
β”œβ”€β”€ argocd/
β”‚   β”œβ”€β”€ projects/
β”‚   β”‚   β”œβ”€β”€ production-project.yaml
β”‚   β”‚   └── development-project.yaml
β”‚   └── applications/
β”‚       β”œβ”€β”€ production/
β”‚       └── development/
└── scripts/
    β”œβ”€β”€ validate-manifests.sh
    └── generate-applications.sh

Base Application Manifests

# applications/web-app/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

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

commonLabels:
  app: web-app
  component: backend

configMapGenerator:
  - name: web-app-config
    envs:
      - config.env
    options:
      disableNameSuffixHash: true

images:
  - name: web-app
    newName: myregistry/web-app
    newTag: latest
# applications/web-app/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-app
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
      annotations:
        # Force restart on config changes
        checksum/config:
          {
            {
              include (print $.Template.BasePath "/configmap.yaml") . | sha256sum,
            },
          }
    spec:
      containers:
        - name: web-app
          image: web-app
          ports:
            - containerPort: 8080
              name: http
          env:
            - name: PORT
              value: '8080'
          envFrom:
            - configMapRef:
                name: web-app-config
            - secretRef:
                name: web-app-secrets
          resources:
            requests:
              memory: '256Mi'
              cpu: '100m'
            limits:
              memory: '512Mi'
              cpu: '200m'
          livenessProbe:
            httpGet:
              path: /health
              port: http
            initialDelaySeconds: 30
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /ready
              port: http
            initialDelaySeconds: 5
            periodSeconds: 5
          securityContext:
            allowPrivilegeEscalation: false
            runAsNonRoot: true
            runAsUser: 1000
            capabilities:
              drop:
                - ALL
      securityContext:
        fsGroup: 1000

Environment-Specific Overlays

# applications/web-app/overlays/production/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

namespace: production

resources:
  - ../../base
  - hpa.yaml
  - pdb.yaml
  - network-policy.yaml
  - service-monitor.yaml

patchesStrategicMerge:
  - replica-patch.yaml
  - resource-patch.yaml

configMapGenerator:
  - name: web-app-config
    envs:
      - config.env
    behavior: merge

images:
  - name: web-app
    newTag: 'v1.2.3'

# Sync wave annotations for ordered deployment
commonAnnotations:
  argocd.argoproj.io/sync-wave: '1'
# applications/web-app/overlays/production/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: web-app-hpa
  annotations:
    argocd.argoproj.io/sync-wave: '2'
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: web-app
  minReplicas: 3
  maxReplicas: 20
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
    - type: Resource
      resource:
        name: memory
        target:
          type: Utilization
          averageUtilization: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60

Advanced GitOps Patterns

1. App of Apps Pattern

The "App of Apps" pattern allows you to manage multiple applications declaratively:

# argocd/root-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root-app
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/k8s-manifests
    targetRevision: main
    path: argocd/applications/production
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
# argocd/applications/production/web-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web-app
  namespace: argocd
spec:
  project: production
  source:
    repoURL: https://github.com/your-org/k8s-manifests
    targetRevision: main
    path: applications/web-app/overlays/production
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

2. Multi-Environment Management

# argocd/projects/production-project.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: production
  namespace: argocd
spec:
  description: Production applications

  # Source repositories
  sourceRepos:
    - 'https://github.com/your-org/k8s-manifests'
    - 'https://charts.helm.sh/stable'

  # Destination clusters and namespaces
  destinations:
    - namespace: 'production'
      server: https://kubernetes.default.svc
    - namespace: 'monitoring'
      server: https://kubernetes.default.svc

  # Cluster resource whitelist
  clusterResourceWhitelist:
    - group: ''
      kind: Namespace
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRole
    - group: 'rbac.authorization.k8s.io'
      kind: ClusterRoleBinding

  # Namespace resource whitelist
  namespaceResourceWhitelist:
    - group: '*'
      kind: '*'

  # Roles for this project
  roles:
    - name: production-admin
      description: 'Admin access to production project'
      policies:
        - p, proj:production:production-admin, applications, *, production/*, allow
        - p, proj:production:production-admin, repositories, *, *, allow
      groups:
        - your-org:platform-team

    - name: production-developer
      description: 'Developer access to production project'
      policies:
        - p, proj:production:production-developer, applications, get, production/*, allow
        - p, proj:production:production-developer, applications, sync, production/*, allow
      groups:
        - your-org:developers

3. Sealed Secrets Integration

Managing secrets in GitOps requires special handling. I use Sealed Secrets:

#!/bin/bash
# scripts/create-sealed-secret.sh

SECRET_NAME=$1
NAMESPACE=$2
KEY_VALUE_PAIRS=$3

echo "Creating sealed secret: $SECRET_NAME in namespace: $NAMESPACE"

# Create temporary secret
kubectl create secret generic $SECRET_NAME \
  --namespace=$NAMESPACE \
  --dry-run=client \
  --output=yaml \
  $KEY_VALUE_PAIRS > temp-secret.yaml

# Seal the secret
kubeseal --format=yaml < temp-secret.yaml > sealed-secrets/$NAMESPACE-$SECRET_NAME.yaml

# Cleanup
rm temp-secret.yaml

echo "Sealed secret created: sealed-secrets/$NAMESPACE-$SECRET_NAME.yaml"
# Example sealed secret
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: web-app-secrets
  namespace: production
spec:
  encryptedData:
    DATABASE_URL: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEQAx...
    API_KEY: Ag+4j8/r4n8I2Dv+Jw5X+e3tY7u6i9o0p1q2w...
  template:
    metadata:
      name: web-app-secrets
      namespace: production
    type: Opaque

CI/CD Integration with GitOps

GitHub Actions for Image Updates

# .github/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      image-tag: ${{ steps.meta.outputs.tags }}
      image-digest: ${{ steps.build.outputs.digest }}
    steps:
      - uses: actions/checkout@v4

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=sha,prefix={{branch}}-

      - name: Build and push
        id: build
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}

  update-manifests:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
        with:
          repository: your-org/k8s-manifests
          token: ${{ secrets.MANIFEST_REPO_TOKEN }}

      - name: Update image tag
        run: |
          cd applications/web-app/overlays/staging

          # Extract just the tag from the full image reference
          NEW_TAG=$(echo "${{ needs.build.outputs.image-tag }}" | cut -d: -f2)

          # Update kustomization.yaml
          yq eval ".images[0].newTag = \"$NEW_TAG\"" -i kustomization.yaml

      - name: Commit and push
        run: |
          git config --local user.email "action@github.com"
          git config --local user.name "GitHub Action"
          git add .
          git commit -m "Update web-app image to ${{ needs.build.outputs.image-tag }}" || exit 0
          git push

  security-scan:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ needs.build.outputs.image-tag }}
          format: 'sarif'
          output: 'trivy-results.sarif'

      - name: Upload Trivy scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

Automatic Promotion Pipeline

# .github/workflows/promote.yml
name: Promote to Production

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Source environment'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          repository: your-org/k8s-manifests
          token: ${{ secrets.MANIFEST_REPO_TOKEN }}

      - name: Get source image tag
        id: source-tag
        run: |
          SOURCE_TAG=$(yq eval '.images[0].newTag' applications/web-app/overlays/${{ github.event.inputs.environment }}/kustomization.yaml)
          echo "tag=$SOURCE_TAG" >> $GITHUB_OUTPUT

      - name: Update production manifests
        run: |
          cd applications/web-app/overlays/production
          yq eval ".images[0].newTag = \"${{ steps.source-tag.outputs.tag }}\"" -i kustomization.yaml

      - name: Create pull request
        uses: peter-evans/create-pull-request@v5
        with:
          token: ${{ secrets.MANIFEST_REPO_TOKEN }}
          commit-message: 'Promote web-app to production: ${{ steps.source-tag.outputs.tag }}'
          title: 'πŸš€ Promote web-app to production'
          body: |
            ## Promotion Request

            Promoting web-app from **${{ github.event.inputs.environment }}** to **production**

            **Image Tag:** `${{ steps.source-tag.outputs.tag }}`

            ### Pre-deployment Checklist
            - [ ] All tests passed in staging
            - [ ] Security scan completed
            - [ ] Performance tests validated
            - [ ] Database migrations reviewed

            ### Deployment Plan
            - [ ] Deploy during maintenance window
            - [ ] Monitor error rates post-deployment
            - [ ] Verify all health checks

          branch: promote/web-app-${{ steps.source-tag.outputs.tag }}
          delete-branch: true

Monitoring and Observability

ArgoCD Metrics and Alerts

# monitoring/argocd-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: argocd-alerts
  namespace: argocd
spec:
  groups:
    - name: argocd.rules
      rules:
        - alert: ArgoCDAppNotSynced
          expr: |
            argocd_app_info{sync_status!="Synced"} == 1
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: 'ArgoCD application {{ $labels.name }} is not synced'
            description: 'ArgoCD application {{ $labels.name }} in namespace {{ $labels.namespace }} has been out of sync for more than 5 minutes.'

        - alert: ArgoCDAppHealthDegraded
          expr: |
            argocd_app_info{health_status!~"Healthy|Progressing"} == 1
          for: 2m
          labels:
            severity: critical
          annotations:
            summary: 'ArgoCD application {{ $labels.name }} health is degraded'
            description: 'ArgoCD application {{ $labels.name }} in namespace {{ $labels.namespace }} has health status {{ $labels.health_status }}.'

        - alert: ArgoCDSyncFailed
          expr: |
            increase(argocd_app_sync_total{phase="Failed"}[5m]) > 0
          for: 0m
          labels:
            severity: critical
          annotations:
            summary: 'ArgoCD sync failed for {{ $labels.name }}'
            description: 'ArgoCD application {{ $labels.name }} sync failed. Check application details for error messages.'

Custom Health Checks

# Custom health check for applications
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  resource.customizations.health.networking.k8s.io_Ingress: |
    hs = {}
    hs.status = "Healthy"

    if obj.status ~= nil then
      if obj.status.loadBalancer ~= nil then
        if obj.status.loadBalancer.ingress ~= nil and table.getn(obj.status.loadBalancer.ingress) > 0 then
          hs.status = "Healthy"
          hs.message = "Ingress has been assigned an IP/hostname"
        else
          hs.status = "Progressing"
          hs.message = "Waiting for ingress IP/hostname assignment"
        end
      end
    end

    return hs

  resource.customizations.health.apps_Deployment: |
    hs = {}
    if obj.status ~= nil then
      if obj.status.updatedReplicas ~= nil and obj.status.replicas ~= nil and obj.status.updatedReplicas == obj.status.replicas then
        if obj.status.readyReplicas ~= nil and obj.status.readyReplicas == obj.status.replicas then
          hs.status = "Healthy"
          hs.message = "Deployment is healthy"
        else
          hs.status = "Progressing"
          hs.message = "Waiting for deployment to be ready"
        end
      else
        hs.status = "Progressing"
        hs.message = "Waiting for rollout to finish"
      end
    end
    return hs

Advanced Deployment Patterns

Blue-Green Deployments with ArgoCD

# Blue-Green deployment using ArgoCD Rollouts
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: web-app-rollout
spec:
  replicas: 5
  strategy:
    blueGreen:
      # Service that the rollout modifies as the active service
      activeService: web-app-active
      # Service that the rollout modifies as the preview service
      previewService: web-app-preview
      # Auto promotion after successful checks
      autoPromotionEnabled: false
      # Manual promotion with analysis
      prePromotionAnalysis:
        templates:
          - templateName: success-rate
        args:
          - name: service-name
            value: web-app-preview
      # Post promotion analysis
      postPromotionAnalysis:
        templates:
          - templateName: success-rate
        args:
          - name: service-name
            value: web-app-active
      # Time to wait before scaling down old version
      scaleDownDelaySeconds: 30
      # Time to wait before scaling down old version after promotion
      prePromotionAnalysisRunDelay: 5
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: web-app:latest
          ports:
            - containerPort: 8080

Canary Deployments with Analysis

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: web-app-canary
spec:
  replicas: 10
  strategy:
    canary:
      steps:
        - setWeight: 10
        - pause: {}
        - setWeight: 20
        - pause: { duration: 60s }
        - analysis:
            templates:
              - templateName: success-rate
              - templateName: latency
            args:
              - name: service-name
                value: web-app
        - setWeight: 50
        - pause: { duration: 120s }
        - setWeight: 80
        - pause: { duration: 180s }

      # Traffic management
      trafficRouting:
        nginx:
          stableIngress: web-app-stable
          canaryIngress: web-app-canary

      # Analysis configuration
      analysis:
        templates:
          - templateName: success-rate
        args:
          - name: service-name
            value: web-app
          - name: prometheus-url
            value: http://prometheus:9090
  selector:
    matchLabels:
      app: web-app
  template:
    metadata:
      labels:
        app: web-app
    spec:
      containers:
        - name: web-app
          image: web-app:latest

---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
    - name: service-name
    - name: prometheus-url
      value: http://prometheus:9090
  metrics:
    - name: success-rate
      interval: 30s
      count: 3
      successCondition: result[0] >= 0.95
      failureLimit: 2
      provider:
        prometheus:
          address: '{{args.prometheus-url}}'
          query: |
            sum(rate(http_requests_total{job="{{args.service-name}}",status=~"2.."}[5m])) /
            sum(rate(http_requests_total{job="{{args.service-name}}"}[5m]))

Troubleshooting and Best Practices

Common Issues and Solutions

1. Sync Issues

#!/bin/bash
# scripts/troubleshoot-sync.sh

APP_NAME=$1
NAMESPACE=${2:-argocd}

echo "πŸ” Troubleshooting ArgoCD application: $APP_NAME"

# Check application status
echo "πŸ“Š Application Status:"
kubectl get application $APP_NAME -n $NAMESPACE -o yaml

# Check sync operation details
echo "πŸ”„ Sync Operation Details:"
argocd app get $APP_NAME --refresh

# Check resource differences
echo "πŸ“‹ Resource Differences:"
argocd app diff $APP_NAME

# Check events
echo "πŸ“… Recent Events:"
kubectl get events -n $NAMESPACE --field-selector involvedObject.name=$APP_NAME --sort-by='.lastTimestamp'

# Suggest actions
echo "πŸ”§ Suggested Actions:"
echo "1. Force refresh: argocd app get $APP_NAME --refresh --hard-refresh"
echo "2. Manual sync: argocd app sync $APP_NAME"
echo "3. Check source repo: git log --oneline -10"
echo "4. Validate manifests: kubectl apply --dry-run=client -f manifests/"

2. Performance Optimization

# ArgoCD performance configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cm
  namespace: argocd
data:
  # Increase concurrent processing
  application.operation.parallelism: '10'

  # Optimize repository caching
  repository.cache.expiration: '24h'

  # Application discovery optimization
  application.discovery.parallelism: '10'

  # Resource tracking optimization
  resource.tracking.method: annotation+label

  # Increase timeout for large applications
  timeout.reconciliation: '300s'
  timeout.hard.reconciliation: '0'

Security Best Practices

# ArgoCD RBAC configuration
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # Platform team - full access
    p, role:platform-admin, applications, *, */*, allow
    p, role:platform-admin, clusters, *, *, allow
    p, role:platform-admin, repositories, *, *, allow

    # Development team - limited access
    p, role:developer, applications, get, */*, allow
    p, role:developer, applications, sync, default/*, allow
    p, role:developer, applications, sync, development/*, allow

    # Production team - production only
    p, role:prod-admin, applications, *, production/*, allow
    p, role:prod-admin, applications, get, */*, allow

    # Group mappings
    g, platform-team, role:platform-admin
    g, developers, role:developer
    g, production-team, role:prod-admin

Key Takeaways and Best Practices

βœ… Do's

  1. Start Simple: Begin with basic GitOps patterns before adding complexity
  2. Structure Repositories: Organize manifests logically with clear separation
  3. Use Projects: Leverage ArgoCD projects for multi-tenancy and security
  4. Monitor Everything: Set up comprehensive monitoring and alerting
  5. Automate Safely: Use analysis templates for automated deployments
  6. Document Processes: Maintain clear runbooks and procedures

❌ Don'ts

  1. Don't Skip Validation: Always validate manifests before committing
  2. Don't Ignore Drift: Set up alerts for configuration drift
  3. Don't Over-Automate: Some deployments should require manual approval
  4. Don't Forget Security: Implement proper RBAC and secret management
  5. Don't Skip Testing: Test GitOps workflows in non-production environments

Conclusion

GitOps with ArgoCD has transformed how I manage Kubernetes deployments. The declarative approach, combined with Git's audit trail and ArgoCD's powerful reconciliation engine, creates a deployment system that is:

  • Reliable: Automatic drift detection and correction
  • Auditable: Complete history of all changes
  • Secure: Pull-based deployment model
  • Scalable: Supports complex multi-environment setups

The patterns I've shared here are battle-tested in production environments managing hundreds of applications across multiple clusters. Start with the basics, gradually add complexity, and always prioritize security and observability.

Remember: GitOps is not just about toolsβ€”it's about establishing a culture of declarative, version-controlled infrastructure that brings predictability and reliability to your deployments.

What GitOps challenges have you encountered? I'd love to hear about your experiences and the patterns that have worked for you!


Next Post Preview: In my next article, I'll explore "Kubernetes Security: Implementing Zero-Trust Architecture in Production."


Tags: #GitOps #ArgoCD #Kubernetes #DevOps #ContinuousDeployment #CloudNative

Let's learn a new thing every day
Get notified about new DevOps articles and cloud infrastructure insights
Buy Me A Coffee
Β© 2025 Bhakta Thapa