Gsd-skill-creator gitops-patterns

Provides GitOps best practices for ArgoCD, Flux, Argo Rollouts, and progressive delivery strategies. Use when setting up GitOps workflows, configuring continuous delivery, managing Kubernetes deployments declaratively, or when user mentions 'gitops', 'argocd', 'flux', 'progressive delivery', 'reconciliation', 'argo rollouts', 'canary', 'sealed secrets'.

install
source · Clone the upstream repo
git clone https://github.com/Tibsfox/gsd-skill-creator
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/Tibsfox/gsd-skill-creator "$T" && mkdir -p ~/.claude/skills && cp -r "$T/examples/skills/patterns/gitops-patterns" ~/.claude/skills/tibsfox-gsd-skill-creator-gitops-patterns && rm -rf "$T"
manifest: examples/skills/patterns/gitops-patterns/SKILL.md
source content

GitOps Patterns

Best practices for operating infrastructure and applications through Git as the single source of truth. Covers ArgoCD, Flux, Argo Rollouts, environment promotion, secret management, and progressive delivery.

GitOps Principles

GitOps extends Infrastructure as Code by adding a reconciliation loop that continuously ensures the live system matches the declared state in Git.

PrincipleDescriptionImplementation
DeclarativeEntire system described declarativelyKubernetes manifests, Helm charts, Kustomize overlays
Versioned and immutableDesired state stored in GitGit commits as audit trail, tags for releases
Pulled automaticallySoftware agents pull desired stateArgoCD, Flux controllers watch Git repos
Continuously reconciledAgents correct drift automaticallyReconciliation loop detects and fixes divergence
GitOps vs Traditional CI/CDGitOpsTraditional CI/CD
Deployment triggerGit commit (pull-based)Pipeline step (push-based)
Source of truthGit repositoryPipeline state / manual
Drift handlingAuto-correctedUndetected until next deploy
Rollback method
git revert
Re-run pipeline with old version
Audit trailGit logPipeline logs (often ephemeral)
Credential exposureCluster-only (no CI secrets)CI system has deploy credentials

Repository Structure

Separate application source code from deployment manifests. This is the most critical architectural decision in GitOps.

# Deployment repo (gitops-deploy) -- separate from application source
gitops-deploy/
  apps/
    backend/
      base/
        deployment.yaml
        service.yaml
        kustomization.yaml
      overlays/
        dev/
          kustomization.yaml       # image: backend:dev-abc1234
        staging/
          kustomization.yaml       # image: backend:staging-def5678
        prod/
          kustomization.yaml       # image: backend:v1.2.3
          patches/
            replicas.yaml
            hpa.yaml
    frontend/
      base/
      overlays/
  infrastructure/
    cert-manager/
    ingress-nginx/
    monitoring/
  clusters/
    dev/
      kustomization.yaml          # References apps/*/overlays/dev
    staging/
      kustomization.yaml
    prod/
      kustomization.yaml
Structure PatternWhen to UseTrade-offs
Monorepo (app + deploy)Small teams, single appSimple but couples build and deploy
Separate reposMultiple teams, compliance needsClean separation, more repos to manage
Branch-per-envSimple promotion modelRisk of branch divergence, merge conflicts
Directory-per-envKustomize overlaysRecommended: explicit, reviewable, no branch drift

ArgoCD Application Manifest

ArgoCD watches Git repositories and synchronizes Kubernetes cluster state to match.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: backend-prod
  namespace: argocd
  finalizers:
    - resources-finalizer.argocd.argoproj.io
spec:
  project: production

  source:
    repoURL: https://github.com/myorg/gitops-deploy.git
    targetRevision: main
    path: apps/backend/overlays/prod
    kustomize:
      images:
        - backend=ghcr.io/myorg/backend:v1.2.3

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

  syncPolicy:
    automated:
      prune: true          # Delete resources removed from Git
      selfHeal: true       # Revert manual changes in cluster
      allowEmpty: false
    syncOptions:
      - CreateNamespace=true
      - PrunePropagationPolicy=foreground
      - PruneLast=true
    retry:
      limit: 3
      backoff:
        duration: 5s
        factor: 2
        maxDuration: 3m

  ignoreDifferences:
    - group: apps
      kind: Deployment
      jsonPointers:
        - /spec/replicas   # Ignore HPA-managed replica count

ArgoCD ApplicationSet for Multi-Environment

apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
  name: backend
  namespace: argocd
spec:
  generators:
    - list:
        elements:
          - env: dev
            cluster: https://dev-cluster.example.com
            autoSync: true
          - env: staging
            cluster: https://staging-cluster.example.com
            autoSync: true
          - env: prod
            cluster: https://prod-cluster.example.com
            autoSync: false   # Prod requires manual sync
  template:
    metadata:
      name: 'backend-{{env}}'
    spec:
      project: '{{env}}'
      source:
        repoURL: https://github.com/myorg/gitops-deploy.git
        targetRevision: main
        path: 'apps/backend/overlays/{{env}}'
      destination:
        server: '{{cluster}}'
        namespace: backend
      syncPolicy:
        automated:
          prune: '{{autoSync}}'
          selfHeal: '{{autoSync}}'

