| name | argocd-helm |
| description | ArgoCD and Helm expert skill. Use when deploying applications with ArgoCD, creating or reviewing Helm charts, designing GitOps workflows, managing ApplicationSets, multi-cluster deployments, progressive delivery with Argo Rollouts, troubleshooting sync issues, secrets management (SOPS, External Secrets Operator), and Kubernetes manifest management. Covers ArgoCD 3.x and Helm 3.x best practices. |
You are operating as a Principal GitOps Engineer with 10+ years of Kubernetes experience, specializing in ArgoCD and Helm for production deployments.
ArgoCD 3.x Setup & Architecture
Quick Install
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.1.9/manifests/install.yaml
kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d
kubectl port-forward svc/argocd-server -n argocd 8080:443
ArgoCD 3.x Breaking Changes
- Annotation-based tracking is now default (was labels)
- RBAC logs enforcement enabled by default
- Legacy metrics removed
- Fine-grained RBAC (per-resource permissions)
- Secrets operators endorsement
Application Manifest
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: default
source:
repoURL: https://github.com/org/repo.git
targetRevision: HEAD
path: k8s/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: my-app
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
Sync Waves & Hooks
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
metadata:
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
Helm Chart Development
Chart Structure
mychart/
├── Chart.yaml
├── Chart.lock
├── values.yaml
├── values.schema.json # JSON schema for values validation
├── .helmignore
├── templates/
│ ├── _helpers.tpl # Template helpers
│ ├── NOTES.txt # Post-install notes
│ ├── deployment.yaml
│ ├── service.yaml
│ ├── ingress.yaml
│ ├── hpa.yaml
│ ├── pdb.yaml
│ ├── serviceaccount.yaml
│ ├── configmap.yaml
│ ├── secret.yaml
│ └── tests/
│ └── test-connection.yaml
└── charts/ # Dependencies
Chart.yaml
apiVersion: v2
name: my-service
description: A Helm chart for my-service
type: application
version: 1.0.0
appVersion: "2.0.0"
dependencies:
- name: postgresql
version: "~15.0"
repository: https://charts.bitnami.com/bitnami
condition: postgresql.enabled
values.yaml Best Practices
replicaCount: 2
image:
repository: ghcr.io/org/my-service
pullPolicy: IfNotPresent
tag: ""
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
podDisruptionBudget:
enabled: true
minAvailable: 1
serviceAccount:
create: true
annotations: {}
name: ""
ingress:
enabled: false
className: nginx
annotations: {}
hosts:
- host: example.com
paths:
- path: /
pathType: Prefix
tls: []
Template Helpers (_helpers.tpl)
{{- define "mychart.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- define "mychart.labels" -}}
helm.sh/chart: {{ include "mychart.chart" . }}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
app.kubernetes.io/version: {{ .Values.image.tag | default .Chart.AppVersion | quote }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{- define "mychart.selectorLabels" -}}
app.kubernetes.io/name: {{ include "mychart.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
Production Deployment Template
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "mychart.fullname" . }}
labels:
{{- include "mychart.labels" . | nindent 4 }}
spec:
{{- if not .Values.autoscaling.enabled }}
replicas: {{ .Values.replicaCount }}
{{- end }}
selector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
labels:
{{- include "mychart.labels" . | nindent 8 }}
spec:
serviceAccountName: {{ include "mychart.serviceAccountName" . }}
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: {{ .Chart.Name }}
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
ports:
- name: http
containerPort: {{ .Values.service.port }}
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: http
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: /readyz
port: http
initialDelaySeconds: 5
periodSeconds: 10
resources:
{{- toYaml .Values.resources | nindent 12 }}
env:
{{- range $key, $value := .Values.env }}
- name: {{ $key }}
value: {{ $value | quote }}
{{- end }}
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
{{- include "mychart.selectorLabels" . | nindent 14 }}
ApplicationSets (Multi-Cluster)
Cluster Generator
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
namespace: argocd
spec:
generators:
- clusters:
selector:
matchLabels:
environment: production
template:
metadata:
name: '{{name}}-my-app'
spec:
project: default
source:
repoURL: https://github.com/org/repo.git
targetRevision: HEAD
path: 'k8s/overlays/{{metadata.labels.environment}}'
destination:
server: '{{server}}'
namespace: my-app
Git Directory Generator
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: apps
spec:
generators:
- git:
repoURL: https://github.com/org/repo.git
revision: HEAD
directories:
- path: apps/*
template:
metadata:
name: '{{path.basename}}'
spec:
source:
repoURL: https://github.com/org/repo.git
path: '{{path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{path.basename}}'
Matrix Generator (Cluster x App)
spec:
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
environment: production
- git:
repoURL: https://github.com/org/repo.git
directories:
- path: apps/*
Repository Structure
Monorepo (App of Apps)
gitops-repo/
├── apps/ # Application definitions
│ ├── frontend/
│ │ ├── base/
│ │ │ ├── deployment.yaml
│ │ │ ├── service.yaml
│ │ │ └── kustomization.yaml
│ │ └── overlays/
│ │ ├── dev/
│ │ ├── staging/
│ │ └── production/
│ └── backend/
├── infrastructure/ # Cluster infrastructure
│ ├── ingress-nginx/
│ ├── cert-manager/
│ ├── monitoring/
│ └── external-secrets/
├── argocd/ # ArgoCD config
│ ├── projects/
│ ├── applications/
│ └── applicationsets/
└── charts/ # Custom Helm charts
└── my-service/
Helm with ArgoCD
spec:
source:
repoURL: https://github.com/org/charts.git
path: charts/my-service
targetRevision: HEAD
helm:
valueFiles:
- values.yaml
- values-production.yaml
parameters:
- name: image.tag
value: "v2.0.0"
Progressive Delivery
Argo Rollouts - Canary
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app
spec:
replicas: 5
strategy:
canary:
steps:
- setWeight: 20
- pause: {duration: 2m}
- analysis:
templates:
- templateName: success-rate
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 100
selector:
matchLabels:
app: my-app
template:
Argo Rollouts - Blue-Green
spec:
strategy:
blueGreen:
activeService: my-app-active
previewService: my-app-preview
autoPromotionEnabled: false
prePromotionAnalysis:
templates:
- templateName: smoke-test
Secrets Management
SOPS + age (Recommended)
age-keygen -o key.txt
creation_rules:
- path_regex: .*.enc.yaml
encrypted_regex: ^(data|stringData)$
age: age1ql3z7hjy54pw3hyww5ayyfg...
sops -e secret.yaml > secret.enc.yaml
External Secrets Operator
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: my-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: my-secret
data:
- secretKey: password
remoteRef:
key: /prod/my-app/db-password
Troubleshooting
ArgoCD OutOfSync
argocd app diff my-app
argocd app sync my-app --force
argocd app get my-app --hard-refresh
argocd app resources my-app
Common Issues
| Issue | Cause | Fix |
|---|
| OutOfSync loop | Mutating webhooks, server-side defaults | Add ignoreDifferences |
| Sync failed | RBAC, invalid manifests | Check argocd app sync logs |
| Health degraded | Probe failures, resource limits | Check pod events/logs |
| Helm values not applied | Wrong valueFiles path | Verify path in Application spec |
| Pruning unexpected | Label/annotation mismatch | Check tracking method |
ignoreDifferences (for fields modified by controllers)
spec:
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas
- group: ""
kind: Service
jqPathExpressions:
- .spec.clusterIP
Helm Commands Quick Reference
helm create my-chart
helm lint my-chart/
helm template my-release my-chart/ -f values-prod.yaml
helm install my-release my-chart/ -n my-namespace --create-namespace
helm upgrade my-release my-chart/ -f values-prod.yaml
helm rollback my-release 1
helm list -A
helm show values my-chart/
helm dependency update my-chart/
helm test my-release
ArgoCD CLI Quick Reference
argocd login argocd.example.com --grpc-web
argocd app list
argocd app get my-app
argocd app sync my-app
argocd app diff my-app
argocd app history my-app
argocd app rollback my-app <history-id>
argocd app delete my-app --cascade
argocd cluster add my-context
argocd cluster list
argocd repo add https://github.com/org/repo.git --ssh-private-key-path ~/.ssh/id_ed25519
Helm Chart Quality Checklist
For detailed references see references/gitops-patterns.md