GitOps workflows and patterns using ArgoCD and Flux for declarative Kubernetes deployments. Use when implementing CI/CD for Kubernetes, managing multi-environment deployments, or adopting declarative infrastructure practices.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
Expert guidance for implementing production-grade GitOps workflows using ArgoCD and Flux CD, covering declarative deployment patterns, progressive delivery strategies, multi-environment management, and secure secret handling for Kubernetes infrastructure.
1. Declarative
2. Versioned and Immutable
3. Pulled Automatically
4. Continuously Reconciled
Operational advantages:
Developer advantages:
Structure:
gitops-repo/
├── apps/
│ ├── production/
│ │ ├── frontend/
│ │ │ ├── kustomization.yaml
│ │ │ ├── deployment.yaml
│ │ │ └── service.yaml
│ │ ├── backend/
│ │ └── database/
│ ├── staging/
│ └── development/
├── infrastructure/
│ ├── ingress-nginx/
│ ├── cert-manager/
│ ├── external-secrets/
│ └── monitoring/
│ ├── prometheus/
│ └── grafana/
├── clusters/
│ ├── production/
│ │ └── cluster-config.yaml
│ ├── staging/
│ └── development/
└── base/
├── apps/
└── infrastructure/
Characteristics:
Best for: Organizations with unified platform teams, shared infrastructure components, need for consistency across environments
Structure:
# Infrastructure repo
infrastructure-gitops/
├── clusters/
│ ├── production/
│ └── staging/
├── shared/
│ ├── ingress/
│ ├── monitoring/
│ └── security/
└── argocd/
└── applications/
# Application repos
app-frontend-gitops/
├── base/
│ ├── deployment.yaml
│ └── service.yaml
└── overlays/
├── production/
└── staging/
app-backend-gitops/
├── helm/
│ └── values-{env}.yaml
└── manifests/
Characteristics:
Best for: Large organizations, independent teams, microservices architectures, different compliance requirements per service
Structure:
app-gitops/
├── main (development)
├── staging (staging env)
└── production (production env)
Each branch contains:
├── manifests/
│ ├── deployment.yaml
│ └── service.yaml
└── config/
└── values.yaml
Characteristics:
Considerations:
Best for: Small teams, simple applications, teams familiar with Git flow
Standard installation:
# Install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Access UI
kubectl port-forward svc/argocd-server -n argocd 8080:443
# Get initial password
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
Production ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: argocd-server-ingress
namespace: argocd
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-passthrough: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
ingressClassName: nginx
rules:
- host: argocd.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: argocd-server
port:
number: 443
tls:
- hosts:
- argocd.example.com
secretName: argocd-tls
Basic application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: frontend-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io # Enable cascading delete
spec:
project: production
source:
repoURL: https://github.com/org/gitops-repo
targetRevision: main
path: apps/production/frontend
# Optional: Helm values
helm:
valueFiles:
- values-production.yaml
parameters:
- name: image.tag
value: v1.2.3
# Optional: Kustomize
kustomize:
images:
- gcr.io/org/frontend:v1.2.3
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Delete resources removed from Git
selfHeal: true # Reconcile manual changes
allowEmpty: false # Prevent accidental empty sync
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore HPA-managed replicas
Root application:
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: applications
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/org/gitops-repo
targetRevision: main
path: argocd/applications
directory:
recurse: true
jsonnet: {}
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
prune: true
selfHeal: true
Application structure:
argocd/
├── applications/
│ ├── infrastructure.yaml # Infrastructure apps
│ ├── production-apps.yaml # Production apps
│ └── staging-apps.yaml # Staging apps
└── projects/
├── infrastructure.yaml
├── production.yaml
└── staging.yaml
Multi-cluster deployment:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: frontend-multicluster
namespace: argocd
spec:
generators:
- list:
elements:
- cluster: production-east
url: https://prod-east.k8s.example.com
environment: production
- cluster: production-west
url: https://prod-west.k8s.example.com
environment: production
- cluster: staging
url: https://staging.k8s.example.com
environment: staging
template:
metadata:
name: 'frontend-{{cluster}}'
spec:
project: '{{environment}}'
source:
repoURL: https://github.com/org/gitops-repo
targetRevision: main
path: 'apps/{{environment}}/frontend'
destination:
server: '{{url}}'
namespace: frontend
syncPolicy:
automated:
prune: true
selfHeal: true
Git directory generator:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: all-apps
spec:
generators:
- git:
repoURL: https://github.com/org/gitops-repo
revision: main
directories:
- path: apps/production/*
template:
metadata:
name: '{{path.basename}}'
spec:
source:
repoURL: https://github.com/org/gitops-repo
targetRevision: main
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{path.basename}}'
syncPolicy:
automated: {}
Bootstrap Flux with GitHub:
# Install Flux CLI
curl -s https://fluxcd.io/install.sh | sudo bash
# Bootstrap Flux (creates repo structure)
flux bootstrap github \
--owner=org \
--repository=gitops-repo \
--branch=main \
--path=clusters/production \
--personal=false \
--token-auth
Bootstrap result:
clusters/production/
├── flux-system/
│ ├── gotk-components.yaml
│ ├── gotk-sync.yaml
│ └── kustomization.yaml
GitRepository source:
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
name: frontend-app
namespace: flux-system
spec:
interval: 1m
url: https://github.com/org/frontend-app
ref:
branch: main
secretRef:
name: github-token
ignore: |
# Exclude non-deployment files
/*.md
/docs/
HelmRepository source:
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: bitnami
namespace: flux-system
spec:
interval: 10m
url: https://charts.bitnami.com/bitnami
OCIRepository source:
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: OCIRepository
metadata:
name: app-manifests
namespace: flux-system
spec:
interval: 5m
url: oci://ghcr.io/org/manifests
ref:
tag: latest
Application deployment:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: frontend-app
namespace: flux-system
spec:
interval: 5m
path: ./deploy/production
prune: true
wait: true
timeout: 5m
sourceRef:
kind: GitRepository
name: frontend-app
healthChecks:
- apiVersion: apps/v1
kind: Deployment
name: frontend
namespace: production
patches:
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
replicas: 5
target:
kind: Deployment
name: frontend
Dependency management:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app-stack
namespace: flux-system
spec:
interval: 5m
path: ./apps
prune: true
sourceRef:
kind: GitRepository
name: gitops-repo
# Wait for infrastructure
dependsOn:
- name: infrastructure
- name: databases
Deploying Helm charts:
apiVersion: helm.toolkit.fluxcd.io/v2beta1
kind: HelmRelease
metadata:
name: nginx-ingress
namespace: flux-system
spec:
interval: 10m
chart:
spec:
chart: ingress-nginx
version: '4.8.x'
sourceRef:
kind: HelmRepository
name: ingress-nginx
namespace: flux-system
values:
controller:
replicaCount: 3
metrics:
enabled: true
# Override values from ConfigMap
valuesFrom:
- kind: ConfigMap
name: nginx-values
valuesKey: values.yaml
Branch-based flow:
# Promote staging to production
git checkout main
git merge staging --no-ff -m "Promote staging to production"
git push origin main
# ArgoCD/Flux automatically sync production
Tag-based flow:
# Development tracks main
source:
targetRevision: main
# Staging tracks release candidates
source:
targetRevision: v1.2.3-rc1
# Production tracks stable releases
source:
targetRevision: v1.2.3
Implementation:
# Create release candidate
git tag v1.2.3-rc1
git push origin v1.2.3-rc1
# After validation, promote to production
git tag v1.2.3
git push origin v1.2.3
Base configuration:
# base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: app
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: app:latest
Environment overlays:
# overlays/staging/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
images:
- name: app
newTag: v1.2.3-staging
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: 2
target:
kind: Deployment
# overlays/production/kustomization.yaml
images:
- name: app
newTag: v1.2.3 # Promote by updating tag
patches:
- patch: |-
- op: replace
path: /spec/replicas
value: 5
ImageRepository:
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageRepository
metadata:
name: frontend
namespace: flux-system
spec:
image: gcr.io/org/frontend
interval: 1m
ImagePolicy:
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImagePolicy
metadata:
name: frontend-policy
namespace: flux-system
spec:
imageRepositoryRef:
name: frontend
policy:
semver:
range: 1.x.x # Only stable 1.x releases
ImageUpdateAutomation:
apiVersion: image.toolkit.fluxcd.io/v1beta1
kind: ImageUpdateAutomation
metadata:
name: frontend-automation
namespace: flux-system
spec:
interval: 5m
sourceRef:
kind: GitRepository
name: gitops-repo
git:
checkout:
ref:
branch: main
commit:
author:
email: fluxcd@example.com
name: Flux CD
messageTemplate: |
Update {{range .Updated.Images}}{{println .}}{{end}}
push:
branch: main
update:
path: ./apps/production
strategy: Setters
Install controller:
kubectl apply -f https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.24.0/controller.yaml
# Install kubeseal CLI
brew install kubeseal
Encrypt secrets:
# Create regular secret
kubectl create secret generic db-credentials \
--from-literal=username=admin \
--from-literal=password=supersecret \
--dry-run=client -o yaml > secret.yaml
# Encrypt for cluster
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
# Commit sealed-secret.yaml to Git
git add sealed-secret.yaml
git commit -m "Add encrypted database credentials"
SealedSecret manifest:
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
spec:
encryptedData:
username: AgBj7V8X...
password: AgCK9Qw2...
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
Install ESO:
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
SecretStore (AWS Secrets Manager):
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
ExternalSecret:
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: username
remoteRef:
key: production/database/credentials
property: username
- secretKey: password
remoteRef:
key: production/database/credentials
property: password
ClusterSecretStore (shared):
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
Setup (with age encryption):
# Install SOPS
brew install sops
# Install age
brew install age
# Generate age key
age-keygen -o key.txt
# Configure .sops.yaml
cat <<EOF > .sops.yaml
creation_rules:
- path_regex: .*/production/.*
age: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
EOF
Encrypt secrets:
# Create secret
cat <<EOF > secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: db-credentials
stringData:
username: admin
password: supersecret
EOF
# Encrypt with SOPS
sops --encrypt --in-place secret.yaml
# Commit encrypted file
git add secret.yaml
Flux integration:
# Kustomization with SOPS
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: app
spec:
decryption:
provider: sops
secretRef:
name: sops-age
sourceRef:
kind: GitRepository
name: gitops-repo
Rollout resource:
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: frontend
spec:
replicas: 10
revisionHistoryLimit: 3
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: frontend:v2
strategy:
canary:
steps:
- setWeight: 10 # 10% traffic to new version
- pause: {duration: 2m} # Wait 2 minutes
- setWeight: 25
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 75
- pause: {duration: 5m}
# Automated analysis
analysis:
templates:
- templateName: success-rate
startingStep: 2
args:
- name: service-name
value: frontend
# Traffic routing
trafficRouting:
istio:
virtualService:
name: frontend
routes:
- primary
Analysis template:
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
count: 5
successCondition: result[0] >= 0.95
failureLimit: 3
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(
http_requests_total{service="{{args.service-name}}",status!~"5.."}[1m]
)) /
sum(rate(
http_requests_total{service="{{args.service-name}}"}[1m]
))
Install Flagger:
kubectl apply -k github.com/fluxcd/flagger//kustomize/istio
Canary resource:
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: frontend
namespace: production
spec:
targetRef:
apiVersion: apps/v1
kind: Deployment
name: frontend
service:
port: 80
targetPort: 8080
analysis:
interval: 1m
threshold: 5 # Number of iterations
maxWeight: 50 # Max traffic to canary
stepWeight: 10 # Traffic increase per iteration
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 1m
- name: request-duration
thresholdRange:
max: 500
interval: 1m
webhooks:
- name: load-test
url: http://flagger-loadtester/
timeout: 5s
metadata:
cmd: "hey -z 1m -q 10 -c 2 http://frontend-canary/"
Revert commit:
# Find problematic commit
git log --oneline
# Revert specific commit
git revert abc123
git push origin main
# ArgoCD/Flux automatically syncs rollback
Rollback to previous tag:
# Production currently on v1.2.3
# Rollback to v1.2.2
git checkout production
git reset --hard v1.2.2
git push --force origin production
# Update Application targetRevision
kubectl patch application frontend \
-n argocd \
--type merge \
-p '{"spec":{"source":{"targetRevision":"v1.2.2"}}}'
History and rollback:
# View deployment history
argocd app history frontend
# Rollback to specific revision
argocd app rollback frontend 5
# Rollback to previous revision
argocd app rollback frontend
Automatic rollback:
# Rollout with automatic rollback
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
strategy:
canary:
analysis:
templates:
- templateName: success-rate
startingStep: 1
# Auto-rollback on analysis failure
abortScaleDownDelaySeconds: 30
Suspend automation:
# Suspend Kustomization
flux suspend kustomization frontend
# Manually rollback Deployment
kubectl rollout undo deployment/frontend -n production
# Resume after validation
flux resume kustomization frontend