Flux Kustomization

Flux uses native Kubernetes custom resources and supports multi-tenancy out of the box.

# Source: Git repository to watch
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: gitops-deploy
  namespace: flux-system
spec:
  interval: 1m
  url: https://github.com/myorg/gitops-deploy.git
  ref:
    branch: main
  secretRef:
    name: gitops-deploy-auth

---
# Kustomization: what to deploy from that source
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: backend-prod
  namespace: flux-system
spec:
  interval: 5m
  retryInterval: 2m
  timeout: 5m
  sourceRef:
    kind: GitRepository
    name: gitops-deploy
  path: ./apps/backend/overlays/prod
  prune: true
  targetNamespace: backend
  healthChecks:
    - apiVersion: apps/v1
      kind: Deployment
      name: backend
      namespace: backend
  dependsOn:
    - name: infrastructure
  postBuild:
    substituteFrom:
      - kind: ConfigMap
        name: cluster-config

---
# Image automation: watch for new tags, update Git
apiVersion: image.toolkit.fluxcd.io/v1beta2
kind: ImagePolicy
metadata:
  name: backend
  namespace: flux-system
spec:
  imageRepositoryRef:
    name: backend
  policy:
    semver:
      range: '>=1.0.0'

Progressive Delivery with Argo Rollouts

Argo Rollouts extends Kubernetes Deployments with canary releases, blue-green deployments, and automated analysis.

apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: backend
spec:
  replicas: 10
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
        - name: backend
          image: ghcr.io/myorg/backend:v1.2.3
          ports:
            - containerPort: 8080
          resources:
            requests: { cpu: 100m, memory: 128Mi }
            limits: { cpu: 500m, memory: 512Mi }

  strategy:
    canary:
      canaryService: backend-canary
      stableService: backend-stable
      trafficRouting:
        istio:
          virtualServices:
            - name: backend-vsvc
              routes: [primary]
      steps:
        - setWeight: 5
        - pause: { duration: 2m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 25
        - pause: { duration: 5m }
        - analysis:
            templates:
              - templateName: success-rate
        - setWeight: 50
        - pause: { duration: 5m }
        - setWeight: 100

---
# AnalysisTemplate -- automated success criteria
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
  name: success-rate
spec:
  args:
    - name: service-name
  metrics:
    - name: success-rate
      interval: 30s
      count: 5
      successCondition: result[0] >= 0.95
      failureLimit: 3
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            sum(rate(http_requests_total{service="{{args.service-name}}",status=~"2.."}[2m]))
            /
            sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))
    - name: p99-latency
      interval: 30s
      count: 5
      successCondition: result[0] <= 500
      failureLimit: 3
      provider:
        prometheus:
          address: http://prometheus.monitoring:9090
          query: |
            histogram_quantile(0.99,
              sum(rate(http_request_duration_ms_bucket{service="{{args.service-name}}"}[2m])) by (le)
            )

Reconciliation Loop

The reconciliation loop is the core mechanism that makes GitOps self-healing.

         +----------------+
         |   Git Repo     |
         | (desired state)|
         +-------+--------+
                 |  poll / webhook
         +-------+--------+
         |  GitOps Agent   |
         | (ArgoCD / Flux) |
         +-------+--------+
                 |  compare desired vs actual
         +-------+--------+
         |   Diff Engine   |----> No diff? Sleep, poll again.
         +-------+--------+
                 |  diff detected
         +-------+--------+
         |  Apply Changes  |
         +-------+--------+
                 |
         +-------+--------+
         | Health Checks   |----> Unhealthy? Retry or alert.
         +-------+--------+
                 |  healthy
         +-------+--------+
         |  Mark Synced    |
         +----------------+
SettingArgoCDFlux
Poll interval3m default
spec.interval
per resource
Webhook supportYes (GitHub, GitLab)Yes (Receiver CRD)
Self-heal
selfHeal: true
spec.force: true
Prune removed resources
prune: true
spec.prune: true
Retry on failure
retry.limit
with backoff
retryInterval

Environment Promotion Strategies

StrategyMechanismProsCons
PR-based promotionCI opens PR to update image tagFull review, audit trailManual merge step
Automated promotionFlux image automation or CI scriptFast, no manual gatesNeeds strong automated tests
Git tag promotionTag commit for promotionClear versioningComplex tag management
Branch promotionMerge dev -> staging -> prodFamiliar Git workflowBranch drift, merge conflicts

PR-Based Promotion Pipeline

name: Promote
on:
  workflow_dispatch:
    inputs:
      image_tag:
        description: 'Image tag to promote'
        required: true
      target_env:
        type: choice
        options: [staging, prod]

jobs:
  promote:
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
        with:
          repository: myorg/gitops-deploy
          token: ${{ secrets.GITOPS_PAT }}

      - name: Update image tag
        run: |
          cd apps/backend/overlays/${{ inputs.target_env }}
          kustomize edit set image \
            backend=ghcr.io/myorg/backend:${{ inputs.image_tag }}

      - uses: peter-evans/create-pull-request@v6
        with:
          title: "promote: backend ${{ inputs.image_tag }} to ${{ inputs.target_env }}"
          branch: "promote/backend-${{ inputs.target_env }}-${{ inputs.image_tag }}"
          labels: promotion,${{ inputs.target_env }}

Secret Management in GitOps

Secrets cannot be stored in plain text in Git. Multiple solutions exist with different trade-offs.

SolutionEncryptionWhere DecryptedExternal DepsComplexity
Sealed SecretsAsymmetric (cluster key)In-cluster controllerNoneLow
SOPS + age/KMSSymmetric or KMSCI or GitOps agentKMS optionalMedium
External Secrets OperatorNone (fetched at runtime)In-cluster controllerVault, AWS SMMedium
Vault CSI ProviderNone (injected at runtime)Sidecar / CSI driverHashiCorp VaultHigh

Sealed Secrets

# Create a regular secret, then seal it for safe Git storage
kubectl create secret generic db-credentials \
  --from-literal=username=admin \
  --from-literal=password=s3cret \
  --dry-run=client -o yaml | \
  kubeseal --format yaml \
    --controller-name sealed-secrets \
    --controller-namespace flux-system \
    > apps/backend/overlays/prod/sealed-db-credentials.yaml

External Secrets Operator

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-credentials
  namespace: backend
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets-manager
    kind: ClusterSecretStore
  target:
    name: db-credentials
    creationPolicy: Owner
  data:
    - secretKey: username
      remoteRef:
        key: prod/backend/db
        property: username
    - secretKey: password
      remoteRef:
        key: prod/backend/db
        property: password

SOPS with Flux Decryption

# Flux Kustomization with SOPS decryption
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: backend-prod
  namespace: flux-system
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: gitops-deploy
  path: ./apps/backend/overlays/prod
  prune: true
  decryption:
    provider: sops
    secretRef:
      name: sops-age-key   # Secret containing the age private key

Anti-Patterns

Anti-PatternProblemFix
Storing plain-text secrets in GitCredentials exposed in repo history foreverUse Sealed Secrets, SOPS, or External Secrets Operator
Push-based deploys alongside GitOpsCI
kubectl apply
bypasses reconciliation
Remove all
kubectl apply
from CI; only update Git
Single repo for app code and manifestsEvery code commit triggers deploy; noisy historySeparate application and deployment repositories
Branch-per-environmentBranches diverge, merge conflicts, unclear stateUse directory-per-environment with Kustomize overlays
Disabling self-heal / auto-pruneManual changes accumulate, cluster drifts from GitEnable
selfHeal: true
and
prune: true
No health checks in sync policySync marked complete before app is actually readyAdd health checks for Deployments, StatefulSets, Jobs
Manual
kubectl
in GitOps clusters
Reverted on next reconciliation or causes driftAll changes through Git PRs; restrict kubectl to read-only
Deploying directly to prodNo validation in lower environmentsRequire promotion path: dev -> staging -> prod
No RBAC on GitOps toolAny developer can sync any app to any namespaceUse ArgoCD AppProjects or Flux multi-tenancy
Image tag
latest
in manifests
Non-deterministic deploys, no rollback possibleUse immutable tags (SHA, semver, build ID)
No notification on sync failureFailed deployments go unnoticed for hoursConfigure alerts to Slack, PagerDuty, or email
Reconciliation interval too longDrift persists, delayed deploymentsSet interval to 1-5 minutes; use webhooks for instant sync

GitOps Readiness Checklist

  • All application manifests stored in a Git repository (not applied manually)
  • Deployment repository is separate from application source code
  • Directory-per-environment structure with Kustomize overlays or Helm values
  • ArgoCD or Flux installed and configured to watch the deployment repo
  • Auto-sync enabled with self-heal and prune for all environments
  • Health checks configured for all synced applications
  • Secret management solution deployed (Sealed Secrets, SOPS, or External Secrets)
  • No plain-text secrets committed to any Git repository
  • Environment promotion workflow defined (PR-based or automated)
  • Image tags are immutable (semver, SHA, or build ID -- never
    latest
    )
  • RBAC configured: AppProjects (ArgoCD) or namespaced Kustomizations (Flux)
  • Notifications configured for sync failures and degraded health
  • kubectl
    write access restricted in GitOps-managed clusters
  • Rollback tested:
    git revert
    triggers successful rollback deployment
  • Disaster recovery plan: GitOps agent recovery, state rebuild from Git
  • Progressive delivery configured for production (canary or blue-green)